需要认真帮助优化脚本以供内存使用

时间:2017-04-07 00:55:52

标签: perl

[我已经更改了以下代码,以反映我在实施人员建议后正在运行的内容]

让我先说明我不是程序员,而只是使用Perl来尽可能地完成某些文本处理工作的人。

我有一个生成频率列表的脚本。它主要执行以下操作:

  • 从格式为$frequency \t $item的文件中读取行。任何给定的$item可能会多次出现,$frequency的值不同。
  • 根据$item
  • 的内容消除某些行
  • 汇总所有相同$item s的频率,无论其情况如何,并将这些条目合并为一个。
  • 对生成的数组执行反向自然排序。
  • 将结果打印到输出文件。

该脚本可以很好地处理最大约1 GB的输入文件。但是,我需要处理高达6 GB的文件,并且由于内存使用而证明这是不可能的。虽然我的机器有32 GB的RAM,使用zRam,并且在SSD上有64 GB的交换只是为了这个目的,当组合的内存使用大约70 GB(92 GB)时,脚本将不可避免地被Linux OOM服务杀死总)。

当然,真正的问题是我的脚本正在使用的大量内存。我可以尝试添加更多的交换,但是我现在已经增加了两次而且它只是被吃掉了。

所以我需要以某种方式优化脚本。这就是我在这里寻求帮助的地方。

下面是我现在正在运行的脚本的实际版本,并保留了一些有用的注释。

如果您的评论和建议包含足够的代码以实际允许我或多或少地将其放入现有脚本中,我会非常感激,因为我

提前致谢!

(顺便说一下,我在Ubuntu 16.04 LTS x64上使用Perl 5.22.1 x64。

#!/usr/bin/env perl

use strict;
use warnings;
use warnings qw(FATAL utf8);
use Getopt::Long qw(:config no_auto_abbrev);

# DEFINE VARIABLES
my $delimiter            = "\t";
my $split_char           = "\t";

my $input_file_name  = "";
my $output_file_name = "";
my $in_basename      = "";
my $frequency        = 0;
my $item             = "";

# READ COMMAND LINE OPTIONS
GetOptions (
             "input|i=s"         => \$input_file_name,
             "output|o=s"        => \$output_file_name,
           );

# INSURE AN INPUT FILE IS SPECIFIED
if ( $input_file_name eq "" ) {
    die
      "\nERROR: You must provide the name of the file to be processed with the -i switch.\n";
}

# IF NO OUTPUT FILE NAME IS SPECIFIED, GENERATE ONE AUTOMATICALLY
if ( $output_file_name eq "" ) {

    # STRIP EXTENSION FROM INPUT FILE NAME
    $in_basename = $input_file_name;
    $in_basename =~ s/(.+)\.(.+)/$1/;

    # GENERATE OUTPUT FILE NAME FROM INPUT BASENAME
    $output_file_name = "$in_basename.output.txt";
}

# READ INPUT FILE
open( INPUTFILE, '<:encoding(utf8)', $input_file_name )
    or die "\nERROR: Can't open input file ($input_file_name): $!";

# PRINT INPUT AND OUTPUT FILE INFO TO TERMINAL
print STDOUT "\nInput file:\t$input_file_name";
print STDOUT "\nOutput file:\t$output_file_name";
print STDOUT "\n\n";

# PROCESS INPUT FILE LINE BY LINE
my %F;

while (<INPUTFILE>) {

    chomp;

    # PUT FREQUENCY IN $frequency AND THEN PUT ALL OTHER COLUMNS INTO $item
    ( $frequency, $item ) = split( /$split_char/, $_, 2 );

    # Skip lines with empty or undefined content, or spaces only in $item
    next if not defined $frequency or $frequency eq '' or not defined $item or $item =~ /^\s*$/;

    # PROCESS INPUT LINES
    $F{ lc($item) } += $frequency;
}
close INPUTFILE;

# OPEN OUTPUT FILE
open( OUTPUTFILE, '>:encoding(utf8)', "$output_file_name" )
    || die "\nERROR: The output file \($output_file_name\) couldn't be opened for writing!\n";

# PRINT OUT HASH WITHOUT SORTING
foreach my $item ( keys %F ) {
    print OUTPUTFILE $F{$item}, "\t", $item, "\n";
}

close OUTPUTFILE;

exit;

以下是源文件的一些示例输入。它是以制表符分隔的,第一列是$frequency,而其他所有列都是$item

2   útil    volver  a   valdivia
8   útil    volver  la  vista
1   útil    válvula de  escape
1   útil    vía de  escape
2   útil    vía fax y
1   útil    y   a   cabalidad
43  útil    y   a   el
17  útil    y   a   la
1   útil    y   a   los
21  útil    y   a   quien
1   útil    y   a   raíz
2   útil    y   a   uno

2 个答案:

答案 0 :(得分:3)

UPDATE 在我的测试中,哈希需要2.5倍于其“单独”数据所需的内存。但是,我的程序大小始终是变量的3-4倍。对于6.3Gb程序,这会将~ 15Gb数据文件转换为~ 60Gb哈希,就像在评论中报告的那样。

所以说6.3Gb == 60Gb。这仍然足以改善起始情况,从而适用于当前的问题,但显然不是解决方案。请参阅下面的(更新的)另一种方法,了解在不加载整个哈希的情况下运行此处理的方法。

没有什么可以导致数量级的内存爆炸。但是,小错误和低效率可能会增加,所以让我们先清理一下。最后见其他方法。

这是对程序核心的简单重写,先试试。

# ... set filenames, variables 
open my $fh_in, '<:encoding(utf8)', $input_file_name
    or die "\nERROR: Can't open input file ($input_file_name): $!";

my %F;    
while (<$fh_in>) {    
    chomp;
    s/^\s*//;                                              #/trim  leading space
    my ($frequency, $item) = split /$split_char/, $_, 2;

    # Skip lines with empty or undefined content, or spaces only in $item
    next if not defined $frequency or $frequency eq '' 
         or not defined $item      or $item =~ /^\s*$/;

    # ... increment counters and aggregates and add to hash
    # (... any other processing?)
    $F{ lc($item) } += $frequency;
}
close $fh_in;

# Sort and print to file
# (Or better write: "value key-length key" and sort later. See comments)
open my $fh_out, '>:encoding(utf8)', $output_file_name 
    or die "\nERROR: Can't open output file ($output_file_name\: $!";

foreach my $item ( sort { 
        $F{$b} <=> $F{$a} || length($b) <=> length($a) || $a cmp $b 
    } keys %F )
{
    print $fh_out $F{$item}, "\t", $item, "\n";
}
close $fh_out;

一些评论,请告诉我是否需要更多。

  • 始终将$!添加到与错误相关的打印件中,以查看实际错误。请参阅perlvar

  • 使用词汇文件句柄(my $fh而不是IN),它会更好。

  • 如果在三参数open中指定了图层,则open pragma设置的图层将被忽略,因此不需要use open ...(但它不是伤害了。)

此处的sort必须至少复制其输入,并且在多个条件下需要更多内存。

这应该不会超过散列大小的2-3倍。虽然最初我怀疑是内存泄漏(或过多的数据复制),但通过将程序简化为基础,显示“正常”程序大小是(可能)罪魁祸首。这可以通过设计自定义数据结构并经济地打包数据来调整。

当然,如果你的文件越来越大,所有这些都在摆弄,正如他们所做的那样。

另一种方法是写出未排序的文件,然后使用单独的程序排序。这样,您就不会将处理中可能存在的内存膨胀与最终排序相结合。

但是,由于与数据相比大大增加了内存占用,因此即使这也推动了限制,因为哈希占用了数据大小的2.5倍,而整个程序的大小只有3-4倍。

然后找到一种算法将数据逐行写入输出文件。这很简单,因为通过显示的处理我们只需要为每个项目累积频率

open my $fh_out, '>:encoding(utf8)', $output_file_name 
    or die "\nERROR: Can't open output file ($output_file_name\: $!";

my $cumulative_freq;

while (<$fh_in>) {
    chomp;
    s/^\s*//;  #/ leading only
    my ($frequency, $item) = split /$split_char/, $_, 2;

    # Skip lines with empty or undefined content, or spaces only in $item
    next if not defined $frequency or $frequency eq '' 
         or not defined $item      or $item =~ /^\s*$/;

    $cumulative_freq += $frequency;  # would-be hash value

    # Add a sort criterion, $item's length, helpful for later sorting
    say $fh_out $cumulative_freq, "\t", length $item, "\t", lc($item);

    #say $fh_out $cumulative_freq, "\t", lc($item);
}
close $fh_out;

现在我们可以使用系统的sort,它针对非常大的文件进行了优化。由于我们编写了一个包含所有排序列value key-length key的文件,因此在终端中运行

sort -nr -k1,1 -k2,2 output_file_name | cut -f1,3-  > result

命令按数字顺序排序,然后按第二个字段排序(然后按第三个字体排序)并反转顺序。这是通过管道输入cut,从STDIN中提取第一个和第三个字段(使用制表符作为默认分隔符),所需的结果是什么。

系统解决方案是使用数据库,非常方便的是DBD::SQLite

我使用Devel::Size来查看变量使用的内存。

答案 1 :(得分:0)

排序输入需要将所有输入保留在内存中,因此您无法在单个进程中执行所有操作。

但是,可以考虑排序:您可以轻松地将输入排序为可排序的存储桶,然后处理存储桶,并通过组合反向排序存储桶顺序的输出来生成正确的输出。频率计数也可以按桶进行。

所以,只需保留您的程序,但添加一些东西:

  1. 将您的输入分区为存储桶,例如由第一个字符或前两个字符
  2. 在每个存储桶上运行您的程序
  3. 以正确的顺序连接输出
  4. 您的最大内存消耗量将略高于原始程序在最大存储桶上占用的内存消耗量。因此,如果您的分区选择得很好,您可以随意将其分解。

    您可以将输入存储桶和每个存储桶输出存储到磁盘,但您甚至可以直接使用管道连接步骤(为每个存储区处理器创建一个子进程) - 这将创建大量并发进程,因此操作系统将像疯了一样分页,但如果你小心,它就不需要写入磁盘。

    这种分区方式的一个缺点是你的水桶最终可能会变得非常不均匀。另一种方法是使用保证平均分配输入的分区方案(例如,通过将每个 n 输入行放入 n 桶中)但这使得组合输出更复杂。