如何在Perl中优化二维哈希遍历



我有一个散列%signal_db的散列。一个典型的元素是:$signal_db{$cycle}{$key}。有10000个信号和10000个按键。

有没有办法优化(按时间)这段代码:

foreach my $cycle (sort numerically keys %signal_db) {
    foreach my $key (sort keys %{$signal_db{$cycle}}) {
        print $signal_db{$cycle}{$key}.$key."n";
    }
}

元素必须按照与我的代码中相同的顺序打印。

两个微观优化:映射内部哈希而不是常量解引用,缓冲区而不是常量打印。可以使用替代存储格式来取消排序,测试了两种变体。结果:

               Rate     original         try3  alternative alternative2
original     46.1/s           --         -12%         -21%         -32%
try3         52.6/s          14%           --         -10%         -22%
alternative  58.6/s          27%          11%           --         -13%
alternative2 67.5/s          46%          28%          15%           --

结论:

最好使用预排序的存储格式,但如果没有C,win可能在100%以内(在我的测试数据集上)。提供的有关数据的信息表明,外部哈希中的键几乎是连续数字,所以这需要数组。

脚本:

#!/usr/bin/env perl
use strict; use warnings;
use Benchmark qw/timethese cmpthese/;
my %signal_db = map { $_ => {} } 1..1000;
%$_ = map { $_ => $_ } 'a'..'z' foreach values %signal_db;
my @signal_db = map { { cycle => $_ } } 1..1000;
$_->{'samples'} = { map { $_ => $_ } 'a'..'z' } foreach @signal_db;
my @signal_db1 = map { $_ => [] } 1..1000;
@$_ = map { $_ => $_ } 'a'..'z' foreach grep ref $_, @signal_db1;
use Sort::Key qw(nsort);
sub numerically { $a <=> $b }
my $result = cmpthese( -2, {
    'original' => sub {
        open my $out, '>', 'tmp.out';
        foreach my $cycle (sort numerically keys %signal_db) {
            foreach my $key (sort keys %{$signal_db{$cycle}}) {
                print $out $signal_db{$cycle}{$key}.$key."n";
            }
        }
    },
    'try3' => sub {
        open my $out, '>', 'tmp.out';
        foreach my $cycle (map $signal_db{$_}, sort numerically keys %signal_db) {
            my $tmp = '';
            foreach my $key (sort keys %$cycle) {
                $tmp .= $cycle->{$key}.$key."n";
            }
            print $out $tmp;
        }
    },
    'alternative' => sub {
        open my $out, '>', 'tmp.out';
        foreach my $cycle (map $_->{'samples'}, @signal_db) {
            my $tmp = '';
            foreach my $key (sort keys %$cycle) {
                $tmp .= $cycle->{$key}.$key."n";
            }
            print $out $tmp;
        }
    },
    'alternative2' => sub {
        open my $out, '>', 'tmp.out';
        foreach my $cycle (grep ref $_, @signal_db1) {
            my $tmp = '';
            foreach (my $i = 0; $i < @$cycle; $i+=2) {
                $tmp .= $cycle->[$i+1].$cycle->[$i]."n";
            }
            print $out $tmp;
        }
    },
} );
my %signal_db = map {$_ => {1 .. 1000}} 1 .. 1000;
sub numerically {$a <=> $b}
sub orig {
    my $x;
    foreach my $cycle (sort numerically keys %signal_db) {
        foreach my $key (sort keys %{$signal_db{$cycle}}) {
            $x += length $signal_db{$cycle}{$key}.$key."n";
        }
    }
}
sub faster {
    my $x;
    our ($cycle, $key, %hash); # move allocation out of the loop
    local *hash;      # and use package variables which are faster to alias into
    foreach $cycle (sort {$a <=> $b} # the {$a <=> $b} literal is optimized
                    keys %signal_db) {
        *hash = $signal_db{$cycle}; # alias into %hash
        foreach $key (sort keys %hash) {
            $x += length $hash{$key}.$key."n";  # simplify the lookup
        }
    }
}
use Benchmark 'cmpthese';
cmpthese -5 => {
    orig   => &orig,
    faster => &faster,
};

得到:

速率原点更快原始2.56/s--15%更快3.03/s 18%--

这不是一个巨大的收获,但它是有意义的。在不更改数据结构以使用预排序数组的情况下,您无法进行更多优化。(或在XS中写入全部内容)

foreach循环切换为使用外部包变量可以节省一点时间,因为perl不必在循环中创建词法。此外,包变量的别名转换速度似乎要快一些。将内部查找减少到单个级别也有帮助。

我假设您正在打印到STDOUT,然后将输出重定向到文件?如果是这样的话,使用Perl直接打开输出文件,然后打印到该句柄可以提高文件IO性能。另一个微观优化可以是用不同的记录大小进行实验。例如,在内部循环中构建一个数组,然后在外部循环的底部连接/打印它,这样可以节省时间吗?但这是相当依赖于设备的(由于其他IO缓存层,可能毫无意义),所以我将把测试留给您。

我首先尝试Sort::Key模块,因为排序比简单的循环和打印花费更长的时间。此外,如果内部散列键(大部分)相同,那么你应该简单地对它们进行预排序,但我认为情况并非如此,否则你已经在做了。

显然,您也应该尝试将$signal_db{$cycle}分配给引用。您可能会发现eachkeys加上检索更快,尤其是与Sort::Key一起使用时。我会检查地图是否也比foreach运行得更快,可能是一样的,但谁知道呢。如果你给print传递一个列表或多次调用它,你可能会发现它运行得更快。

我还没有尝试过这个代码,但把除了each之外的所有这些想法放在一起会得到:

foreach my $cycle (nsort keys %signal_db) {
    my $r = $signal_db{$cycle};
    map { print ($r->{$_},$_,"n"); } (nsort keys %$r);
}

这里有一篇关于perl中排序的文章,如果您想了解如何使用each,请查看Schwartzian Transform。

如果您的代码不需要具有安全意识,那么您可以通过设置Perl_HASH_SSEED或相关变量来禁用Perl对算法复杂性攻击的保护,和/或通过更改设置来重新编译Perl,这样Perl的keysvalues命令就已经按排序顺序返回了键或值,从而节省了大量的排序时间。但在这样做之前,请先看28C3的演讲。我不知道这是否可行,你需要阅读Perl源代码的这一部分,也许在C.中实现循环更容易。

最新更新