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

Posted

技术标签:

【中文标题】使用闭包修改 Perl BEGIN 块中的类【英文标题】:Using closures to modify classes within Perl BEGIN blocks 【发布时间】:2012-03-24 23:38:07 【问题描述】:

前注: 为了这个讨论,请让我们暂时忽略这样一个事实,即通过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 块作为执行此类类操作的有效方法是个好主意吗? 什么时候不推荐?

【问题讨论】:

【参考方案1】:

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

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

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%              --

【讨论】:

谢谢。您关于 “将 BEGIN 块应用于访问器构建循环只是将工作从编译时间结束移动到运行时间开始” 确实推动了@schwern 的观点,即我所做的一切所做的是将工作从一个地方转移到另一个地方,而没有有效地优化任何东西。 那句话也错了!我的意思是 BEGIN 块将工作从运行时转移到编译时,但你知道的!关键是它只完成了一次,而不是像你想象的那样在每次实例化时都完成。【参考方案2】:

当我运行您的基准测试时,我会感到很不自在,这可能会导致您的差异。对于任何 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 没有单一的编译时和运行时。相反,used、required 或 evaled 的每一件事都经历了自己的编译和运行时步骤。 use Some::Class 导致 Some/Class.pm 经历编译和运行时执行 BEGIN、编译子例程然后执行任何其他代码。无论代码是在 BEGIN 块的内部还是外部在一个模块内,对于在该模块外部编码几乎没有什么区别。

【讨论】:

所以,其实我并没有节省时间。只需将相同的工作量(执行一次)转移到程序中的不同点。谢谢。 @SérgioBernardino 没错。 Class::Accessor 使用非常相似的技术,使用闭包构建访问器,只是更灵活。 Class::Accessor::Fast 几乎就是你写的。 Moose 可能会做同样的事情,或者它可能会评估一个字符串,我不确定。【参考方案3】:

如果你将包分离成它们自己的文件并使用use,区别就消失了:一个模块的代码在编译时运行。

【讨论】:

以上是关于使用闭包修改 Perl BEGIN 块中的类的主要内容,如果未能解决你的问题,请参考以下文章

Golang中的匿名函数(闭包)

Perl回调函数和闭包

关于perl闭包(个人理解)

Android Gradle 插件Module 目录下 build.gradle 配置文件 ( plugins 闭包代码块中引入插件 | PluginAware#apply 方法引入插件 )

swift学习第十五天:闭包

在闭包内声明的类与没有闭包的标准类