0

我想出了以下方法来检查换行符的 $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 字节的缓冲读取。- 捷豹路虎

4

5 回答 5

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__

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


您已经展示了可以清理的代码,因此您甚至不需要检查最后一个字符是否有换行符。

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更简单。
  • 如果传递$buffersysread,则使用length($offset).
  • 如您所见,$offset完全没有必要对其进行复制。
  • 将 var 传递给 sub 不会复制它,因此无需传递引用。
  • 如果processLine不需要换行符,请s/(.*)\n//改用。
于 2013-03-01T07:30:33.910 回答
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/ )

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

于 2013-03-01T06:10:46.417 回答
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当然是最快的方法。

于 2013-03-01T08:15:24.807 回答
0

我怀疑 perl 将字符串视为 utf-8 并且出于某种原因必须遍历整个内容。

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

请参阅文档以获取 Perl 的bytes pragmaperlunicode

于 2013-03-01T04:36:31.547 回答
0

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

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

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

于 2013-03-01T04:40:17.053 回答