我正在对Perl性能进行一些基准测试,遇到了一个我认为有点奇怪的情况。 假设您有一个多次使用数组中的值的函数。在这种情况下,您经常会看到一些代码:
sub foo {
my $value = $array[17];
do_something_with($value);
do_something_else_with($value);
}
另一种方法是根本不创建局部变量:
sub foo {
do_something_with($array[17]);
do_something_else_with($array[17]);
}
为了可读性,第一个更清晰。 我认为对于第一种情况,性能至少也会相等(或更好(——毕竟,数组查找需要乘加。
想象一下,当这个测试程序显示相反的情况时,我会感到惊讶。 在我的机器上,重新进行数组查找实际上比存储结果更快,直到我将 ITERATIONS 增加到 7;换句话说,对我来说,创建一个局部变量只有在至少使用7次时才值得!
use Benchmark qw(:all);
use constant { ITERATIONS => 4, TIME => -5 };
# sample array
my @array = (1 .. 100);
cmpthese(TIME, {
# local variable version
'local_variable' => sub {
my $index = int(rand(scalar @array));
my $val = $array[$index];
my $ret = '';
for (my $i = 0; $i < ITERATIONS; $i ++) {
$ret .= $val;
}
return $ret;
},
# multiple array access version
'multi_access' => sub {
my $index = int(rand(scalar @array));
my $ret = '';
for (my $i = 0; $i < ITERATIONS; $i ++) {
$ret .= $array[$index];
}
return $ret;
}
});
结果:
Rate local_variable multi_access
local_variable 245647/s -- -5%
multi_access 257907/s 5% --
这不是一个巨大的差异,但它提出了我的问题:为什么创建局部变量并缓存数组查找比再次查找慢?阅读其他 S.O. 帖子,我看到其他语言/编译器确实有预期的结果,有时甚至将它们转换为相同的代码。 Perl 在做什么?
我今天对此进行了更多的研究,我确定的是,相对于单深度数组查找的开销,任何类型的标量分配都是昂贵的操作。
这似乎只是在重申最初的问题,但我觉得我已经找到了更多的清晰度。 例如,如果我修改我的local_variable子例程以执行另一个任务,如下所示:
my $index = int(rand(scalar @array));
my $val = 0; # <- this is new
$val = $array[$index];
my $ret = '';
。该代码在单赋值版本之外遭受额外的 5% 速度损失 - 即使它只对变量进行虚拟赋值。
我还测试了作用域是否会导致$var
的设置/拆卸妨碍性能,方法是将其切换到全局范围而不是本地范围。 差异可以忽略不计(请参阅上面对@zdim的评论(,指出构造/破坏是性能瓶颈。
最后,我的困惑是基于错误的假设,即标量赋值应该很快。 我习惯于使用 C 语言工作,其中将值复制到局部变量是一个非常快速的操作(1-2 asm 指令(。
事实证明,Perl 中并非如此(虽然我不知道确切的原因,但没关系(。 标量分配是一个相对"缓慢"的操作......相比之下,Perl 内部为获取 Array 对象的第 n 个元素所做的一切实际上都非常快。 我在最初的文章中提到的"乘法和加法"仍然比标量赋值的代码少得多。
这就是为什么需要如此多的查找才能匹配缓存结果的性能:简单地分配给"缓存"变量要慢 ~7 倍(对于我的设置(。
让我们首先翻转语句:缓存查找预计会更快,因为它避免了重复查找,即使它确实需要一些成本,并且一旦完成超过 7 次查找,它就会开始更快。现在这并不那么令人震惊,我想。
至于为什么不到七次迭代会变慢......我猜标量创建的成本仍然大于那几次查找。它肯定大于一次查找,是吗?那么两个呢?我想说"少数"可能是一个很好的衡量标准。