关于性能:PHP函数的Big-O列表

List of Big-O for PHP functions

在使用PHP一段时间后,我注意到并不是所有内置的PHP函数都像预期的那样快。考虑使用缓存的素数数组查找某个数字是否为素数的函数的这两个可能的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
//very slow for large $prime_array
$prime_array = array( 2, 3, 5, 7, 11, 13, .... 104729, ... );
$result_array = array();
foreach( $prime_array => $number ) {
    $result_array[$number] = in_array( $number, $large_prime_array );
}

//speed is much less dependent on size of $prime_array, and runs much faster.
$prime_array => array( 2 => NULL, 3 => NULL, 5 => NULL, 7 => NULL,
                       11 => NULL, 13 => NULL, .... 104729 => NULL, ... );
foreach( $prime_array => $number ) {
    $result_array[$number] = array_key_exists( $number, $large_prime_array );
}

这是因为in_array是通过线性搜索o(n)实现的,随着$prime_array的增长,线性搜索o(n)会减慢速度。其中array_key_exists函数是用散列查找o(1)实现的,除非散列表被极大地填充(在这种情况下,它只是o(n)),否则它不会减速。

到目前为止,我已经通过尝试和错误发现了big-o,并且偶尔查看源代码。现在,关于这个问题…

是否有一个所有内置PHP函数的理论(或实际)大O时间列表?

*或者至少是有趣的

例如,我发现很难预测列出的函数的大O,因为可能的实现依赖于未知的PHP核心数据结构:array_mergearray_merge_recursivearray_reversearray_intersectarray_combinestr_replace(带数组输入)等。


因为在我认为最好把它放在某个地方作为参考之前,似乎没有人做过这件事。我已经通过基准测试或代码略读来描述array_*函数。我试着把更有趣的大O放在靠近顶部的地方。此列表不完整。

注意:假设哈希查找是O(1),即使它真的是O(n),计算出的所有大O都是O(1)。n的系数很低,在查找big-o的特性开始生效之前,存储足够大数组的RAM开销会对您造成伤害。例如,在n=1和n=1000000时,对array_key_exists的调用之间的差异大约增加了50%。

有趣的地方:

  • isset/array_key_existsin_arrayarray_search快得多。
  • EDOCX1(union)比array_merge快一点(看起来更好)。但它的工作方式不同,所以要记住这一点。
  • shufflearray_rand在同一个大O层。
  • array_pop/array_pusharray_shift/array_unshift快,因为重新索引惩罚
  • 查找:

    array_key_existso(n)但确实接近o(1)-这是因为碰撞中的线性轮询,但由于碰撞的可能性很小,系数也很小。我发现你把散列查询当作O(1)来给出一个更现实的大O。例如,n=1000和n=100000之间的差异只慢了50%。

    isset( $array[$index] )o(n),但非常接近o(1),它使用与数组_key_存在相同的查找。因为它是语言构造,所以如果密钥是硬编码的,那么它将缓存查找,从而在重复使用相同密钥的情况下加快速度。

    in_arrayo(n)-这是因为它对数组进行线性搜索,直到找到值为止。

    array_searcho(n)-它使用与数组中相同的核心函数,但返回值。

    队列函数:

    array_pusho(∑var_i,表示所有i)

    array_popo(1)

    array_shifto(n)-必须重新索引所有键

    array_unshifto(n+∑var_i,对于所有i)-它必须重新索引所有键

    数组交集、并集、减法:

    如果交叉点100%do o(max(param_i_size)*∑param_i_count,对于所有i),如果交叉点0%intersect o(∑param_i_size,对于所有i)

    array_intersect如果交叉点100%do o(n^2*∑param_i_count,对于所有i),如果交叉点0%intersect o(n^2)

    如果交叉口100%do o(max(param_i_大小)*∑param_i_计数,对于所有i),如果交叉口0%intersect o(∑param_i_大小,对于所有i)

    array_diffo(π参数i_大小,对于所有i)-这是所有参数大小的乘积

    array_diff_keyo(∑param ou i ou size,对于i!=1)-这是因为我们不需要迭代第一个数组。

    array_mergeo(∑array ou i,i!=1)-不需要迭代第一个数组

    +(union)o(n),其中n是第二个数组的大小(即array_first+array_second),比array_merge的开销小,因为它不需要重新编号。

    array_replaceo(∑array_i,用于所有i)

    随机的:

    shuffleo(n)

    array_rando(n)-需要线性轮询。

    明显的Big-O:

    array_fillo(n)

    array_fill_keysO(N)

    rangeO(N)

    array_spliceo(偏移量+长度)

    array_sliceo(偏移+长度)或o(n)(如果长度=空)

    array_keysO(N)

    array_valuesO(N)

    array_reverseO(N)

    array_padO(垫块尺寸)

    array_flipO(N)

    array_sumo(n)

    array_productO(N)

    array_reduceO(N)

    array_filtero(n)

    array_mapo(n)

    array_chunko(n)

    array_combineo(n)

    我要感谢Eureqa使查找功能的大O变得容易。这是一个惊人的免费程序,可以找到适合任意数据的最佳函数。

    编辑:

    对于那些怀疑php数组查找是O(N)的人,我编写了一个基准测试(对于最实际的值,它们仍然是O(1))。

    php array lookup graph

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    $tests = 1000000;
    $max = 5000001;


    for( $i = 1; $i <= $max; $i += 10000 ) {
        //create lookup array
        $array = array_fill( 0, $i, NULL );

        //build test indexes
        $test_indexes = array();
        for( $j = 0; $j < $tests; $j++ ) {
            $test_indexes[] = rand( 0, $i-1 );
        }

        //benchmark array lookups
        $start = microtime( TRUE );
        foreach( $test_indexes as $test_index ) {
            $value = $array[ $test_index ];
            unset( $value );
        }
        $stop = microtime( TRUE );
        unset( $array, $test_indexes, $test_index );

        printf("%d,%1.15f
    "
    , $i, $stop - $start ); //time per 1mil lookups
        unset( $stop, $start );
    }


    您几乎总是希望使用isset,而不是array_key_exists。我不看内部,但我很确定array_key_exists是O(n),因为它迭代数组的每个键,而isset尝试使用访问数组索引时使用的哈希算法访问元素。应该是O(1)。

    要注意的一点是:

    1
    2
    3
    4
    5
    6
    7
    $search_array = array('first' => null, 'second' => 4);

    // returns false
    isset($search_array['first']);

    // returns true
    array_key_exists('first', $search_array);

    我很好奇,所以我把区别作为基准:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <?php

    $bigArray = range(1,100000);

    $iterations = 1000000;
    $start = microtime(true);
    while ($iterations--)
    {
        isset($bigArray[50000]);
    }

    echo 'is_set:', microtime(true) - $start, ' seconds', '';

    $iterations = 1000000;
    $start = microtime(true);
    while ($iterations--)
    {
        array_key_exists(50000, $bigArray);
    }

    echo 'array_key_exists:', microtime(true) - $start, ' seconds';
    ?>

    is_set:0.132308959961秒array_key_exists:2.33202195168秒

    当然,这并不表示时间的复杂性,但它确实显示了这两个函数如何相互比较。

    要测试时间复杂性,请比较在第一个键和最后一个键上运行这些函数之一所需的时间。


    您具体描述的情况的解释是,关联数组被实现为哈希表,因此按键查找(相应地,array_key_exists是O(1)。然而,数组并不是按值索引的,因此在一般情况下,发现数组中是否存在值的唯一方法是线性搜索。这并不奇怪。

    我认为没有关于PHP方法算法复杂性的具体的全面文档。但是,如果这是一个足够大的问题来保证这项工作,那么您可以一直查看源代码。


    如果人们在实践中遇到密钥冲突的问题,他们将使用二级哈希查找或平衡树来实现容器。平衡树将给出O(log n)最坏情况行为和O(1)平均情况(散列本身)。在大多数实际的内存应用程序中,开销是不值得的,但是可能有一些数据库将这种形式的混合策略作为它们的默认情况来实现。