如何在Perl中设置文件读取缓冲区大小以优化大文件的读取?

7
我知道Java和Perl都会尝试找到一个通用的默认缓冲区大小来读取文件,但我发现它们的选择越来越过时,并且在更改Perl的默认选择时遇到了问题。就Perl而言,我相信它默认使用8K缓冲区,与Java的选择类似,但我无法通过perldoc网站搜索引擎(实际上是Google)找到有关如何将默认文件输入缓冲区大小增加到64K的参考资料。从上面的链接中可以看出,8K缓冲区不具备可扩展性:如果每行通常有大约60个字符,那么10000行文件中大约有610000个字符。仅通过缓冲区逐行读取文件只需要75个系统调用和75个等待磁盘,而不是10001个。
对于一个有5000万行,每行60个字符(包括结尾的换行符)的文件,使用8K缓冲区,需要进行366211次系统调用才能读取2.8GiB的文件。顺便说一句,在Perl程序读取文本文件花费10分钟时,你可以通过查看任务管理器进程列表中的磁盘I/O读取增量(至少在Windows上),来确认这种行为。

有人在perlmonks上问了关于如何增加Perl输入缓冲区大小的问题,有人回答这里可以通过增加"$/"的大小来增加缓冲区的大小,但是从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而言,增加缓冲区大小可以解决80%的性能问题,同时保持简单明了。

我的问题是:

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

4个回答

7
如果你在支持setvbuf的操作系统上运行,则可以影响缓冲区;请参阅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句柄的更多信息链接会很好。 - Brad Gilbert
唯一与早期版本不同的是,句柄被赋予了 IO::Handle 包。这是 /唯一/ 的区别。特别地,仅仅打开一个文件并不能意味着你可以在句柄上调用任何方法。你必须 "use IO::Handle" 以便方法得到定义。 - Elliot Shank
这在 5.10 中并不是新功能;文件句柄很长一段时间以来已被赋予 IO::Handle 的身份(或者,为了向后兼容,赋予 FileHandle 的身份,如果它被加载)。但正如 Elliot 所说的那样,除非您使用 IO::Handle,否则这些方法是未定义的。 - ysth
perldelta for v5.13.8 表示:“当文件句柄上的方法调用因无法解析该方法而失败,并且未加载 IO::File 时,Perl 现在通过 require 加载 IO::File 并再次尝试方法解析”。IO::FileIO::Handle 的子类,因此两者都是按需加载的(以及 IO::Seekable),它们的方法可以在没有显式 use 语句的情况下使用。具备此功能的第一个公共版本是 2011 年的 Perl v5.14.0。 - Borodin

2
没有(除非重新编译修改过的Perl),但您可以将整个文件读入内存,然后从内存中逐行处理:
use File::Slurp;
my $buffer = read_file("filename");
open my $in_handle, "<", \$buffer;
while ( my $line = readline($in_handle) ) {
}

请注意,Perl 5.10之前的版本在大多数情况下默认使用stdio缓冲区(但通常会作弊并直接访问缓冲区,而不是通过stdio库),但在5.10及更高版本中,默认使用其自己的perlio层系统。后者似乎默认使用4k缓冲区,但编写一个允许配置此缓冲区的层应该很容易(一旦您弄清如何编写层:请参见perldoc perliol)。

1

注意,以下代码仅进行了轻微测试。下面的代码是处理文件逐行处理的函数的第一次尝试(因此函数名)。用户可以定义缓冲区大小。它最多接受四个参数:

  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";

1
我开始尝试使用sysread来回应这个问题,但是在那之后我无法满意地解析。这看起来很有前途,但我想知道它是否仍然会比Perl的内置实现慢(缓冲除外)。 - Telemachus
1
嘿,我从来没有说过它会很快,只是说它会按照指定的缓冲区大小读取文件。话虽如此,我打算对其进行基准测试,并将结果作为文档的一部分。 - Chas. Owens

0

我在进行“死帖回复”,因为这个问题出现在this perlmonks thread上。

在使用PerlIO时,无法使用setvbuf,因为自版本5.8.0以来,它已成为默认设置。但是,在CPAN上有一个名为PerlIO::buffersize的模块,允许您在打开文件时设置缓冲区大小:

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

如果我没记错的话,你也可以在脚本开头使用以下代码设置任何新文件的默认值:

    use open ':buffersize(65536)';

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接