【问题标题】:How can I set the file-read buffer size in Perl to optimize it for large files?如何在 Perl 中设置文件读取缓冲区大小以针对大文件对其进行优化?
【发布时间】:2010-11-18 01:47:04
【问题描述】:

我了解 Java 和 Perl 在读取文件时都非常努力地寻找一个适合所有默认缓冲区大小的大小,但我发现他们的选择越来越过时,并且在更改默认选择时遇到问题它涉及到 Perl。

在 Perl 的情况下,我相信它默认使用 8K 缓冲区,类似于 Java 的选择,我无法使用 perldoc 网站搜索引擎(实际上是 Google)找到有关如何增加默认文件输入的参考缓冲区大小说,64K。

从上面的链接,显示 8K 缓冲区如何不缩放:

如果每行通常有大约 60 个字符,那么 10,000 行文件中大约有 610,000 个字符。使用缓冲逐行读取文件只需要 75 次系统调用和 75 次等待磁盘,而不是 10,001 次。

因此,对于每行 60 个字符(包括末尾的换行符)的 50,000,000 行文件,以及 8K 缓冲区,它将进行 366211 次系统调用来读取 2.8GiB 文件。顺便说一句,您可以通过查看任务管理器进程列表中的磁盘 i/o 读取增量(至少在 Windows 中,*nix 中的顶部以某种方式显示相同的东西)来确认此行为作为您的 Perl 程序读取文本文件需要 10 分钟 :)

有人问关于在 perlmonks 上增加 Perl 输入缓冲区大小的问题,有人回答 here 你可以增加“$/”的大小,从而增加缓冲区大小,但是来自 perldoc:

将 $/ 设置为对整数的引用、包含整数的标量或可转换为整数的标量将尝试读取记录而不是行,最大记录大小为引用的整数。

所以我假设这实际上并没有增加 Perl 在使用典型时用于从磁盘读取的缓冲区大小:

while(<>) {
    #do something with $_ here
    ...
}

“逐行”成语。

现在可能是上述代码的不同“一次读取记录然后将其解析为行”版本通常会更快,并且绕过标准习语的潜在问题并且无法更改默认缓冲区大小(如果这确实不可能),因为您可以将“记录大小”设置为您想要的任何内容,然后将每条记录解析为单独的行,希望 Perl 做正确的事情并结束每条记录执行一次系统调用,但这增加了复杂性,我真正想做的就是通过将上面示例中使用的缓冲区增加到相当大的大小(比如 64K)甚至调整缓冲区大小来轻松获得性能提升使用我的系统上的测试脚本来优化长读取的大小,无需额外的麻烦。

就增加缓冲区大小的直接支持而言,Java 中的情况要好得多。

在 Java 中,我相信 java.io.BufferedReader 使用的当前默认缓冲区大小也是 8192 字节,尽管 JDK 文档中的最新引用是模棱两可的,例如,1.5 文档只说:

可以指定缓冲区大小,也可以接受默认大小。对于大多数用途,默认值足够大。

幸运的是,使用 Java,您不必相信 JDK 开发人员已经为您的应用程序做出了正确的决定,并且可以设置自己的缓冲区大小(本例中为 64K):

import java.io.BufferedReader;
[...]
reader = new BufferedReader(new InputStreamReader(fileInputStream, "UTF-8"), 65536);
[...]
while (true) {
                String line = reader.readLine();
                if (line == null) {
                    break;
                }
                /* do something with the line here */
                foo(line);
}

即使使用巨大的缓冲区和现代硬件,一次解析一行也只能挤出这么多的性能,而且我确信有办法通过读取文件来获得每一盎司的性能通过读取大的多行记录并将每个记录分解为标记,然后在每个记录中使用这些标记处理一次,但它们增加了复杂性和边缘情况(尽管如果在纯 Java 中有一个优雅的解决方案(仅使用 JDK 1.5 中存在的功能)很高兴知道)。增加 Perl 中的缓冲区大小至少可以解决 Perl 80% 的性能问题,同时让事情保持直截了当。

我的问题是:

有没有办法在 Perl 中为上述典型的“逐行”习语调整缓冲区大小,类似于 Java 示例中缓冲区大小的增加方式?

【问题讨论】:

    标签: java perl file-io performance


    【解决方案1】:

    不,没有(没有重新编译修改过的 perl),但是您可以将整个文件读入内存,然后从中逐行工作:

    use File::Slurp;
    my $buffer = read_file("filename");
    open my $in_handle, "<", \$buffer;
    while ( my $line = readline($in_handle) ) {
    }
    

    请注意,5.10 之前的 perl 在大多数地方默认使用 stdio 缓冲区(但经常作弊并直接访问缓冲区,而不是通过 stdio 库),但在 5.10 及更高版本默认使用自己的 perlio 层系统。后者好像用的是4k 默认情况下缓冲区,但编写一个允许配置它的层应该是微不足道的 (一旦你弄清楚如何编写层:请参阅perldoc perliol)。

    【讨论】:

      【解决方案2】:

      警告,以下代码仅经过轻度测试。下面的代码是一个函数的第一次尝试,它可以让您使用用户可定义的缓冲区大小逐行处理文件(因此是函数名称)。它最多需要四个参数:

      1. 打开的文件句柄(默认为STDIN
      2. 缓冲区大小(默认为 4k)
      3. 对存储行的变量的引用(默认为$_
      4. 调用文件的匿名子例程(默认打印行)。

      参数是位置参数,除了最后一个参数可能总是匿名子例程。线条是自动切分的。

      可能的错误:

      • 可能不适用于换行符为行尾字符的系统
      • 与词法 $_(在 Perl 5.10 中引入)组合时可能会失败

      您可以从strace 看到它读取具有指定缓冲区大小的文件。如果我喜欢测试的方式,你很快就会在CPAN 上看到这个。

      #!/usr/bin/perl
      
      use strict;
      use warnings;
      use Scalar::Util qw/reftype/;
      use Carp;
      
      sub line_by_line {
          local $_;
          my @args = \(
              my $fh      = \*STDIN,
              my $bufsize = 4*1024,
              my $ref     = \$_,
              my $coderef = sub { print "$_\n" },
          );
          croak "bad number of arguments" if @_ > @args;
      
          for my $arg_val (@_) {
              if (reftype $arg_val eq "CODE") {
                  ${$args[-1]} = $arg_val;
                  last;
              }
              my $arg = shift @args;
              $$arg = $arg_val;
          }
      
          my $buf;
          my $overflow ='';
          OUTER:
          while(sysread $fh, $buf, $bufsize) {
              my @lines = split /(\n)/, $buf;
              while (@lines) {
                  my $line  = $overflow . shift @lines;
                  unless (defined $lines[0]) {
                      $overflow = $line;
                      next OUTER;
                  }
                  $overflow = shift @lines;
                  if ($overflow eq "\n") {
                      $overflow = "";
                  } else {
                      next OUTER;
                  }
                  $$ref = $line;
                  $coderef->();
              }
          }
          if (length $overflow) {
              $$ref = $overflow;
              $coderef->();
          }
      }
      
      my $bufsize = shift;
      
      open my $fh, "<", $0
          or die "could not open $0: $!";
      
      my $count;
      line_by_line $fh, sub {
          $count++ if /lines/;
      }, $bufsize;
      
      print "$count\n";
      

      【讨论】:

      • 我开始使用sysread 来回答这个问题,但之后我对如何解析 lines 感到不高兴。这看起来很有希望,但我想知道它是否仍然不会比 Perl 的内置实现慢(尽管有缓冲)。
      • 嘿,我从来没有声称它会快速,只是它会读取具有指定缓冲区大小的文件。也就是说,我将把它与常见的成语进行基准测试,结果将成为文档的一部分。
      【解决方案3】:

      如果您在支持setvbuf 的操作系统上运行,您可能会影响缓冲;见documentation for IO::Handle

      如果您使用 perl v5.10 或更高版本,则无需 如文档中所述显式创建IO::Handle 对象,因为自该版本以来,所有文件句柄都隐含地祝福到IO::Handle 对象中。

      use 5.010;
      use strict;
      use warnings;
      
      use autodie;
      
      use IO::Handle '_IOLBF';
      
      open my $handle, '<:utf8', 'foo';
      
      my $buffer;
      $handle->setvbuf($buffer, _IOLBF, 0x10000);
      
      while ( my $line = <$handle> ) {
          ...
      }
      

      【讨论】:

      • 最好发布一个链接,以获取有关 Perl 5.10 句柄的更多信息。
      • 唯一与早期版本不同的是句柄被祝福到 IO::Handle 包中。这是 /only/ 的区别。特别是,仅仅打开一个文件并不意味着您可以调用句柄上的任何方法。您必须“使用 IO::Handle”才能定义方法。
      • 这在 5.10 中并不新鲜;文件句柄已经被 IO::Handle 祝福了很长一段时间(或者,为了向后兼容,如果加载了 FileHandle,则进入 FileHandle)。但正如 Elliot 所说,除非您使用 IO::Handle,否则不会定义方法。
      • v5.13.8 的 perldelta 表示 “当文件句柄上的方法调用将因无法解析该方法而终止,并且尚未加载 IO::File 时,Perl现在通过require 加载IO::File 并再次尝试方法解析”IO::FileIO::Handle 的子类,因此两者都是按需加载的(以及IO::Seekable),并且它们的方法可以在没有明确的use 语句的情况下使用。 2011 年,第一个使用此工具的公开版本是 perl v5.14.0。
      【解决方案4】:

      自从this perlmonks thread 出现此问题后,我正在发布 necroposting

      不能在使用 PerlIO 的 perls 上使用 setvbuf,这是自 5.8.0 版以来的默认设置。但是,CPAN 上有 PerlIO::buffersize 模块,允许您在打开文件时设置缓冲区大小:

          open my $fh, '<:buffersize(65536)', $filename;
      

      IIRC,您还可以通过在脚本开头使用它来为任何新文件设置默认值:

          use open ':buffersize(65536)';
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2014-06-07
        • 2010-12-05
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-12-21
        相关资源
        最近更新 更多