Perl 中带有输出参数的子例程的最佳实践命名约定
Posted
技术标签:
【中文标题】Perl 中带有输出参数的子例程的最佳实践命名约定【英文标题】:Best practice naming convention in Perl for subroutine with output arguments 【发布时间】:2016-06-08 19:16:37 【问题描述】:简介:
我通常不使用输出参数(作为输入和输出值或仅作为输出的参数)。这是为了提高我的代码的可读性和可维护性。但是话又说回来,前几天我突然有一个嵌套很深的子程序调用。我发现自己正在尝试优化它以提高速度。
这是一个示例:我有一个字符串,表示从文件中读取的空白修剪行。如果该行是空的,我想用代表返回键的 unicode 符号替换它。假设我在 google 上搜索并发现符号 ↵
(unicode U+21B5
)看起来不错[1]。于是我写了一个简短的子程序:
sub handle_empty_lines
my ( $str ) = @_;
if ( (!defined $str) || $str eq '' )
return "\x21B5";
return $str;
我是这样使用它的:
$line = handle_empty_lines( $line );
现在,我想优化这个调用,但代码仍然可读和可维护。
第一个选项是内联:
$line = "\x21B5" if (!defined $str) || $str eq '';
当然是自然而快速的,但让我们假设我不想用 if 语句2 弄乱代码并拒绝这个选项。
这里有另外两个选项,
传递对$str
的引用以避免在子例程中复制输入参数(即:将按值调用转换为按引用调用),或
利用 Perl 的内置引用机制调用。
这两个选项都引入了"input/output argument"
(即:作为输入和输出的参数),从而降低了代码的可读性并使维护更加困难(在我看来)。
让第三个选项是保留原始版本(按值调用)。以下是三个选项的快速比较,仅用于速度(不是可读性)。
use strict;
use warnings;
use Benchmark qw(timethese);
my $str1 = '';
timethese(
2_000_000,
case1 => sub my $test = $str1; case1( \$test ) ,
case2 => sub my $test = $str1; case2( $test ) ,
case3 => sub my $test = $str1; $test = case3( $test ) ,
);
sub case1
if ( (!defined $$_[0]) || $$_[0] eq '' )
$$_[0] = "\x21B5";
sub case2
if ( (!defined $_[0]) || $_[0] eq '' )
$_[0] = "\x21B5";
sub case3
my ( $str ) = @_;
if ( (!defined $str) || $str eq '' )
return "\x21B5";
return $str;
输出是(Ubuntu 笔记本电脑,Intel(R) Core(TM) i7-4702MQ CPU @ 2.20GHz):
Benchmark: timing 2000000 iterations of case1, case2, case3...
case1: 1 wallclock secs ( 0.84 usr + 0.00 sys = 0.84 CPU) @ 2380952.38/s (n=2000000)
case2: 1 wallclock secs ( 0.45 usr + 0.00 sys = 0.45 CPU) @ 4444444.44/s (n=2000000)
case3: 1 wallclock secs ( 0.70 usr + 0.00 sys = 0.70 CPU) @ 2857142.86/s (n=2000000)
请注意,案例 2 比案例 1 快 87%3,比案例 3 快 56%。
有趣的是,引用调用(案例 1)比值调用(案例 3)慢。
问题:
假设我现在想保留案例 2:
sub handle_empty_lines
if ( (!defined $_[0]) || $_[0] eq '' )
$_[0] = "\x21B5";
然后,如果我使用:
handle_empty_lines( $line );
它没有告诉读者它修改了$line
。
我应该如何处理这个问题?我可以想到两个选择:
在通话后发表评论:
handle_empty_lines( $line ); # Note: modifies $line
更改子程序的名称。取一个能表明
$line
被修改的读者,例如:
handle_empty_lines__modifies_arg( $line );
脚注:
1. 后来我发现我可以使用N
转义来使代码更具可读性,使用“\NDOWNWARDS ARROW WITH CORNER LEFTWARDS”而不是“\x21B5”
2.对于这个简单的案例,我同意这是否可以称为任何形式的混乱都是值得怀疑的。
3. 4444444.44 / 2380952.38 = 1.87
【问题讨论】:
Re "注意案例 2 比案例 1 快 87%3,比案例 3 快 56%。"曾使用cmpthese
而不是timethese
@ikegami 是的,我考虑过使用cmpthese
,但我发现这些数字让我感到困惑
您的每个功能都以两种方式与其他功能不同。明显缺席:sub case4 if ( (!defined $_[0]) || $_[0] eq '' ) return "\x21B5"; return $_[0];
最好的建议是根本不要使用输出参数。副作用是一种必要的邪恶,它们使你的代码更难组合和推理,所以尽可能避免它们。修复对函数的调用,而不是函数本身。
【参考方案1】:
我会写这个
use utf8;
use strict;
use warnings 'all';
use feature 'say';
use open qw/ :std :encoding(utf-8) /;
for ( '', undef, 'xx' )
my $s = $_;
fix_nulls($s);
say $s;
sub fix_nulls
for ( $_[0] )
$_ = "\x21B5" unless defined and length;
【讨论】:
对不起,我不太明白。为什么fix_nulls( $s )
更容易阅读?
我认为这意味着参数将被修改得比handle_empty_lines
更好,这几乎可以做任何事情,比如忽略它们【参考方案2】:
您的测试每次都会更改多个变量。这是一个更好的测试:
use strict;
use warnings;
use Benchmark qw( cmpthese );
sub baseline
my ( $str ) = @_;
if ( (!defined $str) || $str eq '' )
return "\x21B5";
return $str;
sub with_ref
my ( $ref ) = @_;
if ( (!defined $$ref) || $$ref eq '' )
return "\x21B5";
return $$ref;
sub by_ref
if ( (!defined $_[0]) || $_[0] eq '' )
return "\x21B5";
return $_[0];
sub inplace
if ( (!defined $_[0]) || $_[0] eq '' )
$_[0] = "\x21B5";
my $str = '';
cmpthese(-3,
baseline => sub my $test = $str; $test = baseline $test; ,
with_ref => sub my $test = $str; $test = with_ref \$test; ,
by_ref => sub my $test = $str; $test = by_ref $test; ,
inplace => sub my $test = $str; inplace $test; ,
);
结果:
Rate with_ref baseline by_ref inplace
with_ref 1252657/s -- -14% -27% -45%
baseline 1461434/s 17% -- -15% -36%
by_ref 1718499/s 37% 18% -- -24%
inplace 2271005/s 81% 55% 32% --
添加引用会减慢速度。
保持匿名比基线快 18%,节省 0.14 微秒。
$ perl -E'say 1/1718499 - 1/2271005'
1.41569475973388e-07
就地版本比基线快 55%,节省 0.24 微秒。
$ perl -E'say 1/1461434 - 1/2271005'
2.4392574799202e-07
除非这是一个常用的函数,否则这似乎是过早优化的情况。
【讨论】:
感谢添加的测试!有趣但inplace
仍然更快。遗憾的是,仅由于语言设计,无法在实践中使用它。我认为语言设计应该允许例如$str = #inplace( $str )
,然后编译器只需将调用更改为inplace $str
。然后可以同时实现效率和可维护性.. :)
它对sort
确实如此。一般很难做到。
我猜这因 perl 版本而异,我有类似的“排序”,但结果却大不相同。我注意到的一件事是 subs 不是相当等价的,因为“inplace”变体缺少 return 语句。对我来说,这在 5.16.2(Mac OS X)和 5.18.1(Linux in VM)中占了大约 3-4% 的差异。
@polettix,不,删除退货是公平的。其他人需要它,但对于就地潜艇来说没有意义。如果删除它是一种增益,那么增益是切换到就地的一部分,将它留在原地是不公平的。
@ikegami 我明白了。那么不应该将通过提供的输入引用更改输入参数的“with_ref”添加到批次中吗?【参考方案3】:
我总是追求设计和清晰。我觉得一旦性能问题开始影响界面设计,就该重新设计了。 (对我而言,这通常意味着引入额外的层。)我发现您的最后讨论表明,由于所有选项都存在问题,因此必须付出很多。我不会进入@_
。
所以在这种情况下,我会选择按值返回
$line = handle_empty_lines( $line );
sub handle_empty_lines
defined $_[0] && $_[0] ne '' && return $_[0];
return "\x21B5";
或通过引用,我希望它会快一点。
handle_empty_lines( \$line);
sub handle_empty_lines
defined $$_[0] && $$_[0] ne '' && return 1;
$$_[0] = "\x21B5";
更常见的情况当然应该首先出现,而删除分支if
测试的优化会有所帮助,如下面的基准测试所示,但作用不大。
我会根据哪个更适合整体代码设计来选择。
我已将这两个添加到ikegami 发布的基准中,作为
sub micro
defined $_[0] && $_[0] ne '' && return $_[0];
return "\x21B5";
sub microref
defined $$_[0] && $$_[0] ne '' && return 1;
$$_[0] = "\x21B5";
添加这两个函数的ikegami 的基准测试的完整结果
用_ref microref 基线评价 by_ref micro inplace with_ref 1522762/s -- -13% -19% -34% -36% -43% 微参考 1742620/s 14% -- -8% -24% -26% -35% 基线 1890650/s 24% 8% -- -18% -20% -29% by_ref 2296676/s 51% 32% 21% -- -3% -14% 微 2360880/s 55% 35% 25% 3% -- -12% 就地 2680740/s 76% 54% 42% 17% 14% --事实证明,具有按值返回的“mirco”优化版本运行良好。在我看来,这些结果意味着为了速度而做出艰难的设计决策是没有必要的。
在上面传递给函数的字符串是空的。这是一个简单单词时的结果。
用_ref microref 基线评价 by_ref micro inplace with_ref 1470954/s -- -16% -21% -34% -39% -55% 微参考 1742620/s 18% -- -6% -21% -27% -47% 基线 1854974/s 26% 6% -- -16% -23% -43% by_ref 2213644/s 50% 27% 19% -- -8% -32% 微 2399206/s 63% 38% 29% 8% -- -27% 就地 3267412/s 122% 87% 76% 48% 36% --这证明了难以如此精细地估计实际使用中什么更好。实际程序中还有其他因素可能会影响这些因素。我仍然认为没有理由为速度设计而烦恼。
【讨论】:
你说的是你永远不会使用案例2(即使它会导致你的程序显着加速),因为不可能为这种情况编写可维护的代码? (所以你会为了维护者的舒适而牺牲用户对程序的体验) @HåkonHægland 嗯......很难做出如此明确的声明。但是什么是重要足够的呢?如果简单的函数调用会导致明显的减速,我认为绝对应该回到绘图板上。这不仅仅是方便 - 它是开发时间和错误。他们经历了设计中的小裂缝。 #2 的所有设计解决方案都是笨拙且危险的。第三个最少,但在名称中包含 how 并不是好的设计。此外,我还尝试过在wxWidgets
GUI 中加速最简单的函数调用——这很棘手。很难估计得这么精细。
@HåkonHægland 我已将这两个函数添加到ikegami 的基准测试中。它似乎只落后 %14。让我知道将上面的推理添加到帖子中是否是个好主意。以上是关于Perl 中带有输出参数的子例程的最佳实践命名约定的主要内容,如果未能解决你的问题,请参考以下文章