关于require与require_once孰优孰劣其实已经有很多文章了。这些文章基本上都认为require_once对程序运行效率影响很大。那么,实际情况如何呢?
内核简单分析
一点基本知识
首先介绍一点基本知识。PHP中会使用一个名为AST的语法分析树。另外有两个经常看到的东西,一个叫EG,一个叫CG。EG和CG在源代码中定义如下:
/* Compiler */ #ifdef ZTS # define CG(v) ZEND_TSRMG(compiler_globals_id, zend_compiler_globals *, v) #else # define CG(v) (compiler_globals.v) extern ZEND_API struct _zend_compiler_globals compiler_globals; #endif ZEND_API int zendparse(void); /* Executor */ #ifdef ZTS # define EG(v) ZEND_TSRMG(executor_globals_id, zend_executor_globals *, v) #else # define EG(v) (executor_globals.v) extern ZEND_API zend_executor_globals executor_globals; #endif
从命名上就可以看出,CG是关于compiler的变量,EG是关于executor的变量。之所以单独定义一个EG/CG的宏,主要是考虑到线程安全版本(TS)和非线程安全版本(NTS)的差别
基本分析
其实eval、require的作用很相似,因此,Zend关于他们的操作,都集中在一个名为zend_include_or_eval
的函数中(zend_execute.c)
static zend_never_inline zend_op_array* ZEND_FASTCALL zend_include_or_eval(zval *inc_filename, int type) /* {{{ */ { zend_op_array *new_op_array = NULL; zval tmp_inc_filename; ZVAL_UNDEF(&tmp_inc_filename); if (Z_TYPE_P(inc_filename) != IS_STRING) { ZVAL_STR(&tmp_inc_filename, zval_get_string(inc_filename)); inc_filename = &tmp_inc_filename; } if (type != ZEND_EVAL && strlen(Z_STRVAL_P(inc_filename)) != Z_STRLEN_P(inc_filename)) { if (type == ZEND_INCLUDE_ONCE || type == ZEND_INCLUDE) { zend_message_dispatcher(ZMSG_FAILED_INCLUDE_FOPEN, Z_STRVAL_P(inc_filename)); } else { zend_message_dispatcher(ZMSG_FAILED_REQUIRE_FOPEN, Z_STRVAL_P(inc_filename)); }
首先是进行一些转换和判断方面的东西,接下来才进入正题。我们将eval、require/include和require_once/include_once分开进行分析。首先来看eval:
char *eval_desc = zend_make_compiled_string_description("eval()'d code"); new_op_array = zend_compile_string(inc_filename, eval_desc); efree(eval_desc);
嗯,好吧,eval基本上啥事没做,直接把字符串扔去“编译”了
接下来看看require是怎么做的
new_op_array = compile_filename(type, inc_filename);
继续看到compile_filename
的部分代码:
zend_op_array *compile_filename(int type, zval *filename) { zend_file_handle file_handle; zval tmp; zend_op_array *retval; zend_string *opened_path = NULL; if (Z_TYPE_P(filename) != IS_STRING) { tmp = *filename; zval_copy_ctor(&tmp); convert_to_string(&tmp); filename = &tmp; } file_handle.filename = Z_STRVAL_P(filename); file_handle.free_filename = 0; file_handle.type = ZEND_HANDLE_FILENAME; file_handle.opened_path = NULL; file_handle.handle.fp = NULL; retval = zend_compile_file(&file_handle, type);
大概内容就是打开文件,然后就扔给了compile_file
。这里暂时不分析compile_file
到底干了什么,继续来看require_once:
zend_file_handle file_handle; zend_string *resolved_path; resolved_path = zend_resolve_path(Z_STRVAL_P(inc_filename), (int)Z_STRLEN_P(inc_filename)); if (resolved_path) { if (zend_hash_exists(&EG(included_files), resolved_path)) { goto already_compiled; } } else { resolved_path = zend_string_copy(Z_STR_P(inc_filename)); } if (SUCCESS == zend_stream_open(ZSTR_VAL(resolved_path), &file_handle)) { if (!file_handle.opened_path) { file_handle.opened_path = zend_string_copy(resolved_path); } if (zend_hash_add_empty_element(&EG(included_files), file_handle.opened_path)) { zend_op_array *op_array = zend_compile_file(&file_handle, (type==ZEND_INCLUDE_ONCE?ZEND_INCLUDE:ZEND_REQUIRE)); zend_destroy_file_handle(&file_handle); zend_string_release(resolved_path); if (Z_TYPE(tmp_inc_filename) != IS_UNDEF) { zend_string_release(Z_STR(tmp_inc_filename)); } return op_array; } else { zend_file_handle_dtor(&file_handle); already_compiled: new_op_array = ZEND_FAKE_OP_ARRAY; } } else { if (type == ZEND_INCLUDE_ONCE) { zend_message_dispatcher(ZMSG_FAILED_INCLUDE_FOPEN, Z_STRVAL_P(inc_filename)); } else { zend_message_dispatcher(ZMSG_FAILED_REQUIRE_FOPEN, Z_STRVAL_P(inc_filename)); } }
首先,PHP会尝试进行路径的分析。接下来,会判断EG(included_files)中是否包含此路径的文件。如果已经包含了,则返回ZEND_FAKE_OP_ARRAY(即一个“假的”分析结果)如果没有,则会尝试打开此文件,将其添加至EG(included_files),再扔给compile_file
处理
接下来我们来看看compile_file
,这个函数中,除掉一些判断语句后,关键代码只有三句:
zend_save_lexical_state(&original_lex_state); op_array = zend_compile(ZEND_USER_FUNCTION); zend_restore_lexical_state(&original_lex_state);
分别是保存上下文、进行“编译”、再恢复上下文
小结
到这里已经基本上明了了。require_once与require相比,只多出一个对于路径的判断。在zend_hash.c
中可以看到,zend_hash_exists
实际上是遍历一个链表。
总结
我们实际测试一下,遍历链表对性能损耗究竟有多大。我在本地电脑上做了一个简单的测试。生成了10000个简单的PHP文件,分别使用require与require_once进行测试,结果如下:
require: 1.7350928783417 1.688873052597 1.725604057312 1.6682810783386 1.7644829750061 require_once: 2.3190469741821 2.3012549877167 2.2262940406799 2.2391288280487 2.2173840999603
可以看出,require_once确实对性能有一定损耗。但是,大部分时候,我们的文件并不会到10000个数字。一般来说,几百个文件已经是极限了。此时,两者差距究竟有多大?实际测试发现,require_once只比require慢了0.01s。这是在我自己电脑上测试的结果,在服务器上,因为硬盘、CPU等性能的不同,这个差距可能会更小。
因此,你可以直接在程序里使用require_once。当然,如果你可以确定不会重复,使用require是更好的选择