在使用反射时性能下降 $foo->$bar()。

5

我想知道当我使用反射通过字符串调用方法时,实际上发生了什么:

my $foo = Foo->new();
my $method = 'myMethod';
$foo->$method();

比原生调用慢约20%:

$foo->myMethod();

任何有关Perl反射实现的文档指针都会很有帮助。谢谢。

3
我认为很明显,操作码中不应该包含对特定方法的引用,而是需要更多指令来在@ISA层次结构的符号链中查找方法,并在运行时调度。 - Axeman
1
@Axeman,方法调度是动态的——考虑运行时@ISA(或符号表)修改、重新bless对象等。此外,perlobj描述了方法查找为运行时缓存,这乍一看似乎表明探测@ISA不能解释速度差异。 - pilcrow
3个回答

10
> perl -MO=Concise -e '$foo->$bar'
8  <@> leave[1 ref] vKP/REFC ->(end)
1     <0> enter ->2
2     <;> nextstate(main 1 -e:1) v:{ ->3
7     <1> entersub[t3] vKS/TARG ->8
3        <0> pushmark s ->4
-        <1> ex-rv2sv sKM/1 ->5
4           <#> gvsv[*foo] s ->5
6        <1> method K/1 ->7             # ops to read $bar and then call method
-           <1> ex-rv2sv sK/1 ->6       # 
5              <#> gvsv[*bar] s ->6     # 
-e syntax OK

> perl -MO=Concise -e '$foo->bar'
7  <@> leave[1 ref] vKP/REFC ->(end)
1     <0> enter ->2
2     <;> nextstate(main 1 -e:1) v:{ ->3
6     <1> entersub[t2] vKS/TARG ->7
3        <0> pushmark s ->4
-        <1> ex-rv2sv sKM/1 ->5
4           <#> gvsv[*foo] s ->5
5        <$> method_named[PV "bar"] ->6   # op to call the 'bar' method
-e syntax OK
在第一个例子中,Perl需要加载$bar变量,然后检查它是否包含可用作方法的名称或值。由于$bar的内容在调用之间可能会发生更改,因此必须每次执行该查找操作。
在第二个例子中,Perl已经知道应使用字符串"bar"作为方法名称,因此避免了在每次执行时加载变量并检查其内容。
但是你不应过分担心两个本地操作之间20%的速度差异,主要是因为本地操作非常快,它们实际上所需的任何速度都很快地被代码执行的实际算法所淹没。换句话说,除非你已经使用代码分析器将此问题视为热点,否则速度差异具有更多的教育意义而非实际重要性。

4

首先,我不相信我没有看到的基准测试。很容易出错。我自己对它们进行了基准测试。

use strict;
use warnings;

use Benchmark qw( cmpthese );

sub new { return bless({}, $_[0]); }
sub myMethod { }

my %tests = (
   rt => '$foo->$method()  for 1..1000;',
   ct => '$foo->myMethod() for 1..1000;',
);

$_ = 'use strict; use warnings; our $foo; our $method; ' . $_
   for values(%tests);

our $foo = __PACKAGE__->new();
our $method = 'myMethod';

cmpthese(-3, \%tests);

我可以复制您的结果。
     Rate   rt   ct
rt 1879/s   -- -19%
ct 2333/s  24%   --

(Rate is 1/1000th of actual rate.)

那似乎相当大,但在如此快的情况下,百分比可能非常具有误导性。 让我们看一下绝对时间的差异。

Compile-time: 2333000 calls per second = 429 nanoseconds per call
Run-time:     1879000 calls per second = 532 nanoseconds per call
Difference:   103 nanoseconds per call.

没有那么多时间。那么时间都用在哪里了呢?
$ perl -MO=Concise,-exec -e'$foo->myMethod()'     $ perl -MO=Concise,-exec -e'$foo->$method()'
1  <0> enter                                   =  1  <0> enter 
2  <;> nextstate(main 1 -e:1) v:{              =  2  <;> nextstate(main 1 -e:1) v:{
3  <0> pushmark s                              =  3  <0> pushmark s
4  <#> gvsv[*foo] s                            =  4  <#> gvsv[*foo] s
                                               +  5  <#> gvsv[*method] s
5  <$> method_named[PV "myMethod"]             !  6  <1> method K/1
6  <1> entersub[t2] vKS/TARG                   =  7  <1> entersub[t3] vKS/TARG
7  <@> leave[1 ref] vKP/REFC                   =  8  <@> leave[1 ref] vKP/REFC
-e syntax OK                                   =  -e syntax OK

看起来唯一的区别就是多了一个符号表查找。100ns 对于这个操作来说有些过长了。但为了确保,可以与像加1这样的小操作进行比较。

$ perl -MO=Concise,-exec -e'my $y = $x;'     $ perl -MO=Concise,-exec -e'my $y = $x + 1;'
1  <0> enter                              =  1  <0> enter 
2  <;> nextstate(main 1 -e:1) v:{         =  2  <;> nextstate(main 1 -e:1) v:{
3  <#> gvsv[*x] s                         =  3  <#> gvsv[*x] s
                                          +  4  <$> const[IV 1] s
                                          +  5  <2> add[t3] sK/2
4  <0> padsv[$y:1,2] sRM*/LVINTRO         =  6  <0> padsv[$y:1,2] sRM*/LVINTRO
5  <2> sassign vKS/2                      =  7  <2> sassign vKS/2
6  <@> leave[1 ref] vKP/REFC              =  8  <@> leave[1 ref] vKP/REFC
-e syntax OK                              =  -e syntax OK

将该代码以及our $x = 100;插入到上面的基准代码中,我们得到:

            Rate addition  baseline
addition  4839/s       --      -26%
baseline  6532/s      35%        --

(Rate is 1/1000th of actual rate.)

那么,

Basline:    6553000/s = 153 nanoseconds per assignment
Addition:   4839000/s = 207 nanoseconds per assignment+addition
Difference:              54 nanoseconds per addition

仅仅为了查找一个简单的符号表,需要的时间比添加一个符号表项多一倍,这是否合理?可能是,因为它涉及哈希字符串和在短链接列表中查找字符串。

你真的在乎这里多花100纳秒吗?我猜不会。


1
你可以使用方法引用来加速这个过程,例如:
$metref = \&{"Class::$method"};
$instance = new Class;
$instance->$metref(@args);

如果您在编译时知道方法名称,显然可以使用$metref = \&Class::myMethod。还有使用sub { ... }的闭包,Perl可能会比符号解引用更有效地处理它们。请参见this perlmonks discussionperlref : Making References

5
最好使用Class::->can($method)来获取代码引用。它遵循继承并且可以在严格模式下使用。此外,$metref->($instance, @args)甚至更快。 - Eric Strom

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接