in_array() 和 array_search() 是 PHP 最常用的数组查找函数,但其内部实现细节鲜为人知。本文通过源码级追踪,揭示 php_search_array 统一实现、is_equal_function 与 is_identical_function 双比较路径、zend_hash 遍历机制、字符串数值化比较 (zendi_smart_strcmp) 的隐式转换陷阱,并给出生产环境性能优化指南。
// a.php 严格模式 true $y = "12"; $x = []; for($j=0;$j<50000;$j++) $x[] = "$j"; for($i=0;$i<30000;$i++) if (in_array($y, $x, true)) continue; // 耗时: ~0.1 秒 // b.php 非严格模式 false (默认) $y = "1800"; $x = []; for($j=0;$j<50000;$j++) $x[] = "$j"; for($i=0;$i<30000;$i++) if (in_array($y, $x)) continue; // 耗时: ~9.5 秒 (慢 95 倍!)
ltrace 显示 strtol 占用 52% CPU,揭示核心瓶颈:非严格模式下每次比较触发字符串→长整数转换。
php_search_array (ext/standard/array.c)static void php_search_array(INTERNAL_FUNCTION_PARAMETERS, int behavior) {
zval *value, *array, **entry, res;
HashPosition pos;
zend_bool strict = 0;
int (*is_equal_func)(zval*, zval*, zval*) = is_equal_function; // 默认非严格比较
if (zend_parse_parameters(ZEND_NUM_ARGS(), "za|b", &value, &array, &strict) == FAILURE) return;
if (strict) is_equal_func = is_identical_function; // 严格模式切换比较器
zend_hash_internal_pointer_reset_ex(Z_ARRVAL_P(array), &pos);
while (zend_hash_get_current_data_ex(Z_ARRVAL_P(array), (void**)&entry, &pos) == SUCCESS) {
is_equal_func(&res, value, *entry); // 核心比较调用
if (Z_LVAL(res)) {
if (behavior == 0) RETURN_TRUE; // in_array: 返回 bool
else { /* array_search: 返回键名 */ } // 提取当前键返回
}
zend_hash_move_forward_ex(Z_ARRVAL_P(array), &pos);
}
RETURN_FALSE;
}
behavior=0 → in_array,behavior=1 → array_search,核心逻辑完全复用。strict 参数控制比较器分支,零开销切换。is_identical_function — 严格模式 (=== 语义)ZEND_API int is_identical_function(zval *result, zval *op1, zval *op2) {
Z_TYPE_P(result) = IS_BOOL;
if (Z_TYPE_P(op1) != Z_TYPE_P(op2)) { Z_LVAL_P(result) = 0; return SUCCESS; } // 类型不同直接 false
switch (Z_TYPE_P(op1)) {
case IS_NULL: Z_LVAL_P(result) = 1; break;
case IS_LONG: case IS_BOOL: case IS_RESOURCE: Z_LVAL_P(result) = (Z_LVAL_P(op1) == Z_LVAL_P(op2)); break;
case IS_DOUBLE: Z_LVAL_P(result) = (Z_DVAL_P(op1) == Z_DVAL_P(op2)); break;
case IS_STRING: Z_LVAL_P(result) = (Z_STRLEN_P(op1) == Z_STRLEN_P(op2) && !memcmp(Z_STRVAL_P(op1), Z_STRVAL_P(op2), Z_STRLEN_P(op1))); break;
case IS_ARRAY: Z_LVAL_P(result) = (Z_ARRVAL_P(op1) == Z_ARRVAL_P(op2) || zend_hash_compare(..., hash_zval_identical_function, 1) == 0); break;
case IS_OBJECT: Z_LVAL_P(result) = (Z_OBJ_HT_P(op1) == Z_OBJ_HT_P(op2) && Z_OBJ_HANDLE_P(op1) == Z_OBJ_HANDLE_P(op2)); break;
default: Z_LVAL_P(result) = 0; return FAILURE;
}
return SUCCESS;
}
is_equal_function — 非严格模式 (== 语义)ZEND_API int is_equal_function(zval *result, zval *op1, zval *op2) {
if (compare_function(result, op1, op2) == FAILURE) return FAILURE;
ZVAL_BOOL(result, Z_LVAL_P(result) == 0); // compare_function 返回 0 表示相等
return SUCCESS;
}
核心委托 compare_function (Zend/zend_operators.c),实现复杂的类型协变比较规则。
compare_function 类型协变矩阵 (核心 300+ 行 switch)| 类型对 | 比较策略 | 隐式转换 |
|---|---|---|
| LONG vs LONG | 直接整数比较 | 无 |
| DOUBLE vs LONG | LONG → DOUBLE 再比较 | LONG → DOUBLE |
| STRING vs STRING | zendi_smart_strcmp (数值化优先) | 尝试数值化 |
| STRING vs NULL | 二进制比较 | 无 |
| ARRAY vs ARRAY | zend_compare_arrays (键值递归) | 递归元素比较 |
| OBJECT vs OBJECT | 同比较器则 compare_objects,否则视为不等 | 无 |
| 默认标量 | 统一转数值 (zendi_convert_scalar_to_number) | 全转数值 |
zendi_smart_strcmp 与 is_numeric_string_ex// 简化逻辑
ZEND_API void zendi_smart_strcmp(zval *result, zval *s1, zval *s2) {
// 1. 尝试将两字符串解析为数值 (IS_LONG/IS_DOUBLE)
int ret1 = is_numeric_string_ex(Z_STRVAL_P(s1), Z_STRLEN_P(s1), &lval1, &dval1, 0, &oflow1);
int ret2 = is_numeric_string_ex(Z_STRVAL_P(s2), Z_STRLEN_P(s2), &lval2, &dval2, 0, &oflow2);
if (ret1 && ret2) { // 双方均为数值字符串
// 数值比较 (处理溢出、精度、INF/NAN)
if (ret1==IS_DOUBLE || ret2==IS_DOUBLE) { /* double 比较 */ }
else { /* long 比较 */ }
} else {
// 非数值字符串:二进制逐字节比较 (zend_binary_zval_strcmp)
}
}
is_numeric_string_ex 解析流程 (Zend/zend_operators.h)0x/0Xtype == IS_LONG 时调用 strtol(str, NULL, base) → 这就是 ltrace 捕获的热点!$x 数组包含字符串 "0" 到 "49999"。in_array("1800", $x):查找字符串 "1800",数组元素 "1800" 在索引 1800 处。"1800" == "$j" 触发 zendi_smart_strcmp → is_numeric_string_ex → strtol 解析 "$j" 为长整数。strtol。strtol 调用 → 9 秒。in_array("12", $x, true):严格模式直接 memcmp 字符串,无转换,微秒级完成。in_array($needle, $haystack, true),消除隐式转换,语义明确,性能稳定。// ❌ O(N) 线性查找 if (in_array($id, $largeArray)) ... // ✅ O(1) 哈希查找 (建立反向索引) $lookup = array_flip($largeArray); // 值变键 if (isset($lookup[$id])) ... // 或直接构建时用 ID 为键 $data[$id] = $record; if (isset($data[$id])) ...
(string) 或 (int)),避免运行时转换。array_is_list/array_key_exists:语义更清晰,内部优化更好。phpbench/phpbench 纳入 CI,捕获性能回归。in_array/array_search 底层为 php_search_array 哈希表线性遍历 + 双比较器策略。非严格模式通过 compare_function → zendi_smart_strcmp → is_numeric_string_ex → strtol 链路引入隐式数值化转换,在大数组高频调用场景下造成数量级性能损耗。铁律:生产代码默认 strict=true,大数据集查找改用哈希索引 (键值/Flip),类型统一在边界层完成。理解这一机制,是从「会用 API」进阶到「驾驭内核」的关键一步。