我试图对这里介绍的一些方法进行比较。
首先,我创建了一个 Perl 脚本来生成输入文件 file1.txt 和 file2.txt。为了比较一些解决方案,我确保来自file1.txt 的单词只能出现在file2.txt 的第二个字段中。为了能够使用@GeorgeVasiliou 提出的join 解决方案,我对file1.txt 和file2.txt 进行了排序。目前,我仅根据 75 个随机单词(取自 https://www.randomlists.com/random-words)生成输入文件。 file1.txt 中仅使用了这 75 个单词中的 5 个,其余 70 个单词用于填写 file2.txt 中的字段。可能有必要大幅增加字数以获得实际结果(根据 OP,原始 file1.txt 包含 14000 个字)。在下面的测试中,我使用了 file2.txt 和 1000000(100 万)行。该脚本还会生成@BOC的grep解决方案所需的文件regexp1.txt。
gen_input_files.pl:
#! /usr/bin/env perl
use feature qw(say);
use strict;
use warnings;
use Data::Printer;
use Getopt::Long;
GetOptions ("num_lines=i" => \my $nlines )
or die("Error in command line arguments\n");
# Generated random words from site: https://www.randomlists.com/random-words
my $word_filename = 'words.txt'; # 75 random words
my $num_match_words = 5;
my $num_file2_lines = $nlines || 1_000_000;
my $file2_words_per_line = 3;
my $file2_match_field_no = 2;
my $file1_filename = 'file1.txt';
my $file2_filename = 'file2.txt';
my $file1_regex_fn = 'regexp1.txt';
say "generating $num_file2_lines lines..";
my ( $words1, $words2 ) = get_words( $word_filename, $num_match_words );
write_file1( $file1_filename, $words2 );
write_file2(
$file2_filename, $words1, $words2, $num_file2_lines,
$file2_words_per_line, $file2_match_field_no
);
write_BOC_regexp_file( $file1_regex_fn, $words2 );
sub write_BOC_regexp_file {
my ( $fn, $words ) = @_;
open( my $fh, '>', $fn ) or die "Could not open file '$fn': $!";
print $fh '\\|' . (join "|", @$words) . '\\|';
close $fh;
}
sub write_file2 {
my ( $fn, $words1, $words2, $nlines, $words_per_line, $field_no ) = @_;
my $nwords1 = scalar @$words1;
my $nwords2 = scalar @$words2;
my @lines;
for (1..$nlines) {
my @words_line;
my $key;
for (1..$words_per_line) {
my $word;
if ( $_ != $field_no ) {
my $index = int (rand $nwords1);
$word = @{ $words1 }[$index];
}
else {
my $index = int (rand($nwords1 + $nwords2) );
if ( $index < $nwords2 ) {
$word = @{ $words2 }[$index];
}
else {
$word = @{ $words1 }[$index - $nwords2];
}
$key = $word;
}
push @words_line, $word;
}
push @lines, [$key, (join "|", @words_line)];
}
@lines = map { $_->[1] } sort { $a->[0] cmp $b->[0] } @lines;
open( my $fh, '>', $fn ) or die "Could not open file '$fn': $!";
print $fh (join "\n", @lines);
close $fh;
}
sub write_file1 {
my ( $fn, $words ) = @_;
open( my $fh, '>', $fn ) or die "Could not open file '$fn': $!";
print $fh (join "\n", sort @$words);
close $fh;
}
sub get_words {
my ( $fn, $N ) = @_;
open( my $fh, '<', $fn ) or die "Could not open file '$fn': $!";
my @words = map {chomp $_; $_} <$fh>;
close $fh;
my @words1 = @words[$N..$#words];
my @words2 = @words[0..($N - 1)];
return ( \@words1, \@words2 );
}
接下来,我创建了一个包含所有测试用例的子文件夹solutions:
$ tree solutions/
solutions/
├── BOC1
│ ├── out.txt
│ └── run.sh
├── BOC2
│ ├── out.txt
│ └── run.sh
├── codeforester
│ ├── out.txt
│ ├── run.pl
│ └── run.sh
[...]
这里的文件 out.txt 是每个解决方案的 greps 的输出。脚本run.sh 运行给定测试用例的解决方案。
关于不同解决方案的说明
-
BOC1:@BOC 提出的第一个解决方案
grep -E -f regexp1.txt file2.txt
-
BOC2:@BOC 建议的第二种解决方案:
LC_ALL=C grep -E -f regexp1.txt file2.txt
codeforester:@codeforester 接受的 Perl 解决方案(参见 source)
-
codeforester_orig:@codeforested 提出的原始解决方案:
fgrep -f file1.txt file2.txt
dawg:@dawg 提出的使用字典和分割线的 Python 解决方案(参见source)
-
gregory1:@gregory 建议的使用 Gnu Parallel 的解决方案
parallel -k --pipepart -a file2.txt --block "$block_size" fgrep -F -f file1.txt
请参阅下面有关如何选择$block_size 的说明。
hakon1:@HåkonHægland 提供的 Perl 解决方案(请参阅 source)。此解决方案需要在第一次运行代码时编译 c 扩展。当file1.txt 或file2.txt 发生变化时,它不需要重新编译。注意:在初始运行时用于编译 c-extension 的时间不包括在下面显示的运行时间中。
-
ikegami :使用汇编正则表达式和使用@ikegami 给出的grep -P 的解决方案。注意:组装的正则表达式被写入单独的文件regexp_ikegami.txt,因此生成正则表达式的运行时间不包括在下面的比较中。这是使用的代码:
regexp=$(< "regexp_ikegami.txt")
grep -P "$regexp" file2.txt
-
inian1:@Inian 使用match()的第一个解决方案
awk 'FNR==NR{
hash[$1]; next
}
{
for (i in hash) if (match($0,i)) {print; break}
}' file1.txt FS='|' file2.txt
-
inian2 :@Inian 使用 index() 的第二个解决方案
awk 'FNR==NR{
hash[$1]; next
}
{
for (i in hash) if (index($0,i)) {print; break}
}' file1.txt FS='|' file2.txt
-
inian3 :@Inian 仅检查 $2 字段的第三个解决方案:
awk 'FNR==NR{
hash[$1]; next
}
$2 in hash' file1.txt FS='|' file2.txt
-
inian4:@Inian 的第 4 次灵魂(与 codeforester_orig 和 LC_ALL 基本相同):
LC_ALL=C fgrep -f file1.txt file2.txt
-
inian5:@Inian 的第 5 个解决方案(与 inian1 相同,但使用 LC_ALL):
LC_ALL=C awk 'FNR==NR{
hash[$1]; next
}
{
for (i in hash) if (match($0,i)) {print; break}
}' file1.txt FS='|' file2.txt
inian6 :与inian3 相同,但使用LC_ALL=C。感谢@GeorgeVasiliou 的建议。
jjoao :由@JJoao 提议的编译的 flex 生成的 C 代码(参见 source )。注意:每次file1.txt 更改时都必须重新编译可执行文件。用于编译可执行文件的时间不包括在下面显示的运行时间中。
oliv:@oliv 提供的 Python 脚本(见 source)
-
Vasiliou :按照@GeorgeVasiliou 的建议使用join:
join --nocheck-order -11 -22 -t'|' -o 2.1 2.2 2.3 file1.txt file2.txt
Vasiliou2 :与Vasiliou 相同,但使用LC_ALL=C。
zdim :使用@zdim 提供的Perl 脚本(参见source)。注意:这里使用正则表达式搜索版本(而不是分割线解决方案)。
zdim2 :与zdim 相同,只是它使用split 函数而不是正则表达式搜索file2.txt 中的字段。
注意事项
我对 Gnu 并行进行了一些试验(请参阅上面的 gregory1 解决方案)以确定我的 CPU 的最佳块大小。我有 4 个内核,目前看来最佳选择是将文件 (file2.txt) 分成 4 个大小相等的块,并在 4 个处理器中的每一个上运行一个作业。这里可能需要更多测试。因此,对于file2.txt 为20M 的第一个测试用例,我将$block_size 设置为5M(参见上面的gregory1 解决方案),而对于下面介绍的更现实的情况,file2.txt 为268M,$block_size 为67M被使用了。
-
解决方案BOC1、BOC2、codeforester_orig、inian1、inian4、inian5和gregory1都使用了松散匹配。这意味着来自file1.txt 的单词不必在file2.txt 的字段#2 中完全匹配。线上任何地方的匹配都被接受。由于这种行为使得将它们与其他方法进行比较变得更加困难,因此还引入了一些修改的方法。名为BOC1B 和BOC2B 的前两种方法使用了修改后的regexp1.txt 文件。原始regexp1.txt 中的行在\|foo1|foo2|...|fooN\| 形式上,它将匹配任何字段边界处的单词。修改后的文件regexp1b.txt 使用^[^|]*\|foo1|foo2|...|fooN\| 的形式将匹配锚定到字段#2。
然后其余的修改方法codeforester_origB、inian1B、inian4B、inian5B和gregory1B使用了修改后的file1.txt。修改后的文件 file1b.txt 不是每行一个 literal 单词,而是在表单上的每行使用一个 regex:
^[^|]*\|word1\|
^[^|]*\|word2\|
^[^|]*\|word3\|
[...]
此外,对于这些方法,fgrep -f 已替换为 grep -E -f。
运行测试
这是用于运行所有测试的脚本。它使用 Bash time 命令记录每个脚本花费的时间。请注意,time 命令返回三个不同的时间调用real、user 和sys。首先我使用了user + sys,但是在使用Gnu并行命令时意识到这是不正确的,所以下面报告的时间现在是time返回的real部分。有关time 返回的不同时间的更多信息,请参阅this question。
第一个测试使用包含 5 行的 file1.txt 和包含 1000000 行的 file2.txt 运行。这是run_all.pl 脚本的前52 行,脚本的其余部分可用here。
run_all.pl
#! /usr/bin/env perl
use feature qw(say);
use strict;
use warnings;
use Cwd;
use Getopt::Long;
use Data::Printer;
use FGB::Common;
use List::Util qw(max shuffle);
use Number::Bytes::Human qw(format_bytes);
use Sys::Info;
GetOptions (
"verbose" => \my $verbose,
"check" => \my $check,
"single-case=s" => \my $case,
"expected=i" => \my $expected_no_lines,
) or die("Error in command line arguments\n");
my $test_dir = 'solutions';
my $output_file = 'out.txt';
my $wc_expected = $expected_no_lines; # expected number of output lines
my $tests = get_test_names( $test_dir, $case );
my $file2_size = get_file2_size();
my $num_cpus = Sys::Info->new()->device( CPU => () )->count;
chdir $test_dir;
my $cmd = 'run.sh';
my @times;
for my $case (@$tests) {
my $savedir = getcwd();
chdir $case;
say "Running '$case'..";
my $arg = get_cmd_args( $case, $file2_size, $num_cpus );
my $output = `bash -c "{ time -p $cmd $arg; } 2>&1"`;
my ($user, $sys, $real ) = get_run_times( $output );
print_timings( $user, $sys, $real ) if $verbose;
check_output_is_ok( $output_file, $wc_expected, $verbose, $check );
print "\n" if $verbose;
push @times, $real;
#push @times, $user + $sys; # this is wrong when using Gnu parallel
chdir $savedir;
}
say "Done.\n";
print_summary( $tests, \@times );
结果
这是运行测试的输出:
$ run_all.pl --verbose
Running 'inian3'..
..finished in 0.45 seconds ( user: 0.44, sys: 0.00 )
..no of output lines: 66711
Running 'inian2'..
..finished in 0.73 seconds ( user: 0.73, sys: 0.00 )
..no of output lines: 66711
Running 'Vasiliou'..
..finished in 0.09 seconds ( user: 0.08, sys: 0.00 )
..no of output lines: 66711
Running 'codeforester_orig'..
..finished in 0.05 seconds ( user: 0.05, sys: 0.00 )
..no of output lines: 66711
Running 'codeforester'..
..finished in 0.45 seconds ( user: 0.44, sys: 0.01 )
..no of output lines: 66711
[...]
总结
[@Vasiliou 获得的结果显示在中间一栏。]
|Vasiliou
My Benchmark |Results | Details
-------------------------------|---------|----------------------
inian4 : 0.04s |0.22s | LC_ALL fgrep -f [loose]
codeforester_orig : 0.05s | | fgrep -f [loose]
Vasiliou2 : 0.06s |0.16s | [LC_ALL join [requires sorted files]]
BOC1 : 0.06s | | grep -E [loose]
BOC2 : 0.07s |15s | LC_ALL grep -E [loose]
BOC2B : 0.07s | | LC_ALL grep -E [strict]
inian4B : 0.08s | | LC_ALL grep -E -f [strict]
Vasiliou : 0.08s |0.23s | [join [requires sorted files]]
gregory1B : 0.08s | | [parallel + grep -E -f [strict]]
ikegami : 0.1s | | grep -P
gregory1 : 0.11s |0.5s | [parallel + fgrep -f [loose]]
hakon1 : 0.14s | | [perl + c]
BOC1B : 0.14s | | grep -E [strict]
jjoao : 0.21s | | [compiled flex generated c code]
inian6 : 0.26s |0.7s | [LC_ALL awk + split+dict]
codeforester_origB : 0.28s | | grep -E -f [strict]
dawg : 0.35s | | [python + split+dict]
inian3 : 0.44s |1.1s | [awk + split+dict]
zdim2 : 0.4s | | [perl + split+dict]
codeforester : 0.45s | | [perl + split+dict]
oliv : 0.5s | | [python + compiled regex + re.search()]
zdim : 0.61s | | [perl + regexp+dict]
inian2 : 0.73s |1.7s | [awk + index($0,i)]
inian5 : 18.12s | | [LC_ALL awk + match($0,i) [loose]]
inian1 : 19.46s | | [awk + match($0,i) [loose]]
inian5B : 42.27s | | [LC_ALL awk + match($0,i) [strict]]
inian1B : 85.67s | | [awk + match($0,i) [strict]]
Vasiliou Results : 2 X CPU Intel 2 Duo T6570 @ 2.10GHz - 2Gb RAM-Debian Testing 64bit- kernel 4.9.0.1 - no cpu freq scaling.
更真实的测试用例
然后我创建了一个更现实的案例,file1.txt 有 100 个字,file2.txt 有 1000 万行(268Mb 文件大小)。我使用shuf -n1000 /usr/share/dict/american-english > words.txt从/usr/share/dict/american-english的字典中提取了1000个随机单词,然后将其中的100个单词提取到file1.txt中,然后以与上述第一个测试用例相同的方式构造file2.txt。请注意,字典文件是 UTF-8 编码的,我从 words.txt 中删除了所有非 ASCII 字符。
然后我在没有前一个案例中最慢的三种方法的情况下运行测试。 IE。 inian1、inian2 和 inian5 被排除在外。以下是新结果:
gregory1 : 0.86s | [parallel + fgrep -f [loose]]
Vasiliou2 : 0.94s | [LC_ALL join [requires sorted files]]
inian4B : 1.12s | LC_ALL grep -E -f [strict]
BOC2B : 1.13s | LC_ALL grep -E [strict]
BOC2 : 1.15s | LC_ALL grep -E [loose]
BOC1 : 1.18s | grep -E [loose]
ikegami : 1.33s | grep -P
Vasiliou : 1.37s | [join [requires sorted files]]
hakon1 : 1.44s | [perl + c]
inian4 : 2.18s | LC_ALL fgrep -f [loose]
codeforester_orig : 2.2s | fgrep -f [loose]
inian6 : 2.82s | [LC_ALL awk + split+dict]
jjoao : 3.09s | [compiled flex generated c code]
dawg : 3.54s | [python + split+dict]
zdim2 : 4.21s | [perl + split+dict]
codeforester : 4.67s | [perl + split+dict]
inian3 : 5.52s | [awk + split+dict]
zdim : 6.55s | [perl + regexp+dict]
gregory1B : 45.36s | [parallel + grep -E -f [strict]]
oliv : 60.35s | [python + compiled regex + re.search()]
BOC1B : 74.71s | grep -E [strict]
codeforester_origB : 75.52s | grep -E -f [strict]
注意
基于grep 的解决方案正在寻找整行的匹配项,因此在这种情况下,它们包含一些错误匹配项:codeforester_orig、BOC1、BOC2、gregory1、inian4 方法, 和oliv 从 10,000,000 行中提取了 1,087,609 行,而其他方法从 file2.txt 中提取了正确的 997,993 行。
注意事项