使用反射 $foo->$bar() 时性能中断

发布于 2024-12-19 08:27:51 字数 256 浏览 1 评论 0原文

我想知道当我使用反射来调用一个名称为字符串的方法时到底发生了什么:

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

比本机调用慢约 20%:

$foo->myMethod();

任何指向有关如何实现 perl 反射的文档的指针都会有所帮助。

谢谢。

i'd like to know what exactly happens when i use reflection to call a method whose name i have as a string:

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

is ~20% slower than native call:

$foo->myMethod();

Any pointers to documentation about how perl's reflection is implemented would be helpful.

Thanks.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

蘸点软妹酱 2024-12-26 08:27:52
> 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        <
gt; method_named[PV "bar"] ->6   # op to call the 'bar' method
-e syntax OK

在第一个示例中,perl 必须加载 $bar 变量,然后检查它是否包含可用作方法的名称或值。由于 $bar 的内容可能在调用之间发生变化,因此每次都必须执行此查找。

在第二个示例中,perl 已经知道字符串 "bar" 应该用作方法名称,因此这可以避免加载变量并在每次执行时检查其内容。

但您不必太担心两个本机操作之间 20% 的速度差异。主要是因为本机操作非常快,并且它们实际需要的任何速度很快就会与代码必须执行的实际算法相形见绌。换句话说,除非您使用代码分析器将此问题隔离为热点,否则速度差异的教学意义大于实际意义。

> 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        <
gt; method_named[PV "bar"] ->6   # op to call the 'bar' method
-e syntax OK

In the first example, perl has to load the $bar variable, and then check to see if it contains a name or value that can be used as a method. Since the contents of $bar could change between calls, this lookup must be done each time.

In the second example, perl already knows that the string "bar" should be used as a method name, so this avoids loading the variable and checking its contents on each execution.

But you should not worry too much about a 20% percent speed difference between two native operations. Mainly because native operations are very fast, and any speed that they actually do require will quickly be dwarfed by the actual algorithm your code has to perform. In other words, unless you have isolated this issue as a hot spot with a code profiler, the speed difference has more pedagogical than practical importance.

此刻的回忆 2024-12-26 08:27:52

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

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  <
gt; 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 似乎有点过分了。但可以肯定的是,与一些微小的东西相比,比如添加一个。

$ 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  <
gt; 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 纳秒吗?不,我猜。

First, I don't trust benchmarks I don't see. It's too easy to get them wrong. I benchmarked them myself.

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);

I can replicate your results.

     Rate   rt   ct
rt 1879/s   -- -19%
ct 2333/s  24%   --

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

That does seem rather big, but percentages can be very misleading with something so fast. Let's look at the difference in absolute times.

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.

Not that much. So where is that time spent?

$ 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  <
gt; 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

It seems the only difference is an extra symbol table lookup. 100ns seems excessive for that. But to be sure, compare to something tiny, say like adding one.

$ 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  <
gt; 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

Plugging that code and our $x = 100; into the benchmark code above, we get

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

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

So,

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

So is it reasonable for a simple symbol table lookup to take twice as long as adding one? Probably, since it involves hashing a string and looking for a string in short linked list.

Do you really care about spending an extra 100ns here and there? No, I'm guessing.

不及他 2024-12-26 08:27:52

您可以通过使用方法引用来加速此过程,ala:

$metref = \&{"Class::$method"};
$instance = new Class;
$instance->$metref(@args);

如果您在编译时知道方法名称,则显然可以使用 $metref = \&Class::myMethod 来代替。还有使用 sub { ... } 的闭包,perl 可能会比符号解除引用更有效地处理它们。请参阅此 perlmonks 讨论perlref :制作参考

You could speed this up by using a method reference, ala :

$metref = \&{"Class::$method"};
$instance = new Class;
$instance->$metref(@args);

You could obviously use $metref = \&Class::myMethod instead if you knew the method name at compile time. There are also closures using sub { ... } which perl might treat more efficiently than your symbolic dereferencing. See this perlmonks discussion and perlref : Making References.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文