摘要:從這個函數最后一行可以看出,最終執行的還是根據不同的用法會使用不同的處理此處以為例。如下總結效率高于之后有左右的提升有將近的提升。
1、概述
經常使用isset判斷變量或數組中的鍵是否存在, 但是數組中可以使用array_key_exists這個函數, 那么這兩個誰最優呢?
官方文檔對兩者的定義
- | 分類 | 描述 | 文檔 |
---|---|---|---|
isset | 語言構造器 | 檢測變量是否已設置并且非 NULL | http://php.net/manual/zh/function.isset.php |
array_key_exists | 函數 | 檢查數組里是否有指定的鍵名或索引 | http://php.net/manual/zh/function.array-key-exists.php |
isset() 對于數組中為 NULL 的值不會返回 TRUE,而 array_key_exists() 會。2、測試 2.1 測試環境
array_key_exists() 僅僅搜索第一維的鍵。 多維數組里嵌套的鍵不會被搜索到。
要檢查對象是否有某個屬性,應該去用 property_exists()。
OS | PHP | PHPUnit |
---|---|---|
MacOS 10.13.6 | PHP 7.2.7 (cli) | PHPUnit 6.5.7 |
class issetTest extends PHPUnitFrameworkTestCase { /** * @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次, 測試代碼和執行時間如下:
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 |
/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 |
Scanning階段,程序會掃描zend_language_scanner.l文件將代碼文件轉換成語言片段。
"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的定義:
Zend/zend_operators.h 中的i_zend_is_true()
Zend/zend_types.h 中的 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.hZEND_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/** * 這里有一個宏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; }
在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; }
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; }
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_bucket4、總結
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。
5、擴展閱讀PHP源碼
PHP7內核剖析
升級 php7 后 isset 不太對了
[[PHP源碼閱讀]empty和isset函數](http://www.hoohack.me/2016/05...
深入理解PHP內核
PHP安裝與使用VLD查看opcode代碼
array_key_exists官方文檔
isset官方文檔
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/29474.html
摘要:關聯數組變量是否已設置并且非。返回數組所有值。成功返回失敗返回原數組值需要能作為合法鍵名需要是或。內部指針數組分段棧隊列回調函數排序計算數組生成其他 關聯數組 isset bool isset( mixed $val [, mix $...]) 變量是否已設置并且非null。多個參數從左到右計算。 判斷null $a=null;var_dump(isset($a));bool(fal...
摘要:綱要中集群的應用對集群模式的底層實現中集群的應用這部分我想分享下中集群的配置官網也有集群的配置講解但是版還是有點不足只是說了將配置項設為,但光這樣一個選項不能代表,一個新手直接可用集群這部分還包括客戶端的事,所以后面我也會分享下關于的源碼分 綱要: laravel中redis集群的應用 predis對redis集群模式的底層實現 laravel中redis集群的應用 這部分我想分享...
摘要:綱要中集群的應用對集群模式的底層實現中集群的應用這部分我想分享下中集群的配置官網也有集群的配置講解但是版還是有點不足只是說了將配置項設為,但光這樣一個選項不能代表,一個新手直接可用集群這部分還包括客戶端的事,所以后面我也會分享下關于的源碼分 綱要: laravel中redis集群的應用 predis對redis集群模式的底層實現 laravel中redis集群的應用 這部分我想分享...
摘要:這種行為比最初出現的問題更為棘手,同時也是一種常見的錯誤源。這意味著這個數組的一份拷貝將會被返回,因此被調函數與調用者所訪問的數組并不是同樣的數組實例。 showImg(https://segmentfault.com/img/bV7reP?w=620&h=620); PHP 語言讓 WEB 端程序設計變得簡單,這也是它能流行起來的原因。但也是因為它的簡單,PHP 也慢慢發展成一個相對...
閱讀 2462·2021-11-23 09:51
閱讀 1872·2021-10-13 09:40
閱讀 1390·2021-09-30 10:01
閱讀 597·2021-09-26 09:46
閱讀 2256·2021-09-23 11:55
閱讀 1401·2021-09-10 10:51
閱讀 2266·2021-09-09 09:33
閱讀 2235·2019-08-29 17:25