1、概述
經常使用isset
判斷變數或陣列中的鍵是否存在, 但是陣列中可以使用array_key_exists
這個函式, 那麼這兩個誰最優呢?
官方文件對兩者的定義
- | 分類 | 描述 | 文件 |
---|---|---|---|
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()
。
2、測試
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
次, 測試程式碼和執行時間如下:
<?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.9613540172577
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.54935812950134
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 | ||
-dvld.verbosity=3 |
3、原始碼
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_ISSET
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
):
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);
我們知道, 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_true
和 Z_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
複製程式碼
4、總結
-
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
。