【问题标题】:In Perl are there disadvantages to generating getters and setters rather than hard-coding them?在 Perl 中生成 getter 和 setter 而不是硬编码它们有缺点吗?
【发布时间】:2009-09-07 16:25:51
【问题描述】:

在下面的示例模块中,getter 和 setter 是通过将匿名子例程添加到符号表来生成的。在以这种方式创建方法之后,生成的代码是否在功能上等同于具有手动编写的 getter 和 setter 的模块(在行为、速度等方面),或者这种方法是否具有某种固有的责任? (我已经做了一些基本的速度基准测试,到目前为止还没有发现任何差异。)

package Module;    
use strict;
use warnings;

BEGIN {
    my @attr = qw(author title number);
    no strict 'refs';
    for my $a (@attr){
        *{__PACKAGE__ . "::get_$a"} = sub { $_[0]->{$a}         };
        *{__PACKAGE__ . "::set_$a"} = sub { $_[0]->{$a} = $_[1] };
    }
}

sub new {
    my $class = shift;
    bless { @_ }, $class;
}

1;

【问题讨论】:

  • *{"get_$a"} = sub ... 也应该可以工作。 (不需要有__PACKAGE__

标签: perl class module oop


【解决方案1】:

运行时性能应该没有差异如果两种情况下的结果代码相同。然而,这通常是不可能的,除非您使用字符串eval 来创建您的子例程。比如你提供的代码:

... = sub { $_[0]->{$a} };

将比您手动编写的代码慢一点:

sub foo { $_[0]->{'foo'} }

只是因为前者必须先获取变量 $a 的值,然后才能将其用作散列的键,而后者则使用常量作为其散列键。另外,顺便说一句,shift 通常比$_[0] 快。这是一些基准代码:

use Benchmark qw(cmpthese);

package Foo;

sub manual_shift { shift->{'foo'} }
sub manual_index { $_[0]->{'foo'} }

my $attr = 'foo';

*dynamic_shift = sub { shift->{$attr} };
*dynamic_index = sub { $_[0]->{$attr} };

package main;

my $o = bless { foo => 123 }, 'Foo';

cmpthese(-2, {
  manual_shift  => sub { my $a = $o->manual_shift },
  manual_index  => sub { my $a = $o->manual_index },
  dynamic_shift => sub { my $a = $o->dynamic_shift },
  dynamic_index => sub { my $a = $o->dynamic_index },
});

以及我系统上的结果:

                   Rate dynamic_index  manual_index dynamic_shift  manual_shift
dynamic_index 1799024/s            --           -3%           -4%           -7%
manual_index  1853616/s            3%            --           -1%           -4%
dynamic_shift 1873183/s            4%            1%            --           -3%
manual_shift  1937019/s            8%            4%            3%            --

它们非常接近,以至于差异可能会在噪音中消失,但经过多次试验,我认为您会发现“手动换档”变体是最快的。但与所有像这样的微基准测试一样,您必须在您的硬件和您的 perl 版本上测试您的确切场景以确保任何事情。

这里还有字符串 eval。

eval "sub eval_index { \$_[0]->{'$attr'} }";
eval "sub eval_shift { shift->{'$attr'} }";

它应该与“手动”变体完全相同,加上或减去统计噪音。我的结果:

                   Rate dynamic_index manual_index dynamic_shift manual_shift eval_shift eval_index
dynamic_index 1820444/s            --          -1%           -2%          -3%        -4%        -5%
manual_index  1835005/s            1%           --           -1%          -2%        -3%        -4%
dynamic_shift 1858131/s            2%           1%            --          -1%        -2%        -3%
manual_shift  1876708/s            3%           2%            1%           --        -1%        -2%
eval_shift    1894132/s            4%           3%            2%           1%         --        -1%
eval_index    1914060/s            5%           4%            3%           2%         1%         --

同样,这些都非常接近,以至于您必须付出巨大的努力并进行多次试验才能从噪音中挑选出信号。但是使用常量作为散列键和使用变量(必须首先检索其值)作为散列键之间的区别应该显示出来。 (shift 优化是一个单独的问题,更有可能在 perl 的过去或未来版本中以一种或另一种方式改变。)

【讨论】:

  • 你的 eval_index 需要 $_[0] 中的 $ 转义。
  • 如果您要生成访问器,您也可以使用 FAST 生成器。参照。 search.cpan.org/dist/Class-XSAccessor 这将比直接哈希访问更快。
  • ysth:谢谢,我认为反斜杠在本地编辑时被吃掉了。我已经更正了。
  • 将此添加到基准“sub eval_alias { my($arg)=@_; $arg->{'$attr'} }
  • Brad Gilbert:我试过了,它是迄今为止最慢的:eval_alias 1329967/s
【解决方案2】:

没有区别,因为:

sub Some_package::foo { ... }

只是以下的简写:

BEGIN { *Some_package::foo = sub { ... } }

来自perlmod的参考

【讨论】:

    【解决方案3】:

    生成良好的访问器的主要缺点是它们击败了依赖静态分析的工具。例如,您的 IDE 的方法自动完成。如果这是一个大项目的一部分,我衷心推荐你看看Moose。它的访问器生成正确(以及更多)。 IDE 中添加了支持已经足够流行,因此上述问题将在适当的时候消失。

    CPAN 上有许多易于使用并生成中等效率代码的访问器生成器。如果性能是一个问题,那么——如果你坚持使用访问器方法——你不会比Class::XSAccessor 更快,因为它为访问器使用高度优化的 C/XS 代码。

    滚动您自己的访问器生成代码是所有选项中最糟糕的。它永远打败了静态分析,可能相当难以阅读,并可能引入新的错误。

    【讨论】:

      【解决方案4】:

      这两种方法的结果都是在编译时将子例程引用安装到符号表中。行为和运行时性能将完全相同。编译时间的差异可能非常小(即可以忽略不计)。

      类似的方法是通过AUTOLOAD 按需生成访问器,这在运行时确实影响很小。使用AUTOLOAD 方法还可以改变$object->can() 之类的行为。

      显然,生成方法会将它们隐藏在任何形式的静态分析中,包括 ctags 之类的工具。

      【讨论】:

      • 说真的。不要将 AUTOLOAD 用于此类事情。它打开了通往精神错乱的闸门。 (我很确定你,MC,知道 :)
      • WARNING: 不要使用AUTOLOAD,除非您确切知道自己在做什么。
      【解决方案5】:

      运行时行为和性能应该几乎相同(除非您做一些关心方法是否为闭包的事情)。

      对于大量的属性,编译时间和内存使用会有所不同......两者都有利于生成的 getter 和 setter,而不是手动编写的。 例如,试试这个:

      BEGIN {
          no strict 'refs';
          for my $a ("aaaa".."zzzz"){
              *{__PACKAGE__ . "::get_$a"} = sub { $_[0]->{$a}         };
              *{__PACKAGE__ . "::set_$a"} = sub { $_[0]->{$a} = $_[1] };
          }
      }
      print `ps -F -p $$`;  # adjust for your ps syntax
      

      相比

      sub get_aaaa { $_[0]->{aaaa}         }
      sub set_aaaa { $_[0]->{aaaa} = $_[1] }
      sub get_aaab { $_[0]->{aaab}         }
      ...
      sub set_zzzy { $_[0]->{zzzy} = $_[1] }
      sub get_zzzz { $_[0]->{zzzz}         }
      sub set_zzzz { $_[0]->{zzzz} = $_[1] }
      print `ps -F -p $$`;  # adjust for your ps syntax
      

      【讨论】:

        【解决方案6】:

        唯一的区别是启动时间。对于简单的代码生成方案,差异将难以衡量。对于更复杂的系统,它可以加起来。

        Moose 就是一个很好的例子。 Moose 为您生成各种令人惊叹的代码,但它对启动时间有重大影响。 Moose 开发人员正在处理 pmc 文件中的 a scheme to cache generated code 并加载它们而不是每次都重新生成代码,这已经足够了。

        还可以考虑Class::Struct 之类的东西。它使用字符串 eval 生成代码(我上次检查过)。尽管如此,因为它非常简单,所以在启动时不会导致明显的减速。

        【讨论】:

          【解决方案7】:

          除了其他人提到的优点之外,我还想补充一下我发现的主要缺点:它们在分析器中都显示为匿名子例程。无论出于何种原因,Devel::DProf 只是不知道如何找出这个名字。

          现在我希望新的Devel::NYTProf 可能会做得更好——但我还没有尝试过。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2010-09-05
            • 2016-10-28
            • 1970-01-01
            • 2012-08-01
            • 2020-05-19
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多