PHP:处理未定义数组键的最快方法

时间:2013-05-21 17:13:36

标签: php performance

在一个非常紧凑的循环中,我需要访问包含数百万个元素的数组中的数万个值。密钥可以不确定:在这种情况下,返回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。初始加载时间无关紧要。 如果未找到密钥,则仅表示此特定calue不会返回有效结果。麻烦的是每秒完成50k +。

结论

由于没有找到通过单个查找获得值并且没有错误处理的方法,我很难接受单个答案。相反,我赞成了所有伟大的贡献。

最有价值的投入: - 使用array_key_exists,因为它比替代品更快 - 查看php的QuickHash

关于PHP如何处理数组存在很多困惑。如果检查源代码,您将看到所有数组都是平衡树。构建自己的查找方法在C和C ++中很常见,但在高级脚本语言(例如php)中不具备性能。

8 个答案:

答案 0 :(得分:16)

更新

从PHP 7开始,您可以使用null coalesce operator

完成此操作
return $table[$key] ?? null;

旧答案

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

从技术上讲,这句话是最正确的:

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,即使执行两次查找也是如此。

您可以考虑将数组划分为更小的部分,作为缓解长查找时间的一种方法。

答案 1 :(得分:3)

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

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的测试,紧接着只是禁用错误报告的解决方案。

答案 2 :(得分:1)

有两种典型方法。

  1. 定义未定义密钥的默认值。
  2. 检查未定义的密钥。
  3. 以下是如何执行第一个和尽可能少的代码。

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

    以下是如何执行第二次。

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

答案 3 :(得分:1)

只是一个必须要测试的突然想法,但您是否尝试使用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);

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

答案 4 :(得分:0)

首先,通过保存按键排序数据的新数组重新组织数据,但新数组包含常规数字索引。

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

 // 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重复的常规线性搜索快得多。我不能保证它更快。只有八叉树会更快,但构建八叉树的时间可能会抵消搜索性能(我以前经历过)。这取决于您需要在数据中搜索多少。

答案 5 :(得分:0)

这项工作对我来说

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

但这很快

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

答案 6 :(得分:0)

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

答案 7 :(得分:0)

我更喜欢使用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;