【问题标题】:very huge assosiative array in perlperl中非常大的关联数组
【发布时间】:2017-03-02 17:37:01
【问题描述】:

我需要将两个文件合并成一个新文件。

两者有超过 3 亿条管道分隔的记录,第一列作为主键。行未排序。第二个文件可能有第一个文件没有的记录。

示例文件 1:

1001234|X15X1211,J,S,12,15,100.05

示例文件 2:

1231112|AJ32,,,18,JP     
1001234|AJ15,,,16,PP

输出:

1001234,X15X1211,J,S,12,15,100.05,AJ15,,,16,PP

我正在使用以下代码:

tie %hash_REP, 'Tie::File::AsHash', 'rep.in', split => '\|'
my $counter=0;
while (($key,$val) = each %hash_REP) {
    if($counter==0) {
        print strftime "%a %b %e %H:%M:%S %Y", localtime;
    }
}

准备关联数组需要将近 1 个小时。 它真的很好还是真的很糟糕? 有没有更快的方法来处理关联数组中如此大小的记录? 任何脚本语言的任何建议都会有帮助。

谢谢, 尼丁T。

我也尝试了以下程序,也花了 1+ 小时如下:

#!/usr/bin/perl
use POSIX qw(strftime);
my $now_string = strftime "%a %b %e %H:%M:%S %Y", localtime;
print $now_string . "\n";

my %hash;
open FILE, "APP.in" or die $!;
while (my $line = <FILE>) {
     chomp($line);
      my($key, $val) = split /\|/, $line;
      $hash{$key} = $val;
 }
 close FILE;

my $filename = 'report.txt';
open(my $fh, '>', $filename) or die "Could not open file '$filename' $!";
open FILE, "rep.in" or die $!;
while (my $line = <FILE>) {
      chomp($line);
  my @words = split /\|/, $line;
  for (my $i=0; $i <= $#words; $i++) {
    if($i == 0)
    {
       next;
    }
    print $fh  $words[$i] . "|^"
  }
  print $fh  $hash{$words[0]} . "\n";
 }
 close FILE;
 close $fh;
 print "done\n";

my $now_string = strftime "%a %b %e %H:%M:%S %Y", localtime;
print $now_string . "\n";

【问题讨论】:

  • 这篇评论反映了你糟糕的表现结果:cpanratings.perl.org/dist/Tie-File-AsHash
  • 你坚持绑吗?有什么特别的原因吗?
  • 您使用each 迭代哈希的事实向我表明,您实际上并不需要首先使用磁盘数据结构。您实际上想在这里解决什么问题?
  • @Nitin Tripathi,任何提供基于 tie 的界面的东西都会比不提供的要慢。
  • 我会将这两个文件导入一个 sqlite 并让它处理它。让它在磁盘上,让它花一些时间,但不要试图将所有东西都加载到内存中。

标签: perl


【解决方案1】:

由于几个原因,您的技术效率极低。

  • 绑定速度极慢。
  • 您正在将所有内容都拉入内存。

第一个问题可以通过自己阅读和拆分来缓解,但后者总是会成为问题。经验法则是避免将大块数据拉入内存。它会占用所有内存,并可能导致它交换到磁盘并减慢速度,尤其是在您使用旋转磁盘时。

相反,您可以将各种“磁盘哈希”与GDBM_FileBerkleyDB 等模块一起使用。

但实际上没有理由与他们混在一起,因为我们有 SQLite,它可以更快更好地完成他们所做的一切。


在 SQLite 中创建一个表。

create table imported (
    id integer,
    value text
);

使用 sqlite shell 的 .import 导入您的文件,使用 .mode.separator 调整您的格式。

sqlite>     create table imported (
   ...>         id integer,
   ...>         value text
   ...>     );
sqlite> .mode list
sqlite> .separator |
sqlite> .import test.data imported
sqlite> .mode column
sqlite> select * from imported;
12345       NITIN     
12346       NITINfoo  
2398        bar       
9823        baz     

现在,您以及其他任何需要处理数据的人都可以使用高效、灵活的 SQL 来做任何您喜欢的事情。即使导入需要一段时间,您也可以在导入的同时去做其他事情。

【讨论】:

  • Re "你把所有的东西都拉到内存里了。",不,Tie::File::AsHash 的全部意义在于它没有 将所有内容拉入内存。它是 Tie::File 的薄包装器。
  • Re "Tying 非常慢",这也不是问题。问题是从 Tie::File::AsHash 哈希中获取对 Tie::File 数组进行线性扫描,这意味着每次查找哈希元素时都会读取一半文件(平均)!!!
  • 对我来说,这花费了使用 sort 加上与 Perl 合并的时间的两倍。看我的回答。
  • @ikegami 从文件中读取它们后,他们将所有键和值存储在内存中。是的,我确信还有其他性能问题。无论如何,挂钟性能并不重要。这种技术只需要很少的编码或调试,因此您可以设置它并离开,并且只需执行一次。完成后,您将获得一种非常有用且有效的格式,以供进一步操作。 I have an answer that lays out the CSV vs SQLite trade offs.
  • Re "在他们从文件中读取它们之后,他们将所有的键和值都存储在内存中",不。再次,使用 Tie 的重点::File::AsHash 是它不这样做。
【解决方案2】:

我会使用sort 非常快速地对数据进行排序(10,000,000 行需要 5 秒),然后合并排序后的文件。

perl -e'
   sub get {
      my $fh = shift;
      my $line = <$fh>;
      return () if !defined($line);

      chomp($line);
      return split(/\|/, $line);
   }

   sub main {
      @ARGV == 2
         or die("usage\n");

      open(my $fh1, "-|", "sort", "-n", "-t", "|", $ARGV[0]);
      open(my $fh2, "-|", "sort", "-n", "-t", "|", $ARGV[1]);

      my ($key1, $val1) = get($fh1)  or return;
      my ($key2, $val2) = get($fh2)  or return;

      while (1) {
         if    ($key1 < $key2) { ($key1, $val1) = get($fh1)  or return; }
         elsif ($key1 > $key2) { ($key2, $val2) = get($fh2)  or return; }
         else {
            print("$key1,$val1,$val2\n");
            ($key1, $val1) = get($fh1)  or return;
            ($key2, $val2) = get($fh2)  or return;
         }
      }
   }

   main();
' file1 file2 >file

对于每个文件中的 10,000,000 条记录,这在速度较慢的机器上需要 37 秒。

$ perl -e'printf "%d|%s\n", 10_000_000-$_, "X15X1211,J,S,12,15,100.05" for 1..10_000_000' >file1

$ perl -e'printf "%d|%s\n", 10_000_000-$_, "AJ15,,,16,PP" for 1..10_000_000' >file2

$ time perl -e'...' file1 file2 >file
real    0m37.030s
user    0m38.261s
sys     0m1.750s

或者,可以将数据转储到数据库中并让它处理细节。

sqlite3 <<'EOI'
CREATE TABLE file1 ( id INTEGER, value TEXT );
CREATE TABLE file2 ( id INTEGER, value TEXT );
.mode list
.separator |
.import file1 file1
.import file2 file2
.output file
SELECT file1.id || "," || file1.value || "," || file2.value
  FROM file1
  JOIN file2
    ON file2.id = file1.id;
.exit
EOI

但您需要为灵活性付出代价。这花费了两倍的时间。

real    1m14.065s
user    1m11.009s
sys     0m2.550s

注意:我最初在 .import 命令之后有 CREATE INDEX file2_id ON file2 ( id );,但删除它对性能有很大帮助..

【讨论】:

  • 对于10M 记录,即使需要1.30 min 来处理它也非常有效。但是当我进一步查找300M 记录时,它崩溃了,因为我机器上的临时空间是5GB,而实际文件是10GB+,因为排序发生在临时空间中,它崩溃了。删除排序部分并仅保留合并部分后,提供的文件已经排序。需要42 Min 进行合并,请记住在32GB 之外只有2GB 可用,并且它与50% CPU Usage 一起运行。杀死所有进程后,用了 24 分钟,93% CPU Usage。看来,对于 93% 的 CPU 使用率,它仍然更慢!
  • 即使我尝试将文件拆分为 30M 每个,也需要 2 Minutes 每个。但是当我并行运行所有10 process 时,12 Minutes 一起累积,但实际上每个进程都单独使用2 Min
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-03-28
  • 1970-01-01
  • 2011-06-30
  • 1970-01-01
  • 1970-01-01
  • 2014-01-31
  • 1970-01-01
相关资源
最近更新 更多