【问题标题】:The fastest way (execution time) to find the longest element in an list查找列表中最长元素的最快方法(执行时间)
【发布时间】:2010-11-15 06:43:06
【问题描述】:

这是查找列表中最长元素的最快(执行时间)方法吗?

#!/usr/bin/env perl
use warnings;
use 5.012;
use List::Util qw(reduce);
use List::Util::XS;

my @array = qw( one two three four five six seven eight nine ten eleven );

my $l = reduce{ length($a) > length($b) ? $a : $b } @array;

say $l;

【问题讨论】:

  • 没有必要use List::Util::XS,除非你的程序必须死掉,除非安装了XS版本。 use List::Util 自动加载 XS 版本(如果可用)。
  • “最快”是指执行时间还是编码时间?
  • 您是否正在尝试优化您的应用程序?分析器是否真的告诉您应用程序中的这一行占用了大量的处理时间?
  • 至少你应该使用 > 而不是 gt,否则你会得到不正确的结果。
  • 对我来说听起来像是过早的优化。你的名单有多大?真正的应用是什么样的?

标签: perl algorithm performance


【解决方案1】:

当只尝试查找列表的一个元素时,不需要像这里的许多答案那样构建一个 N 大小的数据结构。最快的O(N) 方法是遍历数组,跟踪最大的元素。这样你就有O(N) 访问列表,O(1) 内存使用。

sub longest {
    my $max = -1;
    my $max_i = 0;
    for (0 .. $#_) {              # for each index
        my $len = length $_[$_];  # only get length once per item
        if ($len > $max) {        # save index and update max if larger
            $max = $len;
            $max_i = $_;
        }
    }
    $_[$max_i]   # return the largest item
}

如果您要多次运行上述代码,我建议您内联子例程的主体。

编辑:

drewk 的基准测试显示,上述代码中的数组索引有点瓶颈。多尝试了一下,终于找到了比reduce解决方案更快的方法:

sub fastest {
    my $max = -1;
    my $max_ref;
    for (@_) {
        if (length > $max) {  # no temp variable, length() twice is faster
            $max = length;
            $max_ref = \$_;   # avoid any copying
        }
    }
    $$max_ref
}

产生以下基准:

           Rate longest   drewk  reduce fastest
longest 44245/s      --    -21%    -30%    -47%
drewk   55854/s     26%      --    -11%    -33%
reduce  63014/s     42%     13%      --    -25%
fastest 83638/s     89%     50%     33%      --

【讨论】:

  • 我认为第二次调用length 与隐式$_ 的值相同可能是从你的fastest 的第一次调用中缓存的,不是吗?
  • @drewk => 我不确定优化是如何进行的。从 ysth 的 cmets 看来,Perl 似乎在 SV 中维护了一个存储长度的字段。所以我的猜测是,一旦计算了一次值,即使是乱序查找也将是常数时间。一般来说,进行基准测试以查看对内置函数的重复调用是否比制作词法副本更快总是好的。我已经测试了callerwantarrayref 等等。大多数情况下,重复调用它们会快得多。您需要 30 多次调用,词汇才能为您带来任何优势。
  • 有时人们称之为“高水位”方法。这就是我们在 Learning Perl 中所做的。
【解决方案2】:

这里是带有 for 和 less 变量的 OMG_peanuts 的略微修改版本:

my $len = length $array[0];
my $longest = 0;
for my $i (1 .. $#array) {
    my $i_len = length $array[$i];
    if($i_len > $len) {
        $longest = $i;
        $len = $i_len;
    }
}
my $l = $array[$longest];

我玩了一些基准测试,得到这个小数字(原始数组)

           Rate REDUCE TMPVAR TMPFOR
REDUCE 234862/s     --    -0%    -7%
TMPVAR 235643/s     0%     --    -6%
TMPFOR 251326/s     7%     7%     --

这用于更大的数量或项目(原始数组x 100

         Rate TMPVAR TMPFOR REDUCE
TMPVAR 3242/s     --   -28%   -32%
TMPFOR 4503/s    39%     --    -5%
REDUCE 4750/s    47%     5%     --

请注意,算法的适用性因数据细节而有很大差异(我猜更长的字符串可能会增加算法中length 函数的权重)。

编辑:这是基准测试的完整代码(长数组版本,短数组定义中缺少x 100

use Benchmark  qw(:all);
use List::Util qw(reduce);

my @array = qw( one two three four five six seven eight nine ten eleven ) x 100;

cmpthese(-2, {
    REDUCE => sub {
        my $l = reduce{ length($a) gt length($b) ? $a : $b } @array;
    },
    TMPVAR => sub {
        my $idx = 1;
        my $lastLength = length $array[0];
        my $lastElt = $array[0];
        my $listLength = scalar @array;
        while ($idx < $listLength) {
          my $tmpLength = length $array[$idx];
          if ($tmpLength > $lastLength) {
            $lastElt = $array[$idx];
            $lastLength = $tmpLength
          }
          $idx++
        }
        my $l = $lastElt;
    },
    TMPFOR => sub {
        my $len = length $array[0];
        my $longest = 0;
        for my $i (1 .. $#array) {
            my $i_len = length $array[$i];
            if($i_len > $len) {
                $longest = $i;
                $len = $i_len;
            }
        }
        my $l = $array[$longest];
    },
});

【讨论】:

  • 对于更长的字符串,除了 utf8 字符串之外,长度不会花费更长的时间,即使在那里我相信它会第一次被缓存。报告基准时,请始终显示您的确切基准代码。
  • @ysth:刚刚检查过,你是对的。显然length 是常数时间,它可能存储在 SV 中的某个地方。我以为 C 的 strlen 是在幕后使用的。
  • 不,不使用 strlen,因为 perl 字符串可以包含 nul 字符。 perl 只是跟踪使用了多少字符串缓冲区(除了 utf8 之外,它等于长度)
【解决方案3】:

我最快的是:

sub drewk {
    my $len = -1;
    for (@_) {
        my $tmp=length($_);
        if ( $tmp > $len ) {
            $longest = $_;
            $len = $tmp;
        }
    }
    return $longest;
}

但基准测试:

sub strom {
    my $max = -1;
    my $max_i = 0;
    for (0 .. $#_) {              # for each index
        my $len = length $_[$_];  # only get length once per item
        if ($len > $max) {        # save index and update max if larger
            $max = $len;
            $max_i = $_;
        }
    }
    $_[$max_i]   # return the largest item
}

sub red {
    return reduce{ length($a) > length($b) ? $a : $b } @_;
}

表明reduce 最快:

            Rate  strom  drewk reduce
strom  1323455/s     --   -38%   -45%
drewk  2144549/s    62%     --   -10%
reduce 2390707/s    81%    11%     --

另一个基准是Eric Strom's sub

【讨论】:

  • 看起来数组索引降低了我的第一个版本的速度。令人惊讶的是,临时长度词法的创建有点瓶颈。看看我最新的编辑版本,它只调用了两次长度。
【解决方案4】:

有点像高尔夫球:

my @unsorted = qw( one two three four five six seven eight nine ten eleven );
my $longest =  (
    map { $_->[0] } 
    sort { $b->[1] <=> $a->[1] } 
    map { [ $_, length $_ ] } @unsorted 
)[0];

say $longest;

编辑:map/sort/map 是Schwartzian transform,适用于不熟悉该技术并想知道的任何人。

【讨论】:

  • OP 正在寻找最快的解决方案,因此任何带有排序的东西(即 O(NlogN))都应该比 bvr 解决方案中的简单线性搜索 (O(N)) 慢。
  • 只有在对需要花费一些时间计算的属性进行排序时,施瓦茨变换才能节省时间。字符串的长度是已知的,无需计算任何成本。所以在这种情况下使用 Schwartzian 变换是徒劳的。
  • 我很确定,在我回答的时候,这个问题并没有说明我们是在执行时间方面还是最快/最简单的写作方式。
  • 至于 Schwartzian 变换是否可以节省时间,是的,你是对的。我试图避免大量调用 length() 的开销,但这确实很便宜,并且快速基准测试显示直接排序 { length($a) length($b) } 明显更快。过早的优化确实是万恶之源。
【解决方案5】:

假设目标只是找到最长的字符串,而不是它的索引:

my $longest = $array[0];
my $len = length $longest;
for my $str (@array) {
    if ( length($str) > $len ) {
        $longest = $str;
        $len = length($str);
    }
}

【讨论】:

    【解决方案6】:

    如果您真的想截断计算出的 length 的数量,请查看 Schwartian transform 并将其应用于您的问题。

    编辑:

    我看到没有人发布我的意思的完整示例,所以在这里(我还没有对它进行基准测试,因为我不在我的个人电脑上):

    my @array = qw( one two three four five six seven eight nine ten eleven );
    my $longest = (
                    reduce { $a->[1] > $b->[1] ? $a : $b } 
                    map { [ $_, length $_ ] }
                    @array
                  )[0];
    
    say $longest;
    

    【讨论】:

    • 我不知道它有名字:)
    • 我已经对它进行了基准测试,它比他的 reduce 在小列表上所做的要慢得多。
    • 只有在对需要花费一些时间计算的属性进行排序时,施瓦茨变换才能节省时间。字符串的长度是已知的,无需计算任何成本。所以在这种情况下使用 Schwartzian 变换是徒劳的。
    【解决方案7】:

    这似乎比其他解决方案快得多(基于fastest_Eric_Storm),

    use warnings;
    use 5.012;
    use Benchmark qw(:all) ;
    use List::Util qw(reduce);
    
    my @array = map { ($_) x 50 } qw( one two three four five six seven eight nine ten eleven );
    
    sub list_util_xs {
        my $l = reduce{ length($a) > length($b) ? $a : $b } @array;
        return $l;
    }
    
    sub fastest_Eric_Strom {
        my $max = -1; my $max_ref;
        for (@array) {
            if (length > $max) {
                $max = length;
                $max_ref = \$_;
            }
        }
        return $$max_ref;
    }
    
    sub ysth {
        my $longest = $array[0];
        my $len = length $longest;
        for my $str (@array) {
        if ( length($str) > $len ) {
            $longest = $str;
            $len = length($str);
        }
        }
        return $longest;
    }
    
    sub mpapec {
        my $max = -1;
        my $max_ref;
        length > $max and ($max, $max_ref) = (length, \$_) for @array;
        return $$max_ref;
    }
    
    cmpthese( -10, {
        'list_util_xs'  => sub{ list_util_xs() },
        'fastest_Eric_Storm' => sub{ fastest_Eric_Strom() },
        'ysth'      => sub{ ysth() },
        'mpapec'    => sub{ mpapec() },
    });
    

    输出

                          Rate list_util_xs fastest_Eric_Storm       ysth     mpapec
    list_util_xs       13479/s           --               -24%       -24%       -29%
    fastest_Eric_Storm 17662/s          31%                 --        -0%        -6%
    ysth               17680/s          31%                 0%         --        -6%
    mpapec             18885/s          40%                 7%         7%         --
    

    【讨论】:

      【解决方案8】:

      您可以使用一些临时变量来避免一次又一次地计算长度:

      my @unsorted = qw( one two three four five six seven eight nine ten eleven ); 
      my $idx = 1;
      my $lastLength = length $unsorted[0];
      my $lastElt = $unsorted[0];
      my $listLength = scalar @unsorted;
      while ($idx < $listLength) {
        my $tmpLength = length $unsorted[$idx];
        if ($tmpLength > $lastLength) {
          $lastElt = $unsorted[$idx];
          $lastLength = $tmpLength
        }
        $idx++
      }
      print "Longest element:$lastElt";
      

      基准测试结果:

                 Rate REDUCE TMPVAR
      REDUCE 169297/s     --   -29%
      TMPVAR 237926/s    41%     --
      

      【讨论】:

        【解决方案9】:

        首先我测试了所有例程是否都给了我正确的结果。 MBO 的例程没有通过第一个测试(它返回一个数组引用);为了给他第二次改变,我修改了例程以获得正确的结果。

        我运行了几次基准测试,但并不总是得到相同的顺序。 所以我想说(这里已经 sed)ysth 和 fasest_Eric_Strom 是最快的,但 list_utils reduce 几乎和它们一样快; 从结果中很容易看出,David Precious 排序版本最慢,MBO 修改后的 reduce-version 次之。

        我的结论:list_utils reduce 是最佳性价比的赢家。
        编辑: 颁奖仪式我太快了: List::Util - reduce - length - encoding - question

        David_Precious      64147/s             -- -36%        -73%               -79%  -80% -81%         -85%               -86% -87%
        MBO                100195/s            56%   --        -58%               -67%  -69% -70%         -77%               -79% -80%
        OMG_peanuts        237772/s           271% 137%          --               -21%  -27% -30%         -45%               -50% -52%
        longest_Eric_Strom 300466/s           368% 200%         26%                 --   -8% -11%         -31%               -36% -40%
        drewk              325883/s           408% 225%         37%                 8%    --  -4%         -25%               -31% -34%
        bvr                338156/s           427% 237%         42%                13%    4%   --         -22%               -28% -32%
        list_util_xs       434114/s           577% 333%         83%                44%   33%  28%           --                -8% -13%
        fastest_Eric_Strom 471812/s           636% 371%         98%                57%   45%  40%           9%                 --  -5%
        ysth               497198/s           675% 396%        109%                65%   53%  47%          15%                 5%   --
        

        .

        #!/usr/bin/env perl
        use warnings;
        use 5.012;
        use Benchmark qw(:all) ;
        use List::Util qw(reduce);
        
        my @array = qw( one two three four five six seven eight nine very_long_long ten eleven );
        
        sub list_util_xs {
            my $l = reduce{ length($a) > length($b) ? $a : $b } @array;
            return $l;
        }
        
        sub longest_Eric_Strom {
            my $max = -1; my $max_i = 0;
            for (0 .. $#array) {
                my $len = length $array[$_]; 
                if ($len > $max) { 
                    $max = $len;
                    $max_i = $_;
                }
            }
            return $array[$max_i];
        }
        
        sub fastest_Eric_Strom {
            my $max = -1; my $max_ref;
            for (@array) {
                if (length > $max) {
                    $max = length;
                    $max_ref = \$_;
                }
            }
            return $$max_ref;
        }
        
        sub David_Precious {
            my $longest =  ( map { $_->[0] } sort { $b->[1] <=> $a->[1] } map { [ $_, length $_ ] } @array )[0];
            return $longest;
        }
        
        sub MBO {
            my $longest = ( reduce { $a->[1] > $b->[1] ? $a : $b } map { [ $_, length $_ ] } @array )[0];
            return $longest->[0];
        }
        
        sub drewk {
            my $len = -1; my $longest;
            for (@array) {
                my $tmp=length($_);
                if ( $tmp > $len ) {
                    $longest = $_;
                    $len = $tmp;
                }
            }
            return $longest;
        }
        
        sub ysth {
            my $longest = $array[0];
            my $len = length $longest;
            for my $str (@array) {
            if ( length($str) > $len ) {
                $longest = $str;
                $len = length($str);
            }
            }
            return $longest;
        }
        
        sub bvr {
            my $len = length $array[0];
            my $longest = 0;
            for my $i (1 .. $#array) {
            my $i_len = length $array[$i];
            if($i_len > $len) {
                $longest = $i;
                $len = $i_len;
            }
            }
            return $array[$longest];
        }
        
        sub OMG_peanuts {
            my $idx = 1;
            my $lastLength = length $array[0];
            my $lastElt = $array[0];
            my $listLength = scalar @array;
            while ($idx < $listLength) {
            my $tmpLength = length $array[$idx];
            if ($tmpLength > $lastLength) {
            $lastElt = $array[$idx];
            $lastLength = $tmpLength
            }
            $idx++
            }
            return $lastElt;
        }
        
        cmpthese( -10, {
            'list_util_xs'  => sub{ list_util_xs() },
            'longest_Eric_Storm' => sub{ longest_Eric_Strom() },
            'fastest_Eric_Storm' => sub{ fastest_Eric_Strom() },
            'David_Precious'    => sub{ David_Precious() },
            'MBO'       => sub{ MBO() },
            'drewk'         => sub{ drewk() },
            'ysth'      => sub{ ysth() },
            'OMG_peanuts'   => sub{ OMG_peanuts() },
            'bvr'       => sub{ bvr() },
        });
        

        【讨论】:

        • 距离很小 - 在某些(不经常)运行中最快_Eric_Storm 是最快的。
        【解决方案10】:

        您可以通过减少到结构或数组来减少计算字符串长度的次数,其中包含字符串本身旁边的长度。

        此外,迭代是(将)由reduce 算法优化,length 调用几乎不可优化。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2018-07-26
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多