【问题标题】:Why does Perl's tr/\n// get slower and slower as line lengths increase?为什么 Perl 的 tr/\n// 随着行长的增加越来越慢?
【发布时间】:2009-12-25 08:21:51
【问题描述】:

perlfaq5 中,有How do I count the number of lines in a file? 的答案。当前答案建议sysreadtr/\n//。我想尝试一些其他的事情来看看tr/\n// 的速度会快多少,并且还想针对具有不同平均行长度的文件进行尝试。我创建了一个基准来尝试各种方法来做到这一点。我在 MacBook Air 上的 Mac OS X 10.5.8 和 Perl 5.10.1 上运行它:

  • wc 发起攻击(除了短线最快)
  • tr/\n//(次快,除了较长的平均线长度)
  • s/\n//g(通常很快)
  • while( <$fh> ) { $count++ }(几乎总是慢吞吞,除非tr/// 陷入困境)
  • 1 while( <$fh> ); $.(非常快)

让我们忽略wc,即使有所有 IPC 的东西,它也确实带来了一些有吸引力的数字。

乍一看,tr/\n// 在行长很短(比如 100 个字符)时看起来非常好,但当行长变大(一行 1,000 个字符)时,它的性能就会下降。线条越长,tr/\n// 的效果就越差。我的基准测试是否有问题,或者内部是否有其他事情导致tr/// 降级?为什么s/// 不会同样降级?

首先,结果。

                         Rate very_long_lines-tr very_long_lines-$count very_long_lines-$. very_long_lines-s very_long_lines-wc
very_long_lines-tr     1.60/s                 --                   -10%               -12%              -39%               -72%
very_long_lines-$count 1.78/s                11%                     --                -2%              -32%               -69%
very_long_lines-$.     1.82/s                13%                     2%                 --              -31%               -68%
very_long_lines-s      2.64/s                64%                    48%                45%                --               -54%
very_long_lines-wc     5.67/s               253%                   218%               212%              115%                 --
                    Rate long_lines-tr long_lines-$count long_lines-$. long_lines-s long_lines-wc
long_lines-tr     9.56/s            --               -5%           -7%         -30%          -63%
long_lines-$count 10.0/s            5%                --           -2%         -27%          -61%
long_lines-$.     10.2/s            7%                2%            --         -25%          -60%
long_lines-s      13.6/s           43%               36%           33%           --          -47%
long_lines-wc     25.6/s          168%              156%          150%          88%            --
                     Rate short_lines-$count short_lines-s short_lines-$. short_lines-wc short_lines-tr
short_lines-$count 60.2/s                 --           -7%           -11%           -34%           -42%
short_lines-s      64.5/s                 7%            --            -5%           -30%           -38%
short_lines-$.     67.6/s                12%            5%             --           -26%           -35%
short_lines-wc     91.7/s                52%           42%            36%             --           -12%
short_lines-tr      104/s                73%           61%            54%            14%             --
                      Rate varied_lines-$count varied_lines-s varied_lines-$. varied_lines-tr varied_lines-wc
varied_lines-$count 48.8/s                  --            -6%             -8%            -29%            -36%
varied_lines-s      51.8/s                  6%             --             -2%            -24%            -32%
varied_lines-$.     52.9/s                  8%             2%              --            -23%            -30%
varied_lines-tr     68.5/s                 40%            32%             29%              --            -10%
varied_lines-wc     75.8/s                 55%            46%             43%             11%              --

这是基准。我确实有一个控制在那里,但它是如此之快,我只是不理会它。第一次运行它时,基准测试会创建测试文件并打印一些关于它们的行长的统计数据:

use Benchmark qw(cmpthese);
use Statistics::Descriptive;

my @files = create_files();

open my( $outfh ), '>', 'bench-out';

foreach my $file ( @files )
    {
    cmpthese(
        100, {
#               "$file-io-control" => sub { 
#                       open my( $fh ), '<', $file; 
#                   print "Control found 99999 lines\n";
#                       },
               "$file-\$count" => sub { 
                    open my( $fh ), '<', $file; 
                    my $count = 0;
                    while(<$fh>) { $count++ } 
                    print $outfh "\$count found $count lines\n";
                    },
               "$file-\$."     => sub { 
                    open my( $fh ), '<', $file; 
                    1 while(<$fh>); 
                    print $outfh "\$. found $. lines\n";
                    },
               "$file-tr"      => sub { 
                    open my( $fh ), '<', $file; 
                    my $lines = 0;
                    my $buffer;
                    while (sysread $fh, $buffer, 4096) {
                        $lines += ($buffer =~ tr/\n//);
                        }
                    print $outfh "tr found $lines lines \n";
                    },
               "$file-s"       => sub { 
                    open my( $fh ), '<', $file; 
                    my $lines = 0;
                    my $buffer;
                    while (sysread $fh, $buffer, 4096) {
                        $lines += ($buffer =~ s/\n//g);
                        }
                    print $outfh "s found $lines line\n";
                    },
               "$file-wc"       => sub { 
                    my $lines = `wc -l $file`;
                    chomp( $lines );
                    print $outfh "wc found $lines line\n";
                    },
                    }
           );   
     }

sub create_files
    {
            my @names;
    my @files = (
        [ qw( very_long_lines 10000  4000 5000 ) ],
        [ qw( long_lines   10000 700 800 ) ],
        [ qw( short_lines  10000  60  80 ) ],
        [ qw( varied_lines 10000  10 200 ) ],
        );

    foreach my $tuple ( @files )
        {
        push @names, $tuple->[0];
        next if -e $tuple->[0];
        my $stats = create_file( @$tuple );
        printf "%10s: %5.2f  %5.f \n", $tuple->[0], $stats->mean, sqrt( $stats->variance );
        }

    return @names;
    }


sub create_file
    {
    my( $name, $lines, $min, $max ) = @_;

    my $stats = Statistics::Descriptive::Full->new();

    open my( $fh ), '>', $name or die "Could not open $name: $!\n";

    foreach ( 1 .. $lines )
        {
        my $line_length = $min + int rand( $max - $min );
        $stats->add_data( $line_length );
        print $fh 'a' x $line_length, "\n";
        }

    return $stats;
    }

【问题讨论】:

  • 请分享您的操作系统和 Perl 版本
  • OSX (10.5.8) Perl 5.10.1,我看到的结果类似于 brian 的。但是,我也收到关于“迭代次数太少而无法可靠计数”的重复警告,仅供参考。
  • @brian:今年六月/七月的 MacBookPro。不是那么微不足道,尽管也不是当前的 4GB RAM 型号之一。 (2.26GHz Core2Duo,2 GB RAM,1067MHz,如果重要的话。)这样的结果是否取决于文件系统(部分?)?
  • @Sneakyness:嗯?请参阅我的 cmets 和其他一些人的回复。问题不仅限于一种机器类型。
  • 我突然想到tr/// 基准测试可能有太多活动部件。随着行越来越长,我们不仅仅是在测试tr/// 处理长字符串的能力;我们还修改了行长和sysread 缓冲区大小之间的相对关系。我对这个问题不是很了解,所以也许这是一个虚假的考虑。但是,如果问题是“tr/// 使用长字符串会减慢速度吗?”基准测试不应该只关注这一点而不是涉及 IO 问题吗?

标签: perl count benchmarking


【解决方案1】:

我想知道我们一直在使用的基准测试是否有太多移动部件:我们正在处理不同大小的数据文件,使用不同的行长,并试图衡量 tr 相对于其竞争对手的速度 - 与一个基本的(但未经测试的)假设,即tr 是性能随行长而变化的方法。

另外,正如 brian 在一些 cmets 中指出的那样,我们正在提供tr 的数据缓冲区,这些缓冲区的大小始终相同(4096 字节)。如果任何方法对行大小​​不敏感,则应为tr

然后让我印象深刻:如果tr 是稳定的参考点,而其他方法是随线条大小变化的方法呢?当您从飞船的窗户向外看时,是您还是那只克林贡猛禽在移动?

所以我开发了一个基准来保持数据文件的大小不变:行长不同,但总字节数保持不变。结果显示:

  • tr 是最不敏感的方法 线长度的变化。自从 处理的总字节数为 所有三个线长的常数 经过测试(短,中,长),这个 意味着tr 在 编辑给定的字符串。甚至 虽然短线数据文件 需要更多编辑,tr 方法能够处理数据 文件几乎和它处理文件一样快 长行文件。
  • 依赖&lt;&gt;速度的方法 随着线条变长, 虽然以递减的速度。这个 有道理:因为每次调用&lt;&gt; 需要一些工作,应该是 处理给定 N 个字节的速度较慢 使用较短的行(至少超过 测试范围)。
  • s/// 方法也很敏感 到行长。喜欢tr,这个 方法通过编辑字符串起作用 它是给定的。再次,较短的线 长度意味着更多的编辑。显然, s/// 的能力 编辑效率远低于 tr.

以下是在 Solaris 上使用 Perl 5.8.8 的结果:

#   ln = $.      <>, then check $.
#   nn = $n      <>, counting lines
#   tr = tr///   using sysread
#   ss = s///    using sysread

#   S = short lines  (50)
#   M = medium lines (500)
#   L = long lines   (5000)

       Rate nn-S
nn-S 1.66/s   --
ln-S 1.81/s   9%
ss-S 2.45/s  48%
nn-M 4.02/s 142%
ln-M 4.07/s 145%
ln-L 4.65/s 180%
nn-L 4.65/s 180%
ss-M 5.85/s 252%
ss-L 7.04/s 324%
tr-S 7.30/s 339%    # tr
tr-L 7.63/s 360%    # tr
tr-M 7.69/s 363%    # tr

Windows ActiveState 的 Perl 5.10.0 上的结果大致相当。

最后是代码:

use strict;
use warnings;
use Set::CrossProduct;
use Benchmark qw(cmpthese);

# Args: file size (in million bytes)
#       N of benchmark iterations
#       true/false (whether to regenerate files)
#
# My results were run with 50 10 1
main(@ARGV);

sub main {
    my ($file_size, $benchmark_n, $regenerate) = @_;
    $file_size *= 1000000;
    my @file_names = create_files($file_size, $regenerate);
    my %methods = (
        ln => \&method_ln,  # $.
        nn => \&method_nn,  # $n
        tr => \&method_tr,  # tr///
        ss => \&method_ss,  # s///
    );
    my $combo_iter = Set::CrossProduct->new([ [keys %methods], \@file_names ]);
    open my $log_fh, '>', 'log.txt';
    my %benchmark_args = map {
        my ($m, $f) = @$_;
        "$m-$f" => sub { $methods{$m}->($f, $log_fh) }
    } $combo_iter->combinations;
    cmpthese($benchmark_n, \%benchmark_args);
    close $log_fh;
}

sub create_files {
    my ($file_size, $regenerate) = @_;
    my %line_lengths = (
        S =>    50,
        M =>   500,
        L =>  5000,
    );
    for my $f (keys %line_lengths){
        next if -f $f and not $regenerate;
        create_file($f, $line_lengths{$f}, $file_size);
    }
    return keys %line_lengths;
}

sub create_file {
    my ($file_name, $line_length, $file_size) = @_;
    my $n_lines = int($file_size / $line_length);
    warn "Generating $file_name with $n_lines lines\n";
    my $line = 'a' x ($line_length - 1);
    chop $line if $^O eq 'MSWin32';
    open(my $fh, '>', $file_name) or die $!;
    print $fh $line, "\n" for 1 .. $n_lines;
    close $fh;
}

sub method_nn {
    my ($data_file, $log_fh) = @_;
    open my $data_fh, '<', $data_file;
    my $n = 0;
    $n ++ while <$data_fh>;
    print $log_fh "$data_file \$n $n\n";
    close $data_fh;
}

sub method_ln {
    my ($data_file, $log_fh) = @_;
    open my $data_fh, '<', $data_file;
    1 while <$data_fh>;
    print $log_fh "$data_file \$. $.\n";
    close $data_fh;
}

sub method_tr {
    my ($data_file, $log_fh) = @_;
    open my $data_fh, '<', $data_file;
    my $n = 0;
    my $buffer;
    while (sysread $data_fh, $buffer, 4096) {
        $n += ($buffer =~ tr/\n//);
    }
    print $log_fh "$data_file tr $n\n";
    close $data_fh;
}

sub method_ss {
    my ($data_file, $log_fh) = @_;
    open my $data_fh, '<', $data_file;
    my $n = 0;
    my $buffer;
    while (sysread $data_fh, $buffer, 4096) {
        $n += ($buffer =~ s/\n//g);
    }
    print $log_fh "$data_file s/ $n\n";
    close $data_fh;
}

针对 Brad 的评论进行更新。我尝试了所有三种变体,它们的行为大致类似于 s/\n//g——对于较短行的数据文件,速度较慢(附加限定为 s/(\n)/$1/甚至比其他人慢)。有趣的是m/\n/gs/\n//g 的速度基本相同,这表明正则表达式方法(s///m//)的缓慢并不直接取决于编辑 em> 字符串。

【讨论】:

  • 我认为这可能与可能发生的情况非常接近。当我有更多空闲时间时,我想调整更多输入以梳理出一些变量。例如,只需使用不同数量的替换检查 tr/// 自身。
  • @brian 我有兴趣了解您的发现。从某种意义上说,如果我理解正确的话,我的基准测试(tr-Str-Mtr-L)已经测试了你的提议。所有三个基准测试处理的字节数相同,但tr-S 的编辑量是tr-L 的 100 倍;尽管如此,tr-S 处理文件的速度几乎一样快。
【解决方案2】:

我还看到tr/// 随着行长的增加变得相对较慢,尽管效果并不那么显着。这些结果来自 Windows 7 x64 上的 ActivePerl 5.10.1(32 位)。在 100 次时,我还收到“迭代次数太少,无法可靠计数”的警告,因此我将迭代次数提高到 500 次。

        VL: 4501.06    288
        LO: 749.25     29
        SH: 69.38      6
        VA: 104.66     55
            Rate VL-$count     VL-$.     VL-tr      VL-s     VL-wc
VL-$count 2.82/s        --       -0%      -52%      -56%      -99%
VL-$.     2.83/s        0%        --      -51%      -56%      -99%
VL-tr     5.83/s      107%      106%        --      -10%      -99%
VL-s      6.45/s      129%      128%       11%        --      -99%
VL-wc      501/s    17655%    17602%     8490%     7656%        --
            Rate LO-$count     LO-$.      LO-s     LO-tr     LO-wc
LO-$count 16.5/s        --       -1%      -50%      -51%      -97%
LO-$.     16.8/s        1%        --      -50%      -51%      -97%
LO-s      33.2/s      101%       98%        --       -3%      -94%
LO-tr     34.1/s      106%      103%        3%        --      -94%
LO-wc      583/s     3424%     3374%     1655%     1609%        --
            Rate SH-$count     SH-$.      SH-s     SH-tr     SH-wc
SH-$count  120/s        --       -7%      -31%      -67%      -81%
SH-$.      129/s        7%        --      -26%      -65%      -80%
SH-s       174/s       45%       35%        --      -52%      -73%
SH-tr      364/s      202%      182%      109%        --      -43%
SH-wc      642/s      433%      397%      269%       76%        --
            Rate VA-$count     VA-$.      VA-s     VA-tr     VA-wc
VA-$count 92.6/s        --       -5%      -36%      -63%      -79%
VA-$.     97.4/s        5%        --      -33%      -61%      -78%
VA-s       146/s       57%       50%        --      -42%      -67%
VA-tr      252/s      172%      159%       73%        --      -43%
VA-wc      439/s      374%      351%      201%       74%        --

编辑:我做了一个修改后的基准来比较不同线路长度的费率。它清楚地表明,tr/// 一开始对于短线具有很大的优势,随着线的变长,这种优势会迅速消失。至于为什么会出现这种情况,我只能推测tr///是针对短字符串优化的。

Line count rate comparison http://img69.imageshack.us/img69/6250/linecount.th.png

【讨论】:

  • 记住 tr/// 应该在相同大小的字符串上运行,因为它从 sysread 以 4096 字节块的形式获取输入,只是该字符串中的行长度不同。
【解决方案3】:

长线大约比短线大 65 倍,您的数字表明 tr/\n// 运行速度正好慢 65 倍。这符合预期。

wc 显然更适合长线。我真的不知道为什么;可能是因为它被调整为只计算换行符,尤其是当您使用 -l 选项时。

【讨论】:

  • 我希望一切都会慢下来,但事实并非如此。在 tr 情况下,行长度并不重要,因为它读取 4096 字节的块,并且它仍然应该检查整个字符串。解释其他结果。
  • 由于您在写入文件后立即读取文件,因此 I/O 在上述时序中应该几乎无关紧要。您必须检查整个字符串的事实正是性能应该与行长度成反比的原因(假设图表上“速率(#/秒)”中的“#”指的是行而不是字节)。跨度>
  • @Marcelo:不,速率是指子例程每秒运行的次数,因此计算文件换行的次数。但请记住,tr/// 应该几乎总是检查 4096 个字符的块,所以它不应该关心行长。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多