简介
我通常不使用输出参数(作为输入和输出值或仅作为输出的参数)。这是为了提高我的代码的可读性和可维护性。但话说回来,前几天我突然有一个深深嵌套的子程序调用。我发现自己试图优化它以获得一些速度提升。
这是一个例子:我有一个字符串表示从文件中读取的空白修剪行。如果该行为空,我想用表示返回键的unicode符号替换它。假设我在谷歌搜索并发现符号↵
(unicode U+21B5
)看起来不错 [1] 。所以我写了一个简短的子程序:
sub handle_empty_lines {
my ( $str ) = @_;
if ( (!defined $str) || $str eq '' ) {
return "\x{21B5}";
}
return $str;
}
我用过这样的话:
$line = handle_empty_lines( $line );
现在,我想优化此调用,但仍然可以使代码具有可读性和可维护性。
第一个选项是将其内联:
$line = "\x{21B5}" 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] = "\x{21B5}";
}
}
sub case2 {
if ( (!defined $_[0]) || $_[0] eq '' ) {
$_[0] = "\x{21B5}";
}
}
sub case3 {
my ( $str ) = @_;
if ( (!defined $str) || $str eq '' ) {
return "\x{21B5}";
}
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] = "\x{21B5}";
}
}
然后,如果我用它来调用它:
handle_empty_lines( $line );
它没有告诉读者它修改了$line
。
我该如何处理?我可以想到两个选择:
致电后发表评论:
handle_empty_lines( $line ); # Note: modifies $line
更改子程序的名称。给出一个给出指示的名称
$line
被修改的读者,例如:
handle_empty_lines__modifies_arg( $line );
<子>的脚注: 子>
<子>
1.我后来发现我可以使用N{}
转义来使代码更易读,使用&#34; \ N {DOWNWARDS ARROW WITH CORNER LEFTWARDS}&#34;而不是&#34; \ x&#34;&#34;
子>
<子> 对于这个简单的案例,我同意这是否可以被称为任何形式的混乱是值得怀疑的。 子>
3.4444444.44 / 2380952.38 = 1.87 功能
答案 0 :(得分:2)
我会写这个
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] ) {
$_ = "\x{21B5}" unless defined and length;
}
}
答案 1 :(得分:1)
我总是追求设计和清晰度。我觉得一旦性能问题开始引入界面设计,就应该重新设计了。 (对我而言,通常意味着引入额外的层。)我发现你的最后讨论表明,由于所有选项都存在问题,因此需要付出相当多的代价。我不进入@_
。
所以在这种情况下,我会继续按值返回
$line = handle_empty_lines( $line );
sub handle_empty_lines {
defined $_[0] && $_[0] ne '' && return $_[0];
return "\x{21B5}";
}
或通过引用,我希望它会更快一些。
handle_empty_lines( \$line);
sub handle_empty_lines {
defined ${$_[0]} && ${$_[0]} ne '' && return 1;
${$_[0]} = "\x{21B5}";
}
更常见的情况应该首先出现,而删除分支if
测试的优化有助于,如下面的基准测试所示,但很少。
我会选择哪种更适合设计。
我已将这两个添加到ikegami发布的基准
sub micro {
defined $_[0] && $_[0] ne '' && return $_[0];
return "\x{21B5}";
}
sub microref {
defined ${$_[0]} && ${$_[0]} ne '' && return 1;
${$_[0]} = "\x{21B5}";
}
添加了这两个功能的ikegami基准测试的完整结果
Rate with_ref microref baseline by_ref micro inplace with_ref 1522762/s -- -13% -19% -34% -36% -43% microref 1742620/s 14% -- -8% -24% -26% -35% baseline 1890650/s 24% 8% -- -18% -20% -29% by_ref 2296676/s 51% 32% 21% -- -3% -14% micro 2360880/s 55% 35% 25% 3% -- -12% inplace 2680740/s 76% 54% 42% 17% 14% --
事实证明,具有按价值返回的'mirco'优化版本表现良好。在我看来,这些结果意味着在这里不需要为速度做出艰难的设计决策。
在上面,传递给函数的字符串是空的。这是一个简单的单词时的结果。
Rate with_ref microref baseline by_ref micro inplace with_ref 1470954/s -- -16% -21% -34% -39% -55% microref 1742620/s 18% -- -6% -21% -27% -47% baseline 1854974/s 26% 6% -- -16% -23% -43% by_ref 2213644/s 50% 27% 19% -- -8% -32% micro 2399206/s 63% 38% 29% 8% -- -27% inplace 3267412/s 122% 87% 76% 48% 36% --
这证明了在如此精细地估计实际使用中更好的方面的困难。实际程序中还有其他因素可能会扭曲这些因素。我仍然认为没有理由为速度设计而烦恼。
答案 2 :(得分:1)
您的测试每个都会改变多个变量。这是一个更好的测试:
use strict;
use warnings;
use Benchmark qw( cmpthese );
sub baseline {
my ( $str ) = @_;
if ( (!defined $str) || $str eq '' ) {
return "\x{21B5}";
}
return $str;
}
sub with_ref {
my ( $ref ) = @_;
if ( (!defined $$ref) || $$ref eq '' ) {
return "\x{21B5}";
}
return $$ref;
}
sub by_ref {
if ( (!defined $_[0]) || $_[0] eq '' ) {
return "\x{21B5}";
}
return $_[0];
}
sub inplace {
if ( (!defined $_[0]) || $_[0] eq '' ) {
$_[0] = "\x{21B5}";
}
}
{
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
除非这是一个常用功能,否则这似乎是过早优化的情况。