检查LF是否在Perl中的大标量末尾的最快方法?

时间:2013-03-01 04:23:28

标签: perl

我已经提出以下内容来检查换行器的$ scaler的最终字符:

if( $buffer !~ /\n$/ ) {
if( substr( $buffer, -1, 1 ) !~ /\n/ ) {
if( substr( $buffer, -1, 1 ) ne '\n' ) {

我能用更快的方法吗? $ buffer标量的大小可能会变大,我注意到它越大,这些条件运行的时间就越长。我确实有另一个包含$ buffer长度的标量,如果有帮助的话。

由于

完整代码:

#!/usr/bin/perl
use strict;
use warnings;
use Fcntl qw();
use Time::HiRes qw( gettimeofday tv_interval );

use constant BUFSIZE => 2 ** 21; # 2MB worked best for me, YMMV.

die "ERROR: Missing filename" if( !$ARGV[0] );

my $top = [gettimeofday];
sysopen( my $fh, $ARGV[0], Fcntl::O_RDONLY | Fcntl::O_BINARY ) or
  die "ERROR: Unable to open $ARGV[0], because $!\n";
open my $output, ">", "/dev/null";  # for 'dummy' processing

my $size = -s $ARGV[0];
my $osiz = $size;
my( $buffer, $offset, $lnCtr ) = ( "", "", 0 );
while( $size ) {
    my $read = sysread( $fh, $buffer, BUFSIZE, length($offset) );
    $size -= $read;
    my @lines = split /\n/, $buffer;
    if( substr( $buffer, -1, 1 ) ne "\n" ) {
        $offset = pop( @lines );
    } else {
        $offset = "";
    }
    for my $line ( @lines ) {
        processLine( \$line );
        $lnCtr++;
    }
    $buffer = $offset if( $offset );
}
close $fh;
print "Processed $lnCtr lines ($osiz bytes) in file: $ARGV[0] in ".
      tv_interval( $top ).
      " secs.\n";
print "Using a buffered read of ".BUFSIZE." bytes.  -  JLB\n";

sub processLine {
    if( ref($_[0]) ) {
        print $output ${$_[0]}."\n";
    } else {
        print $output $_[0]."\n";
    }
    return 0;
}

我认为我已经达到了“减少收益点”的努力,试图让这次运行更快。现在似乎能够以我的RAID5 SSD能够读取数据的速度读取数据。正如您所看到的,有一个原因我没有使用chomp(),输入可以包含数十万个换行符,我需要保留它以便能够打破行进行处理。

./fastread.pl newdata.log 处理文件中的516670行(106642635字节):newdata.log,0.674738秒。 使用2097152字节的缓冲读取。 - JLB

5 个答案:

答案 0 :(得分:3)

Perl有两种​​字符串存储格式。

其中一种格式使用相同数量的字节(1)来存储字符串可以包含的每个可能字符。因此,由于Perl会跟踪字符串使用的字节数,因此substr($x, -1)对此格式的字符串的性能不依赖于字符串的长度。

上述格式的问题在于它只能存储非常有限的字符范围。它可用于存储Unicode代码点“Eric”和“Éric”,但不能用于“Ελλάδα”。必要时(甚至在没有必要时),Perl会自动将字符串的存储格式切换为其他格式。

第二种格式可以将任何Unicode代码点存储为字符。实际上,它可以存储任何32位或64位值(取决于perl的构建设置)。缺点是可变数量的字节用于存储每个字符。因此,即使Perl知道整个字符串使用的字节数,它也不会知道第一个字符的起始位置。*要查找最后一个字符,它必须扫描整个字符串。

也就是说,由于存储格式的属性,实际上很容易在常量时间内找到字符串的最后一个字符。

use Inline C => <<'__END_OF_C__';

   # O(1) version of substr($x,-1)
   SV* last_char(SV* sv) {
      STRLEN len;
      const char* s = SvPV(sv, len);

      if (!len)
         return newSVpvn("", 0);

      {
         const U32 utf8 = SvUTF8(sv);
         const char* p = s+len-1;         
         if (utf8) {
            while (p != s && (*p & 0xC0) != 0xC0)
               --p;
         }

         return newSVpvn_utf8(p, s+len-p, utf8);
      }
   }

__END_OF_C__

* - 它确实保留了一对char位置到字节位置映射的缓存。


您已经显示了可以清理的代码,因此您甚至无需检查新行的最后一个字符。

sub processLine {
   print $_[0] $_[1];
}


open(my $fh, '<:raw', $ARGV[0])
   or die("Can't open $ARGV[0]: $!\n");

my $buffer = '';
my $lnCtr = 0;
while (1) {
   my $rv = sysread($fh, $buffer, BUFSIZE, length($buffer));
   die $! if !defined($rv);
   last if !$rv;

   while ($buffer =~ s/(.*\n)//) {
      processLine($1);
      ++$lnCtr;
   }
}

if (length($buffer)) {
   processLine($output, $buffer);
   ++$lnCtr;
}

注意:

  • 无需sysopenopen更简单。
  • 如果您将$buffer传递给sysread,则使用length($offset)没有意义。
  • 如您所见,$offset及其复制完全没必要。
  • 将var传递给sub不会复制它,因此不需要传递引用。
  • 如果processLine不需要换行符,请改用s/(.*)\n//

答案 1 :(得分:1)

你为什么关心速度?这段代码是否在程序的一部分中是非常缓慢的,或许可以用Devel :: NYTProf进行分析?如果没有,那么我建议你选择最清楚的和最惯用的,这可能是

if( $buffer !~ /\n$/ )

您的最终版本:

if( substr( $buffer, -1, 1 ) ne '\n' )

也是一个很好的选择,除了你单引号换行,因此给你一个由反斜杠和小写n组成的双字符串。也许你来自C,其中单个字符是单引号,字符串是双引号?你想要

if( substr( $buffer, -1, 1 ) ne "\n" )

此版本

if( substr( $buffer, -1, 1 ) !~ /\n/ )

正在进行一个不应该的正则表达式匹配,因为它正在检查单字符正则表达式的单字符串。下一个阅读代码的人会认为这很奇怪并且想知道你为什么这么做。另外,回到那个速度的东西,将字符串与正则表达式匹配比仅仅比较单个字符的相等性要慢。

答案 2 :(得分:1)

这是一个基准:

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

my $buffer = 'abc'x10_000_000;
$buffer .= "\n";
my $count = -2;
cmpthese($count, {
    'regex' => sub {
        if ($buffer !~ /\n$/) { }
    },
    'substr + regex' => sub {
        if (substr($buffer, -1, 1) !~ /\n$/) { }
    },
    'substr + ne' => sub {
        if (substr($buffer, -1, 1) ne "\n") { }
    },
    'chomp' => sub {
        if (chomp $buffer) { }
    },
});

<强>输出:

                     Rate substr + regex  substr + ne         regex        chomp
substr + regex  6302468/s             --         -11%          -44%         -70%
substr + ne     7072032/s            12%           --          -37%         -66%
regex          11294695/s            79%          60%            --         -46%
chomp          20910531/s           232%         196%           85%           --

chomp肯定是最快的方式。

答案 3 :(得分:0)

我怀疑perl将字符串视为utf-8,并且由于某种原因必须迭代整个字符串。

您可以暂时切换到字节语义,以查看末尾的char是否为换行符。

请参阅Perl bytes pragmaperlunicode的文档。

答案 4 :(得分:0)

您可以尝试chomp。 Chomp将返回从一行末尾删除的EOL字符数:

if ( chomp $buffer ) {
    print "You had an LF on the end of \$buffer";
}

当然,chomp会删除它计算的NL字符。