请稍侯

PHP in_array 源码学习, 以及in_array和isset效率比较

19 July 2017

背景知识

工作中通过xhprof分析接口性能,在xhprof产生的结果中发现如下一条

Function Name Calls Calls% Incl. Wall Time(microsec) IWall% Excl. Wall Time(microsec)
in_array 6,115 36.9% 1,857,818 43.6% 1,857,818

调用了6K次的in_array耗时接近2秒, 数组里面大概有1W个元素, 这个耗时明显是不能接受的。就想办法改成了通过isset来实现in_array的功能。

通过简单的脚本测试,发现isset确实比in_array快, 这也很好理解, isset是O(1)的时间复杂度, in_array则是O(n)。

in_array 源码梳理

为了弄清楚in_array的实现,梳理了下in_array的源码。

函数实现入口在php-src/ext/standard/array.c如下:
/* proto bool in_array(mixed needle, array haystack [, bool strict])
   Checks if the given value exists in the array */
PHP_FUNCTION(in_array)
{
    php_search_array(INTERNAL_FUNCTION_PARAM_PASSTHRU, 0);
}


主要实现逻辑都在php_search_array中, 代码如下:
/* void php_search_array(INTERNAL_FUNCTION_PARAMETERS, int behavior)
 * 0 = return boolean  in_array时 behavior为0
 * 1 = return key array_search 时behavior为1
 */
static inline void php_search_array(INTERNAL_FUNCTION_PARAMETERS, int behavior)
{
    zval *value,                /* value to check for */
         *array,                /* array to check in */
         *entry;                /* pointer to array entry */
    zend_ulong num_idx;
    zend_string *str_idx;
    zend_bool strict = 0;       /* strict comparison or not */


    /*
     * php7 使用了fast parameter parsing Api 来解析参数
     * ZEND_PARSE_PARAMETERS_START() 的两个参数分别为最少参数数和最多参数数。
     * Z_PARAM_ZVAL() 则将参数视为zval,Z_PARAM_ARRAY() 将参数视为数组。
     * Z_PARAM_OPTIONAL 则表示后面的参数为可选参数
     * Z_PARAM_BOOL 表示参数是布尔型
     * add by huyongde 参考:http://www.php-internals.com/book/?p=chapt11/11-02-01-zend-parse-parameters
     */

    ZEND_PARSE_PARAMETERS_START(2, 3) /*  */
        Z_PARAM_ZVAL(value)
        Z_PARAM_ARRAY(array)
        Z_PARAM_OPTIONAL
        Z_PARAM_BOOL(strict)
    ZEND_PARSE_PARAMETERS_END();

    if (strict) {
        ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(array), num_idx, str_idx, entry) {  // 对数组进行遍历
            ZVAL_DEREF(entry);
            if (fast_is_identical_function(value, entry)) {
                if (behavior == 0) {
                    RETURN_TRUE;
                } else {
                    if (str_idx) {
                        RETVAL_STR_COPY(str_idx);
                    } else {
                        RETVAL_LONG(num_idx);
                    }
                    return;
                }
            }
        } ZEND_HASH_FOREACH_END(); // 数组遍历结束
    } else {
        if (Z_TYPE_P(value) == IS_LONG) {
            ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(array), num_idx, str_idx, entry) {
                if (fast_equal_check_long(value, entry)) {
                    if (behavior == 0) {
                        RETURN_TRUE;
                    } else {
                        if (str_idx) {
                            RETVAL_STR_COPY(str_idx);
                        } else {
                            RETVAL_LONG(num_idx);
                        }
                        return;
                    }
                }
            } ZEND_HASH_FOREACH_END();
        } else if (Z_TYPE_P(value) == IS_STRING) { 
            ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(array), num_idx, str_idx, entry) {
                if (fast_equal_check_string(value, entry)) {
                    if (behavior == 0) {
                        RETURN_TRUE;
                    } else {
                        if (str_idx) {
                            RETVAL_STR_COPY(str_idx);
                        } else {
                            RETVAL_LONG(num_idx);
                        }
                        return;
                    }
                }
            } ZEND_HASH_FOREACH_END();
        } else {
            ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(array), num_idx, str_idx, entry) {
                if (fast_equal_check_function(value, entry)) {
                    if (behavior == 0) {
                        RETURN_TRUE;
                    } else {
                        if (str_idx) {
                            RETVAL_STR_COPY(str_idx);
                        } else {
                            RETVAL_LONG(num_idx);
                        }
                        return;
                    }
                }
            } ZEND_HASH_FOREACH_END();
        }
    }

    RETURN_FALSE;
}

php_search_array 中一些主要的宏或者函数介绍如下
  • ZEND_HASH_FOREACH_KEY_VAL 和 ZEND_HASH_FOREACH_END 两个宏 定义在php-src/Zend/zend_hash.h 中, 用来实现遍历数组中的元素
  • fast_is_identical_function 定义在php-src/Zend/zend_operators.h 中, 用来弱类型比较两个对象是否相等
  • fast_equal_check_[string long function] 三个函数也定义在php-src/Zend/zend_operators.h 中用来比较字符串是否相等, 整型是否相等,函数是否相等

从源码可以看到in_array时间复杂度确实是O(n)。

isset 和 in_array性能比较

简单的测试代码如下:

function testInArray($arr) {
    for($i=0; $i<10000; $i++) {
        $rand = rand(0, 10000);
        in_array($rand, $arr);

    }

}
function testIsset($arr) {
    for($i=0; $i<10000; $i++) {
        $rand = rand(0, 10000);
        isset($arr[$rand]);

    }

}
$arr = array();
for($i=0; $i<10000; $i++) {
    array_push($arr, $i);
}
$arr2 = array();
for($i=0; $i<10000; $i++) {
    $arr2[$i] = 1;
}

$start = microtime(true);
testInArray($arr);
$start2 = microtime(true);
testIsset($arr2);
$end = microtime(true);
echo "in_array time: " . ($start2 - $start) * 1000 . "\n";
echo "isset time: " . ($end - $start2) * 1000 . "\n";

测试结果输出结果如下:

in_array time: 717.60702133179
isset time: 44.092893600464

时间单位毫秒, 差别还是挺明显的。