如何在C级上实现PHP数组?

时间:2010-02-28 06:54:50

标签: php arrays

PHP array是PHP的核心功能之一。它是稀疏的,允许同一数组中的多类型键,并支持集合,字典,数组,堆栈/队列和迭代功能。

但是在使用PHP一段时间之后,我发现很多array_*函数比你初看起来慢得多。就像array_rand在非常大的数组(10000+)上的情况一样。 array_rand实际上是如此之慢,以至于在使用php数组作为索引数组的情况下,像rand( 0, array_length( $array ) - 1 )这样的函数运行速度比array_rand快。

现在问我的问题。

如何在C级别上实现PHP数组?这对于预测大量使用PHP数组数据类型的不同功能的函数的Big O非常有用。

5 个答案:

答案 0 :(得分:34)

在读完zend / zend_hash.h和ext / standard / array.c之后我想我找到了答案(谢谢Chris和gumbo的建议)。

PHP数组是一个链式哈希表(在键冲突上查找O(c)和O(n)),它允许使用int和string键。它使用2种不同的散列算法将这两种类型拟合到相同的散列键空间中。此外,存储在散列中的每个值都链接到它之前存储的值和存储在(链接列表)之后的值。它还有一个临时指针,用于保存当前项,以便可以迭代哈希值。

array_rand函数的捕获是为了确保密钥是真正随机的,array_rand函数必须遍历数组rand(0, count($array))次(O(n))。这是因为在O(c)时间内无法移动到散列表中的偏移量,因为无法保证该范围内没有丢失键。

这个发现让我感到有些困扰,因为这意味着PHP中没有数据类型具有正常的C数组特征。现在大部分时间都没问题,因为哈希查找速度非常快,但在array_rand这样的情况下会出现错误。

另一件令我困扰的事情是array_key_existsin_array的实施之间的差异。 array_key_exists使用哈希查找(大部分时间为O(c))来查看密钥是否在数组中,而in_array必须线性搜索哈希值(O(n))。

考虑以下两个例子:

in_array版本

$array = range(0, 100000);
if( in_array( $random_key, $array ) ) {
   //we found a value
}

array_key_exists版本

$array = array_fill_keys( range(0, 100000), NULL );
if( array_key_exists( $random_key, $array ) ) {
   //we found a value, err key
}

虽然in_array版本看起来更清晰,更容易理解,但它在大型数组(O(n))上实际上非常慢,其中array_key_exists(尽管是令人讨厌的长名称)非常快(O(c)或接近)。

总结: 我希望zend HashTable数据结构中有一个透明标志,在使用array_pusharray[] = $value创建数组的情况下设置它,这将允许像C数组而不是链接一样进行扩展列表。

答案 1 :(得分:28)

PHP关联数组实际上是HashTables的实现。

在内部,可以制作数字数组或关联数组。 如果将它们组合在一起,它就是关联数组。

在数值数组中,它与C非常相似。您有指向ZVAL结构的指针数组。

因为指针具有固定长度(让我们称之为n),所以偏移(x)计算很容易:x * n。

在PHP类型中是ZVAL结构(因为它实现了动态类型),但它也有助于关联数组,因为你可以假设固定长度。因此,即使直接访问数组较慢,它仍然被认为是O(1)。

那么字符串键会发生什么? PHP使用哈希函数将它们转换为整数。

在数字和关联数组中搜索具有相似的效率,因为在内部它们都是数字。

由于附加级别(哈希函数),只能直接访问数组键。

答案 2 :(得分:5)

由于PHP数组are ordered maps(即使使用连续的整数索引)array_rand()可能必须包含从中选择元素的键列表。我猜测缓存(经常无效的)密钥列表的空间和时间是无效的,因此每次调用都会产生至少O(n)遍历和建设成本。

因为您的rand(... length ...)实现利用了您所拥有的特殊知识,即密钥是连续的整数,所以您将获得O(log n)查找成本。这似乎与Chacha102的数据一致。

答案 3 :(得分:2)

在文档中查看此注释,确认您的困境,即array_rand虽然对于小型数组来说很快,但扩展性很差。

  

我修改了fake_array_rand以便总是只返回1个元素,并且在调用array_rand时使用第二个参数作为1进行了一些基准测试。我为每个元素的数量运行了100个样本并获得了平均结果。虽然内部array_rand对于少量元素来说更快,但它的扩展性非常差。

1 elements: 2.0619630813599E-05 sec. for array_rand,8.4352493286133E-05 sec. 
for fake_array_rand 

10 elements: 2.1675825119019E-05 sec. for array_rand,8.427619934082E-05 sec. 
for fake_array_rand 

100 elements: 2.9319524765015E-05 sec. for array_rand,8.4599256515503E-05 sec. 
for fake_array_rand 

1000 elements: 0.0001157283782959 sec. for array_rand,8.5572004318237E-05 sec. 
for fake_array_rand 

10000 elements: 0.0016669762134552 sec. for array_rand,8.5201263427734E-05 sec. 
for fake_array_rand 

100000 elements: 0.015599734783173 sec. for array_rand,8.5580348968506E-05 sec. 
for fake_array_rand 

1000000 elements: 0.18011983394623 sec. for array_rand,8.6690187454224E-05 sec. for fake_array_rand 

<?php 
function fake_array_rand ($array) 
{ 
        $count = count ($array); 
        # Help keep the number generator random :) 
        $randval and usleep ("0.$randval"); 

        # Seed the random number generator 
        # Generate a random number 
        srand ((double) microtime() * 10000000); 
        $randval = rand(); 

        # Use the random value to 'pick' an entry from the array 
        # Count the number of times that the entry is picked 
        ++$index[$randval % $count]; 

        return $array[$randval % $count]; 
} 
?>

http://us.php.net/manual/en/function.array-rand.php#22360

答案 4 :(得分:2)

查看zend/zend_hash.czend/zend_hash.h

我不知道c,但对我来说,它看起来像一个链式哈希表。