深刻理解PHP之isset和array_key_exists對比

一、概述

常常使用isset判斷變量或數組中的鍵是否存在, 可是數組中能夠使用array_key_exists這個函數, 那麼這兩個誰最優呢?php

官方文檔對二者的定義node

- 分類 描述 文檔
isset 語言構造器 檢測變量是否已設置而且非 NULL php.net/manual/zh/f…
array_key_exists 函數 檢查數組裏是否有指定的鍵名或索引 php.net/manual/zh/f…

isset() 對於數組中爲 NULL 的值不會返回 TRUE,而 array_key_exists() 會。 array_key_exists() 僅僅搜索第一維的鍵。 多維數組裏嵌套的鍵不會被搜索到。 要檢查對象是否有某個屬性,應該去用 property_exists()laravel

二、測試

2.1 測試環境

OS PHP PHPUnit
MacOS 10.13.6 PHP 7.2.7 (cli) PHPUnit 6.5.7

2.2 單元測試

class issetTest extends \PHPUnit\Framework\TestCase {
    /** * @dataProvider dataArr */
    public function testName($arr) {
        $this->assertTrue(isset($arr['name']));
        $this->assertFalse(isset($arr['age']));
        $this->assertTrue(isset($arr['sex']));
        $this->assertTrue(array_key_exists('name', $arr));
        $this->assertTrue(array_key_exists('age', $arr));
        $this->assertTrue(array_key_exists('sex', $arr));
        $this->assertFalse(empty($arr['name']));
        $this->assertTrue(empty($arr['age']));
        $this->assertTrue(empty($arr['sex']));
    }

    public function dataArr() {
        return [
            [
                ['name' => 123, 'age' => null, 'sex' => 0]
            ]
        ];
    }
}

/* PHPUnit 6.5.7 by Sebastian Bergmann and contributors. . 1 / 1 (100%) Time: 113 ms, Memory: 8.00MB OK (1 test, 9 assertions) */
複製代碼

2.3 性能-執行時間

如上, php cli環境下, 執行10000000次, 測試代碼和執行時間以下:git

<?php

$arr = [
    'name' => 123,
    'age' => null
];

$max = 10000000;

testFunc($arr, 'name', $max);
testFunc($arr, 'age', $max);

function testFunc($arr, $key, $max = 1000) {
    echo '`$arr[\'', $key, '\']` | - | -', PHP_EOL;

    $startTime = microtime(true);
    for ($i = 0; $i <= $max; $i++) {
        isset($arr[$key]);
    }
    echo '^ | isset | ', microtime(true) - $startTime, PHP_EOL;

    $startTime = microtime(true);
    for ($i = 0; $i <= $max; $i++) {
        array_key_exists($key, $arr);
    }
    echo '^ | array_key_exists | ', microtime(true) - $startTime, PHP_EOL;

    $startTime = microtime(true);
    for ($i = 0; $i <= $max; $i++) {
        isset($arr[$key]) || array_key_exists($key, $arr);
    }
    echo '^ | isset or array_key_exists | ', microtime(true) - $startTime, PHP_EOL;
}
複製代碼

PHP 5.6 -|函數|執行時間(s) ---|---|--- $arr['name'] | - | - ^ | isset | 0.64719796180725 ^ | array_key_exists | 2.5713651180267 ^ | isset or array_key_exists | 1.1359150409698 $arr['age'] | - | - ^ | isset | 0.53988218307495 ^ | array_key_exists | 2.7240340709686 ^ | isset or array_key_exists | 2.9613540172577github

PHP 7.2.4 -|函數|執行時間(s) ---|---|--- $arr['name'] | - | - ^ | isset | 0.24308800697327 ^ | array_key_exists | 0.3645191192627 ^ | isset or array_key_exists | 0.28933310508728 $arr['age'] | - | - ^ | isset | 0.23279714584351 ^ | array_key_exists | 0.33850502967834 ^ | isset or array_key_exists | 0.54935812950134express

2.4 性能-使用VLD查看opcode

/usr/local/Cellar/php/7.2.7/bin/php -d vld.active=1 -dvld.verbosity=3 vld.php
複製代碼
描述 isset array_key_exists
code $arr = ['name' => 'li']; isset($arr['name']); $arr = ['name' => 'li']; array_key_exists('name', $arr);
-dvld.active=1
image
image
-dvld.verbosity=3
image
image

三、源碼

3.1 isset 源碼分析

Zend/zend_language_scanner.l (Scanning階段)

Scanning階段,程序會掃描zend_language_scanner.l文件將代碼文件轉換成語言片斷。數組

<ST_IN_SCRIPTING>"isset" {
	RETURN_TOKEN(T_ISSET);
}
複製代碼

可見 isset 生成對應的token爲 T_ISSETbash

3.1.2 Zend/zend_language_parser.y (Parsing階段)

當執行PHP源碼,會先進行語法分析,isset的yacc以下: 接下來就到了Parsing階段,這個階段,程序將 T_ISSET 等Tokens轉換成有意義的表達式,此時會作語法分析,Tokens的yacc保存在zend_language_parser.y文件中。isset的yacc以下(T_ISSET):php7

internal_functions_in_yacc:
		T_ISSET '(' isset_variables ')' { $$ = $3; }
	|	T_EMPTY '(' expr ')' { $$ = zend_ast_create(ZEND_AST_EMPTY, $3); }
	|	T_INCLUDE expr
			{ $$ = zend_ast_create_ex(ZEND_AST_INCLUDE_OR_EVAL, ZEND_INCLUDE, $2); }
	|	T_INCLUDE_ONCE expr
			{ $$ = zend_ast_create_ex(ZEND_AST_INCLUDE_OR_EVAL, ZEND_INCLUDE_ONCE, $2); }
	|	T_EVAL '(' expr ')'
			{ $$ = zend_ast_create_ex(ZEND_AST_INCLUDE_OR_EVAL, ZEND_EVAL, $3); }
	|	T_REQUIRE expr
			{ $$ = zend_ast_create_ex(ZEND_AST_INCLUDE_OR_EVAL, ZEND_REQUIRE, $2); }
	|	T_REQUIRE_ONCE expr
			{ $$ = zend_ast_create_ex(ZEND_AST_INCLUDE_OR_EVAL, ZEND_REQUIRE_ONCE, $2); }
;

isset_variables:
		isset_variable { $$ = $1; }
	|	isset_variables ',' isset_variable
			{ $$ = zend_ast_create(ZEND_AST_AND, $1, $3); }
;

isset_variable:
		expr { $$ = zend_ast_create(ZEND_AST_ISSET, $1); }
;

%%
複製代碼
/* Zend/zend_ast.c */
# zend_ast_export_ex
case ZEND_AST_EMPTY:
	FUNC_OP("empty");
case ZEND_AST_ISSET:
	FUNC_OP("isset");
複製代碼

最終執行了zend_ast_create(ZEND_AST_ISSET, $1);ide

咱們知道, PHP7開始, 語法解析過程的產物保存於CG(AST),接着zend引擎會把AST進一步編譯爲 zend_op_array ,它是編譯階段最終的產物,也是執行階段的輸入

3.1.3 Zend/zend_compile.c(將表達式編譯成opcodes)

將表達式編譯成opcodes,可見isset對應的opcodes爲ZEND_AST_ISSET。打開zend_compile.c文件

# void zend_compile_expr(znode *result, zend_ast *ast) /* {{{ */
	
case ZEND_AST_ISSET:
case ZEND_AST_EMPTY:
	zend_compile_isset_or_empty(result, ast);
	return;
複製代碼

最終執行了zend_compile_isset_or_empty函數,在源碼目錄中查找, 能夠發現,此函數也在 zend_compile.c 文件中定義。

void zend_compile_isset_or_empty(znode *result, zend_ast *ast) /* {{{ */ {
	zend_ast *var_ast = ast->child[0];

	znode var_node;
	zend_op *opline = NULL;

	ZEND_ASSERT(ast->kind == ZEND_AST_ISSET || ast->kind == ZEND_AST_EMPTY);

	if (!zend_is_variable(var_ast) || zend_is_call(var_ast)) {
		if (ast->kind == ZEND_AST_EMPTY) {
			/* empty(expr) can be transformed to !expr */
			zend_ast *not_ast = zend_ast_create_ex(ZEND_AST_UNARY_OP, ZEND_BOOL_NOT, var_ast);
			zend_compile_expr(result, not_ast);
			return;
		} else {
			zend_error_noreturn(E_COMPILE_ERROR,
				"Cannot use isset() on the result of an expression "
				"(you can use \"null !== expression\" instead)");
		}
	}

	switch (var_ast->kind) {
		case ZEND_AST_VAR:
			if (is_this_fetch(var_ast)) {
				opline = zend_emit_op(result, ZEND_ISSET_ISEMPTY_THIS, NULL, NULL);
			} else if (zend_try_compile_cv(&var_node, var_ast) == SUCCESS) {
				opline = zend_emit_op(result, ZEND_ISSET_ISEMPTY_VAR, &var_node, NULL);
				opline->extended_value = ZEND_FETCH_LOCAL | ZEND_QUICK_SET;
			} else {
				opline = zend_compile_simple_var_no_cv(result, var_ast, BP_VAR_IS, 0);
				opline->opcode = ZEND_ISSET_ISEMPTY_VAR;
			}
			break;
		case ZEND_AST_DIM:
			opline = zend_compile_dim_common(result, var_ast, BP_VAR_IS);
			opline->opcode = ZEND_ISSET_ISEMPTY_DIM_OBJ;
			break;
		case ZEND_AST_PROP:
			opline = zend_compile_prop_common(result, var_ast, BP_VAR_IS);
			opline->opcode = ZEND_ISSET_ISEMPTY_PROP_OBJ;
			break;
		case ZEND_AST_STATIC_PROP:
			opline = zend_compile_static_prop_common(result, var_ast, BP_VAR_IS, 0);
			opline->opcode = ZEND_ISSET_ISEMPTY_STATIC_PROP;
			break;
		EMPTY_SWITCH_DEFAULT_CASE()
	}

	result->op_type = opline->result_type = IS_TMP_VAR;
	opline->extended_value |= ast->kind == ZEND_AST_ISSET ? ZEND_ISSET : ZEND_ISEMPTY;
}
/* }}} */
複製代碼

從這個函數最後一行能夠看出,最終執行的仍是ZEND_ISSET, 根據不一樣的用法會使用不一樣的opcode處理, 此處以ZEND_ISSET_ISEMPTY_DIM_OBJ爲例。

3.1.4 Zend/zend_vm_execute.h (執行opcodes)

opcode 對應處理函數的命名規律:

ZEND_[opcode]SPEC(變量類型1)_(變量類型2)_HANDLER

變量類型1和變量類型2是可選的,若是同時存在,那就是左值和右值,概括有下幾類: VAR TMP CV UNUSED CONST 這樣能夠根據相關的執行場景來斷定。

zend_vm_execute.h: ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_CONST_CONST_HANDLER,
zend_vm_execute.h: ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_CONST_TMPVAR_HANDLER,
zend_vm_execute.h: ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_CONST_CV_HANDLER,
zend_vm_execute.h: ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_TMPVAR_CONST_HANDLER,
zend_vm_execute.h: ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_TMPVAR_TMPVAR_HANDLER,
zend_vm_execute.h: ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_TMPVAR_CV_HANDLER,
zend_vm_execute.h: ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_TMPVAR_CONST_HANDLER,
zend_vm_execute.h: ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_TMPVAR_TMPVAR_HANDLER,
zend_vm_execute.h: ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_TMPVAR_CV_HANDLER,
zend_vm_execute.h: ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_CV_CONST_HANDLER,
zend_vm_execute.h: ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_CV_TMPVAR_HANDLER,
zend_vm_execute.h: ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_CV_CV_HANDLER,
             
複製代碼

咱們看下 ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_CV_CV_HANDLER 這個處理函數

if (opline->extended_value & ZEND_ISSET) {
	/* > IS_NULL means not IS_UNDEF and not IS_NULL */
	result = value != NULL && Z_TYPE_P(value) > IS_NULL &&
	    (!Z_ISREF_P(value) || Z_TYPE_P(Z_REFVAL_P(value)) != IS_NULL);
} else /* if (opline->extended_value & ZEND_ISEMPTY) */ {
	result = (value == NULL || !i_zend_is_true(value));
}
複製代碼

上面的 if ... else 就是判斷是isset,仍是empty,而後作不一樣處理,Z_TYPE_P, i_zend_is_true 不一樣判斷。

可見,isset的最終實現是經過 Z_TYPE_P 獲取變量類型,而後再進行判斷的。

函數的完整定義請查看Zend/zend_vm_execute.h,如下是 i_zend_is_trueZ_TYPE_P的定義:

3.2 array_key_exists 源碼分析

3.2.1 ext/standard/array.c (數組擴展中實現)

array_key_exists是php內置函數,經過擴展方式實現的。打開php源碼,ext/standard/目錄下

// ➜  standard git:(master) ✗ grep -r 'PHP_FUNCTION(array_key_exists)' *

array.c: PHP_FUNCTION(array_key_exists)
php_array.h: PHP_FUNCTION(array_key_exists);
複製代碼

具體實現以下:

/* {{{ proto bool array_key_exists(mixed key, array search)
   Checks if the given key or index exists in the array */
PHP_FUNCTION(array_key_exists)
{
	zval *key;					/* key to check for */
	HashTable *array;			/* array to check in */

#ifndef FAST_ZPP
	if (zend_parse_parameters(ZEND_NUM_ARGS(), "zH", &key, &array) == FAILURE) {
		return;
	}
#else
	ZEND_PARSE_PARAMETERS_START(2, 2)
		Z_PARAM_ZVAL(key)
		Z_PARAM_ARRAY_OR_OBJECT_HT(array)
	ZEND_PARSE_PARAMETERS_END();
#endif

	switch (Z_TYPE_P(key)) {
		case IS_STRING:
			if (zend_symtable_exists_ind(array, Z_STR_P(key))) {
				RETURN_TRUE;
			}
			RETURN_FALSE;
		case IS_LONG:
			if (zend_hash_index_exists(array, Z_LVAL_P(key))) {
				RETURN_TRUE;
			}
			RETURN_FALSE;
		case IS_NULL:
			if (zend_hash_exists_ind(array, ZSTR_EMPTY_ALLOC())) {
				RETURN_TRUE;
			}
			RETURN_FALSE;

		default:
			php_error_docref(NULL, E_WARNING, "The first argument should be either a string or an integer");
			RETURN_FALSE;
	}
}
/* }}} */
複製代碼

能夠看到, 是經過 Z_TYPE_P 宏獲取變量類型, 經過 zend_hash 相關函數判斷 key 是否存在。以key爲字符串爲例,在Zend/zend_hash.h追蹤具體實現:

3.2.2 Zend/zend_hash.h

ZEND_API zval* ZEND_FASTCALL zend_hash_find(const HashTable *ht, zend_string *key);

...

static zend_always_inline int zend_symtable_exists_ind(HashTable *ht, zend_string *key) {
	zend_ulong idx;

	if (ZEND_HANDLE_NUMERIC(key, idx)) {
		return zend_hash_index_exists(ht, idx);
	} else {
		return zend_hash_exists_ind(ht, key);
	}
}

static zend_always_inline int zend_hash_exists_ind(const HashTable *ht, zend_string *key) {
	zval *zv;

	zv = zend_hash_find(ht, key);
	return zv && (Z_TYPE_P(zv) != IS_INDIRECT ||
			Z_TYPE_P(Z_INDIRECT_P(zv)) != IS_UNDEF);
}

複製代碼

再次先經過函數ZEND_HANDLE_NUMERIC對key作判斷,看這個字符串是否是數字類型的, 當key爲數字時執行 zend_hash_index_exists, 實現以下:

3.2.3 Zend/zend_hash.c

3.2.3.1 zend_hash_index_exists()
/**
 * 這裏有一個宏HASH_FLAG_PACKED,爲真就表明當前數組的key都是系統生成的,也就是說是按從0到1,2,3等等按序排列的,因此判讀鍵爲key的是否存在,直接檢查arData數組中第idx個元素是否有定義就好了,這裏不涉及什麼hash查找,衝突解決等一系列問題。
 *  
 * 但若是HASH_FLAG_PACKED爲假,那麼確定就須要先計算idx的hash值,找到key爲idx的數據應該在arData的第幾位才行。這就要經過函數zend_hash_index_find_bucket了。
 */
ZEND_API zend_bool ZEND_FASTCALL zend_hash_index_exists(const HashTable *ht, zend_ulong h)
{
	Bucket *p;

	IS_CONSISTENT(ht);

	if (ht->u.flags & HASH_FLAG_PACKED) {
		if (h < ht->nNumUsed) {
			if (Z_TYPE(ht->arData[h].val) != IS_UNDEF) {
				return 1;
			}
		}
		return 0;
	}

	p = zend_hash_index_find_bucket(ht, h);
	return p ? 1 : 0;
}
複製代碼
3.2.3.2 zend_hash_find()

Zend/zend_hash.c中有zend_hash_find()的實現, code以下:

/*++-- zend_hash_find --++*/
/* Returns the hash table data if found and NULL if not. */
ZEND_API zval* ZEND_FASTCALL zend_hash_find(const HashTable *ht, zend_string *key)
{
	Bucket *p;

	IS_CONSISTENT(ht);

	p = zend_hash_find_bucket(ht, key);
	return p ? &p->val : NULL;
}
複製代碼
3.2.3.3 zend_hash_index_find_bucket()
static zend_always_inline Bucket *zend_hash_index_find_bucket(const HashTable *ht, zend_ulong h)
{
	uint32_t nIndex;
	uint32_t idx;
	Bucket *p, *arData;

	arData = ht->arData;
	nIndex = h | ht->nTableMask;
	idx = HT_HASH_EX(arData, nIndex);
	while (idx != HT_INVALID_IDX) {
		ZEND_ASSERT(idx < HT_IDX_TO_HASH(ht->nTableSize));
		p = HT_HASH_TO_BUCKET_EX(arData, idx);
		if (p->h == h && !p->key) {
			return p;
		}
		idx = Z_NEXT(p->val);
	}
	return NULL;
}
複製代碼
3.2.3.4 zend_hash_find_bucket()
static zend_always_inline Bucket *zend_hash_find_bucket(const HashTable *ht, zend_string *key)
{
	zend_ulong h;
	uint32_t nIndex;
	uint32_t idx;
	Bucket *p, *arData;

	h = zend_string_hash_val(key);
	arData = ht->arData;
	nIndex = h | ht->nTableMask;
	idx = HT_HASH_EX(arData, nIndex);
	while (EXPECTED(idx != HT_INVALID_IDX)) {
		p = HT_HASH_TO_BUCKET_EX(arData, idx);
		if (EXPECTED(p->key == key)) { /* check for the same interned string */
			return p;
		} else if (EXPECTED(p->h == h) &&
		     EXPECTED(p->key) &&
		     EXPECTED(ZSTR_LEN(p->key) == ZSTR_LEN(key)) &&
		     EXPECTED(memcmp(ZSTR_VAL(p->key), ZSTR_VAL(key), ZSTR_LEN(key)) == 0)) {
			return p;
		}
		idx = Z_NEXT(p->val);
	}
	return NULL;
}
複製代碼

這裏須要明白一點,數字的哈希值就等於他自己,因此纔有不計算h的哈希值,就執行h | ht->nTableMask。

而後處理一下衝突,最後得出key爲idx的數據是否存在於數組中。

若是idx確確實實是字符串,那麼思路更簡單一點,最後經過zen_hash_find_bucket來判斷是否存在,與上面zend_hash_index_find_bucket不一樣的是,函數中要先計算字符串key的哈希值,而後再執行h | ht->nTableMask。

以下,

zend_symtable_exists_ind -->ZEND_HANDLE_NUMERIC{ZEND_HANDLE_NUMERIC}
    ZEND_HANDLE_NUMERIC --> zend_hash_index_exists
    ZEND_HANDLE_NUMERIC --> zend_hash_exists_ind
    zend_hash_index_exists-->zend_hash_index_find_bucket
    zend_hash_exists_ind-->zend_hash_find
    zend_hash_find-->zend_hash_find_bucket
複製代碼

四、總結

  • isset效率高於array_key_exists, PHP7以後有30%左右的提高, php5.6有將近70%的提高。

  • isset是語法結構, array_key_exists是函數, 調用開銷要小。

  • isset經過 Z_TYPE_P 獲取變量類型,而後再進行判斷實現的; array_key_exists則是經過hash查找來實現的。

  • 對於數組,isset的性能要高於array_key_exists 因此,若是數組比較大,咱們應該用以下方法保證性能和準確性 isset or array_key_exists

五、擴展閱讀

相關文章
相關標籤/搜索