为什么Perl的tr/\n //随着线路长度的增加而变得越来越慢?

bri*_*foy 13 perl benchmarking count

perlfaq5中,有一个答案我如何计算文件中的行数?.目前的答案表明a sysread和a tr/\n//.我想尝试一些其他的东西,看看会有多快tr/\n//,并尝试对不同平均线长的文件.我创建了一个基准测试来尝试各种方法来实现它.我在Mac OS X 10.5.8和MacBook Air上的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%              --
Run Code Online (Sandbox Code Playgroud)

这是基准.我确实有一个控制器,但它是如此之快我只是不打扰它.第一次运行它时,基准测试会创建测试文件并打印一些有关其行长度的统计信息:

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;
    }
Run Code Online (Sandbox Code Playgroud)

FMc*_*FMc 9

我想知道我们使用的基准测试是否有太多可移动的部分:我们正在处理不同大小的数据文件,使用不同的线路长度,并试图衡量tr相对于其竞争对手的速度- 具有底层(但未经测试)假设tr是性能随线长度变化的方法.

此外,正如brian在一些评论中指出的那样,我们正在提供tr总是大小相同的数据缓冲区(4096字节).如果任何方法应该行大小不敏感,它应该是tr.

然后它让我感到震惊:如果tr稳定的参考点和其他方法是否随线大小而变化?当你看到你的宇宙飞船窗口时,你或那个克林贡鸟类正在移动吗?

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

  • tr是对线长变化最不敏感的方法.由于处理的所有三个行长度(短,中,长)处理的总N个字节是恒定的,这意味着tr在编辑它给出的字符串时非常有效.即使短行数据文件需要更多编辑,该tr 方法也能够处理数据文件几乎与处理长行文件一样快.
  • <>随着线路变得越来越长,依赖于加速的方法,尽管速度越来越慢.这是有道理的:因为每次调用都<> 需要一些工作,所以使用较短的行(至少在测试的范围内)处理给定的N个字节应该更慢.
  • s///方法对线路长度也很敏感.就像tr,这种方法通过编辑它给出的字符串来工作.同样,较短的线长意味着更多的编辑.显然,s///进行这种编辑的能力远远低于tr.

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

#   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
Run Code Online (Sandbox Code Playgroud)

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;
}
Run Code Online (Sandbox Code Playgroud)

更新以回应Brad的评论.我尝试了所有三种变体,它们表现得大致相似s/\n//g- 对于行数较短的数据文件较慢(附加限定s/(\n)/$1/甚至比其他更慢).有趣的部分是m/\n/g基本相同的速度s/\n//g,这表明正则表达式的方法的缓慢(包括s///m//)不直接对此事铰链编辑字符串.


Mar*_*tos -1

长行比短行大约大 65 倍,并且您的数字表明 tr/\n// 运行速度正好慢 65 倍。这正如预期的那样。

wc 显然对于长队来说可以更好地扩展。我真的不知道为什么;也许是因为它被调整为只计算换行符,特别是当您使用该-l选项时。