【问题标题】:When accessing individual characters in a string in Perl, is substr or splitting to an array faster?在 Perl 中访问字符串中的单个字符时,是 substr 还是拆分为数组更快?
【发布时间】:2010-10-21 00:00:51
【问题描述】:

我正在编写一个 Perl 脚本,我需要在其中循环遍历字符串的每个字符。有很多字符串,每个都有 100 个字符长(它们是很短的 DNA 序列,如果你想知道的话)。

那么,使用substr 一次提取每个字符是否更快,还是使用split 将字符串放入数组然后遍历数组更快?

在等待答案的同时,我想我会阅读有关如何在 Perl 中进行基准测试的内容。

【问题讨论】:

  • 你到底想做什么?举例说明你想对这些字符做什么。通常,有比substrsplit 更好的做事方式。另请参阅perldoc.perl.org/Benchmark.html
  • 我的即兴回答是“没关系”——先让你的脚本工作,然后再考虑优化它。至少在最初,“最快”的方法是最容易编写且没有错误的方法。
  • 我通常不会关心优化这么多,但我必须处理大约 400,000,000 个序列。我实际上是在计算每个位置的字符分布,所以我确实需要单个字符。
  • +1 了解如何对此进行基准测试。哪个更快可能取决于您对数据的处理方式,而不是一般情况下的答案。

标签: perl string performance character


【解决方案1】:

这实际上取决于您对数据的处理方式——但是,嘿,您的最后一个问题是正确的!不要猜测,基准测试。

Perl 为这种事情提供了Benchmark 模块,使用它真的非常简单。下面是一个小示例代码,可以开始使用:

#!/usr/bin/perl
use strict;
use warnings;
use Benchmark qw(cmpthese);

my $dna;
$dna .= [qw(G A T C)]->[rand 4] for 1 .. 100;

sub frequency_substr {
  my $length = length $dna;
  my %hist;

  for my $pos (0 .. $length) {
    $hist{$pos}{substr $dna, $pos, 1} ++;
  }

  \%hist;
}

sub frequency_split {
  my %hist;
  my $pos = 0;
  for my $char (split //, $dna) {
    $hist{$pos ++}{$char} ++;
  }

  \%hist;
}

sub frequency_regmatch {
  my %hist;

  while ($dna =~ /(.)/g) {
    $hist{pos($dna)}{$1} ++;
  }

  \%hist;
}


cmpthese(-5, # Run each for at least 5 seconds
  { 
    substr => \&frequency_substr,
    split => \&frequency_split,
    regex => \&frequency_regmatch
  }
);

还有一个示例结果:

         Rate  regex  split substr
regex  6254/s     --   -26%   -32%
split  8421/s    35%     --    -9%
substr 9240/s    48%    10%     --

事实证明 substr 速度惊人。 :)

【讨论】:

  • 是的,我的基准测试还显示 substr 在每种方法运行 100 万次后获胜。作为奖励,我在它运行时完成了一些家务。
  • 别忘了vec: $hist{$_}{vec $dna, $_, 8}++ for 0 .. $length => -3% of substr 在我的基准测试中。
  • 有趣的是,unpack 'C*' 实际上比split 慢(比substr-15%)---我想知道为什么。
【解决方案2】:

这是我要做的,而不是首先尝试在 substrsplit 之间进行选择:

#!/usr/bin/perl

use strict; use warnings;

my %dist;
while ( my $s = <> ) {
    while ( $s =~ /(.)/g ) {
        ++ $dist{ pos($s) }{ $1 };
    }
}

更新:

我的好奇心占了上风。这是一个基准:

#!/usr/bin/perl

use strict; use warnings;
use Benchmark qw( cmpthese );

my @chars = qw(A C G T);
my @to_split = my @to_substr = my @to_match = map {
    join '', map $chars[rand @chars], 1 .. 100
} 1 .. 1_000;

cmpthese -1, {
    'split'  => \&bench_split,
    'substr' => \&bench_substr,
    'match'  => \&bench_match,
};

sub bench_split {
    my %dist;
    for my $s ( @to_split ) {
        my @s = split //, $s;
        for my $i ( 0 .. $#s ) {
            ++ $dist{ $i }{ $s[$i] };
        }
    }
}

sub bench_substr {
    my %dist;
    for my $s ( @to_substr ) {
        my $u = length($s) - 1;
        for my $i (0 .. $u) {
            ++ $dist{ $i }{ substr($s, $i, 1) };
        }
    }
}

sub bench_match {
    my %dist;
    for my $s ( @to_match ) {
        while ( $s =~ /(.)/g ) {
            ++ $dist{ pos($s) }{ $1 };
        }
    }
}

输出:

 速率拆分匹配 substr
分裂 4.93/s -- -31% -65%
匹配 7.11/s 44% -- -49%
substr 14.0/s 184% 97% --

【讨论】:

  • 把这个答案看作是给 Perl (TMTOWTDI) 的情书,我觉得自己像个书呆子。 :)
【解决方案3】:

我在 Mastering Perl 中有一个例子来处理这个问题。您是否想创建一堆单独的标量,每个标量都带有 Perl 标量的内存开销,或者将所有内容存储在单个字符串中以减少内存但可能做更多的工作。你说你有很多这样的东西,所以如果你担心记忆,把它们保留为单个字符串可能会更好。

精通 Perl 还有几章涉及基准测试和分析,如果您对此感到好奇的话。

Ether 说先让它工作,然后再担心其余的事情。其中一部分是将操作隐藏在面向任务的界面后面。一个不错的面向对象模块可以为您做到这一点。如果你不喜欢这个实现,你就改变它。但是,更高级别的程序不必更改,因为界面保持不变。

【讨论】:

  • 基准脚本或多或少对我需要如何使用字符有正确的想法:作为哈希中的索引。我编写了自己的基准脚本,但这里的每个人都击败了我。我得到了同样的结果。内存不是问题,因为我一次处理一个字符串,每个字符串只有 100 个字符。
猜你喜欢
  • 1970-01-01
  • 2013-05-28
  • 2012-04-08
  • 2012-10-13
  • 1970-01-01
  • 2014-08-06
  • 2011-08-28
  • 2013-11-08
  • 1970-01-01
相关资源
最近更新 更多