关于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是更好的选择