关于require与require_once的研究

关于require与require_once的研究

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