在一个非常紧密的循环中,我需要访问包含数百万个元素的数组中的数万个值。键可以是未定义的:在这种情况下,返回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中进行两次查找:一次检查是否存在,另一次检索值。这有效地使运行时间加倍。
- 第二个使用错误抑制操作符,因此在该行上创建了大量开销。
- 第三个调用错误处理程序(它将检查error_reporting设置,然后不显示任何内容),从而产生开销。
我的问题是如果我错过了一种方法来避免错误处理,但工作与单个b树查找?
回答一些问题:
数组缓存一个复杂计算的结果——复杂到可以实时完成。在数十亿个可能的值中,只有数百万个产生有效的结果。数组看起来像1234567 =>23457, 1234999 =>74361年,…它被保存到一个几兆字节的PHP文件中,并在执行开始时包含include_once-d。初始加载时间无关紧要。如果没有找到键,这仅仅意味着这个特定的值不会返回有效的结果。问题是要以每秒50k+的速度完成。
结论编辑:过时,检查接受的答案
由于没有办法通过一次查找而没有错误处理来获得值,因此我很难接受单个答案。相反,我给所有伟大的贡献都点了赞。
最有价值的输入:
- 使用array_key_exists,因为它比其他方法更快
- 查看PHP的QuickHash
关于PHP如何处理数组有很多困惑。如果检查源代码,您将看到所有数组都是平衡树。构建自己的查找方法在C和c++中很常见,但在PHP等高级脚本语言中表现不佳。
编辑:关于"重复问题":这个问题涉及性能基准,而不是解释"未定义索引"的含义。
更新
从PHP 7开始,您可以使用空合并操作符来完成此操作:
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;
除了没有引用的副作用之外,它在运行时也更快,即使执行两次查找也是如此。
你可以考虑把你的数组分成更小的部分,作为减少长时间查找的一种方法。
我用以下代码做了一些基准测试:
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
紧随其后的解决方案,只是禁用错误报告。
This Work for me
{{ isset($array['key']) ? $array['key']: 'Default' }}
但是这很快
{{ $array['key'] or 'Default' }}
有两种典型的方法。
- 定义未定义键的默认值
- 检查未定义的键。
下面是如何执行第一个和尽可能少的代码。
$data = array_merge(array($key=>false),$data);
return $data[$key];
下面是执行第二个命令的方法。
return isset($data[$key]) ? $data[$key] : false;
只是一个突然的想法,必须进行测试,但您是否尝试使用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);
请注意,我没有尝试性能方面的
@操作符和error_reporting方法都比使用isset要慢。使用这两种方法,它修改了PHP的错误报告设置,但仍然会调用PHP的错误处理程序。错误处理程序将检查error_reporting设置并在不报告任何内容的情况下退出,但是这仍然需要时间。
我更喜欢使用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;
首先,通过保存一个新数组来重新组织数据,其中数据按键排序,但新数组包含一个常规数字索引。
这部分会很耗时,但只做一次。
// 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);
一旦这样做了,它应该很快找到关键使用二进制搜索。稍后您可以使用这样的函数。
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
重复的常规线性搜索快得多。我不能保证更快。只有八叉树会更快,但是构建八叉树的时间可能会抵消搜索性能(我以前经历过)。这取决于你在数据中搜索了多少