【问题标题】:PHP: Fastest way to handle undefined array keyPHP:处理未定义数组键的最快方法
【发布时间】:2013-05-16 13:25:48
【问题描述】:

在一个非常紧凑的循环中,我需要访问包含数百万个元素的数组中的数万个值。键可以是未定义的:在这种情况下,返回 NULL 而没有任何错误消息是合法的:

数组键存在:元素的返回值。 数组键不存在:返回 null。

我确实知道多种解决方案:

    if (isset($lookup_table[$key])) {
        return $lookup_table[$key];
    } else {
        return;
    }

@return $lookup_table[$key];

error_reporting(0);
$return = $lookup_table[$key];
error_reporting(E_ALL);
return $return;

所有解决方案都远非最佳:

  • 第一个需要在 B-TREE 中进行 2 次查找:一个用于检查存在,另一个用于检索值。这实际上使运行时间加倍。
  • 第二个使用错误抑制运算符,因此会在该行上产生大量开销。
  • 第三个调用错误处理程序(它将检查 error_reporting 设置,然后什么也不显示),从而产生开销。

我的问题是我是否错过了一种避免错误处理但仍使用单个 Btree 查找的方法?

回答一些问题:

该数组缓存了复杂计算的结果——要实时完成复杂。 在数十亿个可能的值中,只有数百万个产生有效结果。该数组看起来像 1234567 => 23457, 1234999 => 74361, ...。它被保存到一个几兆字节的 PHP 文件中,并在执行开始时 include_once-d。初始加载时间无关紧要。 如果未找到该键,则仅表示此特定值不会返回有效结果。问题是每秒完成 50k+。

结论

由于无法通过单次查找和错误处理来获取值,因此我无法接受单个答案。相反,我对所有伟大的贡献投了赞成票。

最有价值的输入:

  • 使用 array_key_exists,因为它比其他方法更快
  • 查看 PHP 的 QuickHash

关于 PHP 如何处理数组存在很多困惑。如果你查看源代码,你会发现所有的数组都是平衡树。构建自己的查找方法在 C 和 C++ 中很常见,但在 PHP 等高级脚本语言中性能不佳。

【问题讨论】:

  • isset($lookup_table[$key]) 将返回 false 如果 $lookup_table[$key]null ...这不是你想要的相反吗?不确定它的性能如何,但如果键存在但其值为nullarray_key_exists 将返回true
  • @sgroves:这个例子确实反映了我的意图。我更新了问题以澄清:如果键存在,则返回值,否则返回null。 (现有键的值永远不会为空。)坦率地说,array_key_exist 比 isset 版本慢,所以我在示例中跳过了它。
  • Quickhash 可能适合您的情况。
  • “一个包含数百万个元素的数组”。您可能需要重新考虑您的策略,并涉及某种类型的分页或数据细分方式。
  • 迈克,马修,我更新了我的问题来回答你的问题。肖恩,这不是相信的问题。当然,原生 C 比 PHP 更快。我根本不愿意花一个月的时间来写我可以用 PHP 写一整夜的代码。 :)

标签: php performance


【解决方案1】:

有两种典型的方法。

  1. 为未定义的键定义默认值。
  2. 检查未定义的键。

这里是如何执行第一个和尽可能少的代码。

$data = array_merge(array($key=>false),$data);
return $data[$key];

这里是如何执行第二个。

return isset($data[$key]) ? $data[$key] : false;

【讨论】:

  • 谢谢。使用三元运算符的第二个版本是双重查找,就像我的第一个 sn-p 中一样。第一个版本令人兴奋,但我希望渺茫:它临时创建了一个 huge 数组的副本,虽然只有一个 btree 查找,但有一个完整的(临时)数组需要平衡。运行时间从 2log(n) 增加到 2n。
  • 您是否可以选择重新组织这些数据以提高性能?八叉树查找比线性搜索更有效。您可以按每个键的第一个字母分组,然后按第二个字母分组,以此类推。 en.wikipedia.org/wiki/Octree
  • 好方法! PHP 在内部将数组作为完全平衡的树来处理。我认为八叉树不会胜过它,但我对算法分析的了解并不深入。你有这种比较的经验吗?谢谢!
  • PHP 更快的唯一方法是键在内部排序,并使用优化的查找来查找键。我从来没有读过任何说 PHP 为数组键使用优化索引的东西。
【解决方案2】:

更新

从 PHP 7 开始,您可以使用 null coalesce operator 完成此操作:

return $table[$key] ?? null;

旧答案

首先,数组不是作为B-tree实现的,它是一个哈希表;一个桶数组(通过哈希函数索引),每个都有一个实际值的链接列表(在哈希冲突的情况下)。这意味着查找时间取决于哈希函数在桶中“传播”值的程度,即哈希冲突的数量是一个重要因素。

从技术上讲,这种说法是最正确的:

return array_key_exists($key, $table) ? $table[$key] : null;

这引入了一个函数调用,因此比优化的isset()很多。多少钱? ~2e3 倍慢。

接下来是使用引用来避免第二次查找:

$tmp = &$lookup_table[$key];

return isset($tmp) ? $tmp : null;

不幸的是,如果项目不存在,这会修改原始的$lookup_table 数组,因为 PHP 始终使引用有效。

剩下的方法很像你自己的:

return isset($lookup_table[$key]) ? $lookup_table[$key] : null;

除了没有引用的副作用之外,它在运行时也是faster,即使执行两次查找也是如此。

您可以考虑将您的数组分成更小的部分,作为一种减少查找时间过长的方法。

【讨论】:

  • 我对这种方法持怀疑态度,所以我生成了一个包含 0-100,000 范围内的大约 5000 个键的随机数组,并从 0-100,000 遍历该数组。我用引用尝试了你的方法,一个没有引用的方法,但仍然是一个辅助变量,一个没有辅助变量或引用的简单方法。您的参考方法花费的时间是其他方法的两倍多。朴素方法比辅助变量方法略快。
  • @kba 答案提到它修改数组(如果该项目不存在),显然这需要时间。它还提到没有它,OP给出的第一个解决方案可能是最快的。
  • 您的方法以什么方式修改数组?我没有看到您对 $tmp$lookup_table[$key] 进行任何更改。
  • @ZsoltSzilagy 当您在不存在的变量上创建引用时,PHP 会创建必要的结构以使其成为有效引用;在这种情况下,它会创建一个值为null 的数组条目。当然,如果项目总是在那里,它会很好:)
  • 不过,我的测试表明这毕竟并没有提高性能。即使在遍历数组中的每个项目一次之后,查找时间仍然更糟。 My tests could be bad.
【解决方案3】:

只是一个突然的想法,必须进行测试,但是您是否尝试使用 array_intersect_key() 获取现有值并使用 array_merge 来填充()其余部分?它将消除循环访问数据的需要。类似的东西:

$searched_keys = array ('key1' => null, 'key2' => null); // the list of the keys to find

$exiting_values = array_intersect_key($lookup_table, $searched_keys);
$all_values = array_merge($searched_keys, $exiting_keys);

请注意,我没有在性能方面尝试过。

【讨论】:

  • 不错的方法。不幸的是,$all_values 会被认为是一个新数组,因此它会建立一个新索引。
【解决方案4】:

我用以下代码做了一些基准测试:

set_time_limit(100);

$count = 2500000;
$search_index_end = $count * 1.5;
$search_index_start = $count * .5;

$array = array();
for ($i = 0; $i < $count; $i++)
    $array[md5($i)] = $i;

$start = microtime(true);
for ($i = $search_index_start; $i < $search_index_end; $i++) {
    $key = md5($i);
    $test = isset($array[$key]) ? $array[$key] : null;
}
$end = microtime(true);
echo ($end - $start) . " seconds<br/>";

$start = microtime(true);
for ($i = $search_index_start; $i < $search_index_end; $i++) {
    $key = md5($i);
    $test = array_key_exists($key, $array) ? $array[$key] : null;
}
$end = microtime(true);
echo ($end - $start) . " seconds<br/>";


$start = microtime(true);
for ($i = $search_index_start; $i < $search_index_end; $i++) {
    $key = md5($i);
    $test = @$array[$key];
}
$end = microtime(true);
echo ($end - $start) . " seconds<br/>";

$error_reporting = error_reporting();
error_reporting(0);
$start = microtime(true);
for ($i = $search_index_start; $i < $search_index_end; $i++) {
    $key = md5($i);
    $test = $array[$key];
}
$end = microtime(true);
echo ($end - $start) . " seconds<br/>";
error_reporting($error_reporting);

$start = microtime(true);
for ($i = $search_index_start; $i < $search_index_end; $i++) {
    $key = md5($i);
    $tmp = &$array[$key];
    $test = isset($tmp) ? $tmp : null;
}
$end = microtime(true);
echo ($end - $start) . " seconds<br/>";

我发现运行最快的测试是使用 isset($array[$key]) ? $array[$key] : null 的测试,紧随其后的是禁用错误报告的解决方案。

【讨论】:

  • 即使关闭错误报告,错误仍然会产生。
  • 正确@Jack。虽然是矛盾的,但它被称为 error_reporting off,而不是 error_generation。
  • 我想知道 try/catch 和丢弃的异常是否比关闭错误报告更快。
  • 警告不会被抛出,所以在这种情况下 try/catch 不会做任何事情
【解决方案5】:

首先,通过保存一个新数组来重新组织数据以提高性能,其中数据按键排序,但新数组包含常规数字索引。

这部分会很耗时,但只做一次。

 // first sort the array by it's keys
 ksort($data);

 // second create a new array with numeric index
 $tmp = new array();
 foreach($data as $key=>$value)
 {
    $tmp[] = array('key'=>$key,'value'=>$value);
 }
 // now save and use this data instead
 save_to_file($tmp);

完成后,使用Binary Search 应该可以快速找到密钥。以后你可以使用这样的函数。

  function findKey($key, $data, $start, $end)
  { 
    if($end < $start) 
    { 
        return null; 
    } 

    $mid = (int)(($end - $start) / 2) + $start; 

    if($data[$mid]['key'] > $key) 
    { 
        return findKey($key, $data, $start, $mid - 1); 
    } 
    else if($data[$mid]['key'] < $key) 
    { 
        return findKey($key, $data, $mid + 1, $end); 
    } 

    return $data[$mid]['value'];
 }

要执行对键的搜索,您可以这样做。

 $result = findKey($key, $data, 0, count($data));
 if($result === null)
 {
      // key not found.
 }

如果count($data) 一直执行,那么您可以将其缓存在存储数组数据的文件中。

我怀疑这种方法的性能会比针对$data 重复的常规线性搜索快得多。我不能保证它会更快。只有八叉树会更快,但构建八叉树的时间可能会抵消搜索性能(我以前经历过)。这取决于您必须在数据中进行多少搜索。

【讨论】:

    【解决方案6】:

    这个作品适合我

    {{ isset($array['key']) ? $array['key']: 'Default' }} 
    

    但这很快

    {{ $array['key'] or 'Default' }}
    

    【讨论】:

      【解决方案7】:

      @ 运算符和 error_reporting 方法都比使用 isset 慢。使用这两种方法,它都会修改 PHP 的错误报告设置,但仍会调用 PHP 的错误处理程序。错误处理程序将检查 error_reporting 设置并退出而不报告任何内容,但这仍然需要时间。

      【讨论】:

        【解决方案8】:

        我更喜欢使用isset 函数而不是逃避错误。 我做了一个函数来检查键是否存在,如果不返回默认值,在嵌套数组的情况下,您只需要按顺序添加其他键:

        嵌套数组查找:

        /**
         * Lookup array value.
         *
         * @param array $array
         * @param array $keys
         * @param $defaultValue
         */
        public static function array_key_lookup($array, $keys, $defaultValue)
        {
            $value = $array;
            foreach ($keys as $key) {
                if (isset($value[$key])) {
                    $value = $value[$key];
                } else {
                    $value = $defaultValue;
                    break;
                }
            }
        
            return $value;
        }
        

        使用示例:

        $array = [
            'key1' => 'value1',
            'key2' => 'value2',
            'key3' => [
                'key3a' => 'value3a',
                'key3b' => 'value3b'
            ]
        ];
        
        array_key_lookup($array, ['key3', 'key3a'], 'default')
        'value3a'
        
        array_key_lookup($array, ['key2', 'key2a'], 'default')
        'default'
        
        array_key_lookup($array, ['key2'], 'default')
        'value2'
        
        array_key_lookup($array, ['key5'], 'default')
        'default'
        
        

        转义错误:

        $value = @$array[$key1][$key2] ?: $defaultValue;
        

        【讨论】:

        • 嘿,从可用性的角度来看,这是一个不错的方法,但在性能方面它实际上比其他选择更差。函数调用隐式创建一个数组,foreach 复制它,第一行复制一个变量等。调用 $value = isset($array[$key1][$key2]) 会快得多? $array[$key1][$key2] : $defaultValue;
        猜你喜欢
        • 1970-01-01
        • 2022-10-13
        • 1970-01-01
        • 2011-11-25
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-07-29
        • 1970-01-01
        相关资源
        最近更新 更多