在使用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_merge、array_merge_recursive、array_reverse、array_intersect、array_combine、str_replace(带数组输入)等。
- 完全脱离主题,但是,1不是主要的。
- 打个好电话给杰森·普尼恩
- PHP中的数组是哈希表。这应该告诉你你需要知道的一切。在哈希表中搜索键是o(1)。搜索一个值是O(N)——在未排序的集合上是无法击败的。你最感兴趣的功能可能是O(N)。当然,如果你真的想知道,你可以阅读源代码:cvs.php.net/viewvc.cgi/php-src/ext/standard/…
- 根据记录,您尝试执行的操作的最快实现是使用true,然后使用isset($large_prime_array[$number])测试是否存在(而不是使用空值)。如果我没记错的话,它的速度是in_array函数的数百倍。
- 马巴斯塔:如果你说的是真的,那么我刚刚学会了我整个星期学到的最酷的东西。
- @马巴斯塔,它不是还有同样的大O,但只是一个较小的系数吗?
- @肯德尔不太确定,但我用过,而且速度很快。
- @根据我的测试,Isset更快的唯一原因是因为它是一种语言结构,并且存在一些数组键的过度使用。根据我的测试pastebin.com/e9wtuzpb,这个开销只有约50%(0.0000006和0.0000004之间的差异)。我相信ISset之所以能够更快地达到数量级,是因为它可以在硬编码密钥时缓存查找,因为它是一种语言构造。我想我还是会坚持使用数组键,因为它没有空的gotcha。
- 大O符号不是关于速度的。这是关于限制行为。
- @肯德尔,我不是在和array_key_exists比较,我是在和in_array比较。in_array迭代数组中的每个项,并将值与传递给它的指针进行比较。如果您将值翻转到键上(只需将每个值替换为一个虚拟值,如true,那么使用isset会快很多倍。这是因为数组的键是由PHP索引的(如哈希表)。因此,以这种方式搜索数组可以显著提高速度。
- 很有趣!因此,使用第二个更快的表单的快速转换是使用$prime_array=array_fill_键($prime_array,空)。这将键转换为值,现在我们可以使用isset()而不是在_Array()中。来自stackoverflow.com/questions/10641865/…
- 没有定义编号的$array_??
因为在我认为最好把它放在某个地方作为参考之前,似乎没有人做过这件事。我已经通过基准测试或代码略读来描述array_*函数。我试着把更有趣的大O放在靠近顶部的地方。此列表不完整。
注意:假设哈希查找是O(1),即使它真的是O(n),计算出的所有大O都是O(1)。n的系数很低,在查找big-o的特性开始生效之前,存储足够大数组的RAM开销会对您造成伤害。例如,在n=1和n=1000000时,对array_key_exists的调用之间的差异大约增加了50%。
有趣的地方:
isset/array_key_exists比in_array和array_search快得多。
EDOCX1(union)比array_merge快一点(看起来更好)。但它的工作方式不同,所以要记住这一点。
shuffle与array_rand在同一个大O层。
array_pop/array_push比array_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))。
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 );
} |
- @西姆斯,这是一件好事,它在葡萄酒中运行,那么:)
- 也许这对逆向工程设备驱动程序是有益的。我会坚持使用virtualbox;)现在享受它的乐趣。谢谢!
- 你确定哈希查找是O(N)吗?据我所知,PHP只对哈希键使用字符串和整数,所以不能使用一些logn数据结构(例如二进制搜索树)来处理冲突吗?
- @在我回答你的第一个问题之前,我先谈谈索引是如何存储的。首先,索引只存储为字符串,如果符合整数形式,则可以神奇地将其类型化为整数。例如,如果您检查数组中的所有后续键的类型:array( 1 => NULL,"1" => NULL,"1000" => NULL,"-100" => NULL ),它们看起来都像int,但这些是字符串:array("01" => NULL,"--1" => NULL,"1" => NULL," 1" => NULL )。
- @cam(continue)正因为如此,只有一种类型需要散列,所以您的右边可能有一个o(log(n))。然而,在实践中,对于较小(甚至相当大)的n值,散列可以更快,即使它以发生冲突时的线性轮询为代价。正如我在文章中所说,n=1和n=1000000的数组之间的差异只慢了2倍,所以散列函数显然足够大,在大多数情况下可以吃掉大部分时间。但是为了回答第一个问题,php散列查询在技术上是O(N),您甚至可以看到它,因为您将首先用完RAM。
- @cam我更新了post w/an查找基准图,显示它是O(N)。
- @肯德尔:谢谢!我做了一些阅读,结果发现PHP使用"嵌套"哈希表来解决冲突。也就是说,它使用另一个哈希表而不是用于冲突的logn结构。我也理解,实际上讲PHP哈希表可以提供O(1)性能,或者至少平均为O(1)-这就是哈希表的用途。我只是好奇为什么你说它们是"真的O(N)"而不是"真的O(logN)"。顺便说一句,很棒的帖子!
- @cam big-o是n->infitty时函数的上界。虽然该函数在"填充阶段"中有O(1)和O(log(N)),但最终稳定到O(N)。
- @肯德尔:在最坏的情况下,它确实会有O(N)的表现。但是我认为平均的案例表现仍然是O(1)。要了解这一点:发生冲突时,将为该键的所有值创建一个新的哈希表,并在其中重新显示键。碰撞不太可能在那里再次发生。当我们在更深的深度添加哈希表来处理冲突时,实际上我们讨论的是输入的一个越来越小的子集,因为这些嵌套的冲突很少发生。所以平均情况运行时是O(1),而不是O(n)和一个小常量。
- 这就是我理解它的方式——如果我是错的,我真的很想理解为什么它是O(N),即使是在一般情况下。
- @cam我认为你对php的array是如何实现的错误。PHP的数组(或哈希)是使用链接实现的。这意味着在填充第一层时,由于开销高昂,它基本上是O(1),但一旦填充了第一层,就必须使用线性轮询(遍历列表)来达到冲突。与任何实际值N<10000000的散列开销相比,这也是微不足道的(实际上为什么是o(1))。如果你还不清楚,那就多看看维基网页。
- 文档中应包含时间复杂性!选择正确的功能可以节省你这么多时间,或者告诉你不要做你计划做的事情:p谢谢你已经有了这个列表!
- 简要说明:ISSET是一个运营商,所以它是预期的(许多博客文章和文件太远)。key_exists只执行哈希查找,这比搜索所有数组元素快得多。
- 我知道这很古老…但是什么?这条曲线根本不显示O(n),它显示O(logn),en.wikipedia.org/wiki/logarith。这与嵌套散列映射的期望值也很精确。
- 数组元素上的unset的big-o是什么?
- 虽然哈希表确实具有最坏的O(N)查找复杂性,但平均情况是O(1),并且基准测试的特定情况甚至保证为O(1),因为它是一个基于零的、连续的、数字索引的数组,不会发生哈希冲突。您仍然看到依赖数组大小的原因与算法的复杂性无关,这是由CPU缓存效果引起的。数组越大,随机访问查找越有可能导致缓存未命中(层次结构中的缓存未命中更高)。
- 仅供参考,PHP7对数组使用了一个大大改进的哈希表实现。
您几乎总是希望使用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秒
当然,这并不表示时间的复杂性,但它确实显示了这两个函数如何相互比较。
要测试时间复杂性,请比较在第一个键和最后一个键上运行这些函数之一所需的时间。
- 这是错误的。我百分之百确信数组键的存在不需要迭代每个键。如果你不相信,请看下面的链接。ISSET速度快得多的原因是它是一种语言结构。这意味着它没有执行函数调用的开销。另外,我认为它可能正在缓存查找,因为这一点。而且,这不是问题的答案!我想要一个PHP函数的大(O)列表(如问题所述)。我的例子中没有一个基准。svn.php.net/repository/php/php src/branches/php_5_3/ext/&hellip;
- 如果你仍然不相信我,我已经创建了一个小基准来证明这一点。pastebin.com/bdkpnvke
- 您的基准测试有什么问题,您必须禁用xdebug。=)
- 有两个关键的原因可以解释为什么要在数组上使用ISSET。首先,ISSET是一种减轻函数调用成本的语言结构。这类似于$arrray[] = $append对array_push($array, $append)的论点。第二,数组键的存在也区分了非集合值和空值。对于$a = array('fred' => null);array_key_exists('fred', $a)将返回真,而isset($['fred'])将返回假。这个额外的步骤是非常重要的,并且会大大增加执行时间。
您具体描述的情况的解释是,关联数组被实现为哈希表,因此按键查找(相应地,array_key_exists是O(1)。然而,数组并不是按值索引的,因此在一般情况下,发现数组中是否存在值的唯一方法是线性搜索。这并不奇怪。
我认为没有关于PHP方法算法复杂性的具体的全面文档。但是,如果这是一个足够大的问题来保证这项工作,那么您可以一直查看源代码。
- 这不是真正的答案。正如我在问题中所说,我已经尝试研究了PHP源代码。因为PHP是用C语言编写的,它使用复杂的宏,这使得有时很难"看到"函数的底层大O。
- @Kendall我忽略了您对深入到源代码中的引用。但是,我的回答中有一个答案:"我认为没有关于PHP方法算法复杂性的具体全面文档。""否"是一个完全有效的答案。(C:
如果人们在实践中遇到密钥冲突的问题,他们将使用二级哈希查找或平衡树来实现容器。平衡树将给出O(log n)最坏情况行为和O(1)平均情况(散列本身)。在大多数实际的内存应用程序中,开销是不值得的,但是可能有一些数据库将这种形式的混合策略作为它们的默认情况来实现。