2016-06-08 31 views
3

简介:最佳实践命名约定子程序与输出参数

我通常不使用输出参数(参数充当输入和输出值或仅输出)。这是为了提高我的代码的可读性和可维护性。但是,再一次,我突然有了一个深深嵌套的子程序调用。我发现自己试图优化它以获得一些速度提升。

这是一个例子:我有一个字符串表示从文件读取的空白修剪线。如果该行为空,我想用代表返回键的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语句,拒绝参与这个选项杂乱的代码。

下面是另外两个选项,

  1. 传递一个参考$str以避免在子程序复制输入参数(即:转换呼叫按值来调用按引用),或

  2. 利用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的笔记本电脑,英特尔(R)核心(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是87%速度比情况1,比情况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{}转义以使代码更易于使用“\ N {DOWNWARDS ARROW WITH CORNER LEFTWARDS}”而不是“\”

2.对于这个简单的情况下,我同意这是有问题的,如果这可以被称为任何形式的杂波..

3. 4444444.44/2380952.38 = 1.87

+0

请注意,情况2比情况1快87%3,比情况3快56%。*“,如果您使用'cmpthese'而不是'timethese', – ikegami

+0

@ikegami是的,我考虑过使用'cmpthese',但是我发现这些数字让我困惑。 –

+0

你的每个函数都与其他函数有两种不同。缺少:'sub case4 if((!defined $ _ [0])|| $ _ [0] eq''){ return“\ x {} {}}; } return $ _ [0]; }' – ikegami

回答

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; 
    } 
} 
+0

对不起,我不太明白。为什么'fix_nulls($ s)'更容易阅读? –

+0

我认为这意味着该参数会比'handle_empty_lines'更好地修改,它可以做任何事情,比如忽略它们。 – Borodin

1

我总是去设计和clar性。我觉得一旦性能问题开始咬人界面设计,就需要重新设计了。 (对于我来说,常常意味着引入额外的图层。)我发现最后的讨论表明,您必须付出相当多的代价,因为所有选项都会出现问题。我会不是进入@_

因此,在这种情况下,我用值

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

原来“微型”优化版本的价值回报表现良好。在我看来,这些结果意味着在这里不需要进行艰难的速度设计决策。


在上面传递给函数的字符串是空的。这是一个简单单词的结果。

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

这在证明实际使用中更好的估计方面存在困难。真实计划中还有其他因素可能会使这些因素偏离。我仍然认为没有理由为速度而设计麻烦。

+0

你说的是你永远不会使用第二种情况(即使它会导致显着的加速你的程序),因为不可能为这种情况编写可维护的代码? (所以你会牺牲用户的维护者的舒适程序的经验) –

+1

@HåkonHægland那么...很难做出如此明确的表述。但是什么是显着的?如果简单的函数调用引入显着的减速,我认为应该回到绘图板。这不仅仅是方便 - 而是开发时间和错误。他们通过设计中的小裂缝。 #2的所有设计解决方案都很尴尬,而且很危险。第三个至少,但包括_how_的名称不是好的设计。另外,我在“wxWidgets”GUI中加速了最简单的函数调用 - 这很棘手。这很难估计得如此之好。 – zdim

+1

@HåkonHægland我已经添加了这两个函数[ikegami](http://stackoverflow.com/users/589924/ikegami)的基准。它似乎只落后了14%。让我知道如果把我的推理加到帖子上是个好主意。 – zdim

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 
    

除非这是一个常用的功能,这似乎是过早的优化的情况下。

+0

感谢您的补充测试!有趣但仍然'就地'要快得多。这是一个可怜的人,由于语言设计,不能在实践中使用它。我认为语言设计应该允许例如'$ str = #inplace($ str)',然后编译器只需将该调用改为'inplace $ str'。然后,可以实现效率和可维护性.. :) –

+0

它确实为'排序'。很难做到一般。 – ikegami

+0

我想这与perl版本有所不同,我有类似的“排序”,但结果非常不同。我注意到的一件事是,这些潜艇并不相当,因为“就地”变体缺乏返回声明。对我来说,这在5.16.2(Mac OS X)和5.18.1(VM在Linux中)之间约有13-15%的差异,约占3-4%。 – polettix