使用闭包来修改Perl BEGIN块中的类

时间:2012-03-07 12:30:09

标签: perl oop module closures accessor

FORWARD NOTE: 为了讨论起见,请暂时忽略这样一个事实,即可以通过Class::Accessor来实现同一目的,甚至是只需使用Moose(在考虑代码可读性和可维护性时可能会有更好的结果)。

关于面向对象的Perl,本书Programming Perl讨论了使用闭包生成存取方法的能力。例如,这是一段有效的代码:

#!perl

use v5.12;
use warnings;

# at run-time
package Person1;

my @attributes = qw/name age address/;

for my $att ( @attributes )
{
  my $accessor = __PACKAGE__ . "::$att";

  no strict 'refs'; # allow symbolic refs to typeglob

  *$accessor = sub {
    my $self = shift;
    $self->{$att} = shift if @_;
    return $self->{$att};
  };
}

sub new { bless {}, shift }

package main;

use Data::Dumper;

my $dude = Person1->new;
$dude->name('Lebowski');
say Dumper($dude);

在上面的例子中,如果我没有弄错的话,该类是在运行时组成的,其访问器是在实例化类的同时创建的。这意味着对象创建会受到速度惩罚。

现在考虑以下替代方案:

#!perl

use v5.12;
use warnings;

package Person2;

BEGIN
{
  for my $att (qw/name age address/)
  {
    my $accessor = __PACKAGE__ . "::$att";

    no strict 'refs'; # allow symbolic refs to typeglob

    *$accessor = sub {
      my $self = shift;
      $self->{$att} = shift if @_;
      return $self->{$att};
    };
  }
}

sub new { bless {}, shift }

package main;

use Data::Dumper;

my $dude = Person2->new;
$dude->name('Lebowski');
say Dumper($dude);

在这个版本上,组合是在BEGIN块内(即在编译时),我相信通过在程序的生命周期中尽快处理这个任务,我在运行时对象实例化期间节省了时间。

一个简单的Benchmark

# benchmark it!
package main;

use Benchmark qw/cmpthese/;

cmpthese(-2, {
  accessors_new   => sub { Person1->new },
  accessors_begin => sub { Person2->new },
});

似乎支持我的理论这些结果:

                    Rate accessors_begin   accessors_new
accessors_begin 853234/s              --             -9%
accessors_new   937924/s             10%              --

假设到目前为止我的推理是正确的,

  • 比较这两种策略时还有哪些其他好处/缺点?
  • 依靠BEGIN块作为进行这种类操作的有效方法是一个好主意吗?
  • 什么时候不推荐?

3 个答案:

答案 0 :(得分:10)

当我运行你的基准测试时,我会受到很大的影响,这可能会导致你的差异。对于10%或更小的差异,运行几次以确定。

                     Rate accessors_begin   accessors_new
accessors_begin 1865476/s              --             -4%
accessors_new   1943339/s              4%              --

                     Rate accessors_begin   accessors_new
accessors_begin 1978799/s              --             -1%
accessors_new   2001062/s              1%              --

                     Rate   accessors_new accessors_begin
accessors_new   1943339/s              --             -2%
accessors_begin 1988089/s              2%              --

                     Rate accessors_begin   accessors_new
accessors_begin 1796509/s              --             -8%
accessors_new   1949296/s              9%              --

                     Rate accessors_begin   accessors_new
accessors_begin 1916122/s              --             -3%
accessors_new   1969595/s              3%              --

但实际上你所有的基准测试都是sub new { bless {}, shift }。对自己进行基准测试同样会强调颤动。生成访问器的工作已经在代码加载时完成,并且从未进入,BEGIN阻止与否。

Perl没有一个编译时和运行时。相反,每个use d,require d或eval ed都会通过它自己的编译和运行时步骤。 use Some::Class导致Some/Class.pm遍历编译和运行时执行BEGIN,编译子例程然后执行任何其他代码。无论代码是在模块内的BEGIN块内部还是外部,对于模块外部的代码都没什么区别。

答案 1 :(得分:3)

  

在上面的例子中,如果我没有记错的话,这个班是由   运行时,其访问器与创建器同时创建   正在实例化类。这意味着会有速度   对象创造的惩罚。

你说访问器是在运行时创建的是正确的,但在你显示的代码中,它们只在执行开始时创建一次 - 当然不是在实例化时。您可以看到构造函数的作用:

sub new { bless {}, shift }

这是非常简短和相应的快速。将BEGIN块应用于访问器构建循环只会将工作从运行时间的开始移动到编译时间的结束,并且您什么也没有实现。你在基准测试中得到的变化是微不足道的,我认为主要是由于噪音。


我已经在我自己的系统上重现了你的基准测试,将运行时间提高到10秒,并通过四次测试获得了以下结果。似乎添加BEGIN块确实可以略微提高性能,但这是一个很小的改进,我无法立即解释它。

                     Rate   accessors_new accessors_begin
accessors_new   1463771/s              --             -1%
accessors_begin 1476583/s              1%              --


                     Rate   accessors_new accessors_begin
accessors_new   1469833/s              --             -0%
accessors_begin 1472234/s              0%              --


                     Rate   accessors_new accessors_begin
accessors_new   1454942/s              --             -1%
accessors_begin 1469680/s              1%              --


                     Rate   accessors_new accessors_begin
accessors_new   1462613/s              --             -1%
accessors_begin 1473985/s              1%              --

答案 2 :(得分:1)

如果将软件包分离为自己的文件并使用use,则差别很大:模块的代码正在编译时运行。