Perl中大哈希表的快速加载

6

我有大约30个文本文件,结构如下:

wordleft1|wordright1
wordleft2|wordright2
wordleft3|wordright3
...

文件的总大小约为1 GB,包含大约3200万行单词组合。我尝试了几种方法以最快的速度加载它们,并将组合存储在哈希表中。
$hash{$wordleft} = $wordright

逐个打开文件并逐行读取大约需要42秒。然后我使用Storable模块存储哈希。

store \%hash, $filename

重新加载数据。
$hashref = retrieve $filename

将时间缩短至约28秒。我使用快速的SSD驱动器和快速的CPU,并拥有足够的RAM来存储所有数据(大约需要7 GB)。

我正在寻找一种更快的方法将这些数据加载到RAM中(由于几个原因,我不能将其保留在那里)。


4
尝试使用Sereal。另外可以考虑将数据存储在Berkeley DB或sqlite中。 - ysth
2
非常频繁是多频繁?或许更好的表述方式是:每次读取数据时,您将查找数据的次数有多少?(我猜这个数字必须达到数千万或上亿次,才能使将所有数据保存在内存中变得值得尝试) - ysth
1
将其放入SQLite数据库中,或者使用任何比文本文件中分隔值千兆更好的格式。 - Schwern
4
有趣的事实:我甚至使用不同的编程语言来实现读取和哈希存储,特别是使用C++(使用stdlib中的哈希类),Java,C#和Go。我的原始Perl实现仍然最快。 - André
尝试使用 SharedHashFile [1] 编写 C 代码,允许将键值对内存映射到 RAM 中。当您第一次访问包含键值对的虚拟页面时,会有一小部分内核开销。使用进程 a 来编写键值对,使用进程 b 来读取,因此共享内存几乎没有额外开销。为了提高速度,将二进制文件保存在 RAM 磁盘上,例如 /dev/shm。此外,使用多个进程并发地读写键值对;测试结果表明,在单台计算机上每秒可以进行超过 1000 万次读取。 [1] https://github.com/simonhf/sharedhashfile - simonhf
显示剩余10条评论
2个回答

1
你可以尝试使用Dan Bernstein的CDB文件格式,并使用绑定哈希,这将需要最小的代码更改。您可能需要安装CDB_File。在我的笔记本电脑上,cdb文件可以很快打开,我可以每秒进行约200-250k次查找。这是一个创建/使用/基准测试cdb的示例脚本:

test_cdb.pl

#!/usr/bin/env perl

use warnings;
use strict;

use Benchmark qw(:all) ;
use CDB_File 'create';
use Time::HiRes qw( gettimeofday tv_interval );

scalar @ARGV or die "usage: $0 number_of_keys seconds_to_benchmark\n";
my ($size)    = $ARGV[0] || 1000;
my ($seconds) = $ARGV[1] || 10;

my $t0;
tic();

# Create CDB
my ($file, %data);

%data = map { $_ => 'something' } (1..$size);
print "Created $size element hash in memory\n";
toc();

$file = 'data.cdb';
create %data, $file, "$file.$$";
my $bytes = -s $file;
print "Created data.cdb [ $size keys and values, $bytes bytes]\n";
toc();

# Read from CDB
my $c = tie my %h, 'CDB_File', 'data.cdb' or die "tie failed: $!\n";
print "Opened data.cdb as a tied hash.\n";
toc();

timethese( -1 * $seconds, {
          'Pick Random Key'    => sub { int rand $size },
          'Fetch Random Value' => sub { $h{ int rand $size }; },
});

tic();
print "Fetching Every Value\n";
for (0..$size) {
    no warnings; # Useless use of hash element
    $h{ $_ };
}
toc();

sub tic {
    $t0 = [gettimeofday];    
}

sub toc {
    my $t1 = [gettimeofday];
    my $elapsed = tv_interval ( $t0, $t1);
    $t0 = $t1;
    print "==> took $elapsed seconds\n";
}

输出(100万个键,在10秒内测试)

./test_cdb.pl 1000000 10

Created 1000000 element hash in memory
==> took 2.882813 seconds
Created data.cdb [ 1000000 keys and values, 38890944 bytes]
==> took 2.333624 seconds
Opened data.cdb as a tied hash.
==> took 0.00015 seconds
Benchmark: running Fetch Random Value, Pick Random Key for at least 10 CPU seconds...
Fetch Random Value: 10 wallclock secs (10.46 usr +  0.01 sys = 10.47 CPU) @ 236984.72/s (n=2481230)
Pick Random Key:  9 wallclock secs (10.11 usr +  0.02 sys = 10.13 CPU) @ 3117208.98/s (n=31577327)
Fetching Every Value
==> took 3.514183 seconds

输出结果(1000万个键,在10秒内测试)

./test_cdb.pl 10000000 10

Created 10000000 element hash in memory
==> took 44.72331 seconds
Created data.cdb [ 10000000 keys and values, 398890945 bytes] 
==> took 25.729652 seconds
Opened data.cdb as a tied hash.
==> took 0.000222 seconds
Benchmark: running Fetch Random Value, Pick Random Key for at least 10 CPU seconds...
Fetch Random Value: 14 wallclock secs ( 9.65 usr +  0.35 sys = 10.00 CPU) @ 209811.20/s (n=2098112)
Pick Random Key: 12 wallclock secs (10.40 usr +  0.02 sys = 10.42 CPU) @ 2865335.22/s (n=29856793)
Fetching Every Value
==> took 38.274356 seconds

1
我尝试过这个。创建数据库的速度足够快,打开数据库确实非常快。不幸的是,查找比使用内存哈希慢了约6倍,因此我的任务的总运行时间比读取所有数据到哈希中要长。但我会记住这个模块,因为它可能对其他任务有用。非常感谢。 - André
你可以尝试复制cdb哈希my %copy = %tied_hash;来加快查找速度(在我的笔记本上快3倍),但是复制可能会花费太长时间。 - xxfelixxx

0

听起来你确实有一个使用内存perl哈希的好用例。

为了更快地存储/检索,我建议使用Sereal(Sereal :: Encoder / Sereal :: Decoder)。如果您的磁盘存储速度较慢,您甚至可能需要启用Snappy压缩。


我尝试了Sereal。读取数据需要25秒,而Storable模块需要28秒。Sereal稍微快一点,但在编码时需要大量内存,因为与Storable不同,它不能直接写入文件(至少我没有找到方法)。 - André

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