使用哈希和哈希引用的Perl速度比较

4

我正在尝试比较使用哈希表还是引用哈希表哪个更好,据我所知,哈希表引用只是指向哈希表本身的指针,因此我认为不应该有速度差异。

我进行了基本的基准测试,并发现哈希表引用比直接使用哈希表平均慢20-27%。

这是我使用的基本基准测试代码:

use Benchmark qw(:all);

cmpthese(10_000_000, {
    hash            =>  sub { my %hash = (); },
    hashref =>  sub { my $hahref = {}; }
});

cmpthese(10_000_000, {
    hash            =>  sub {
                                    my %hash;
                                    $hash{fname}="first name";
                                    $hash{mname}="middle name";
                                    $hash{lname}="last name";
                                },

    hashref =>  sub {
                                    my $hahref;
                                    $hahref->{fname}="first name"; 
                                    $hahref->{mname}="middle name";
                                    $hahref->{lname}="last name"; 
                                },

    hashrefs    =>  sub {
                                    my $hahref;
                                    $$hahref{fname}="first name"; 
                                    $$hahref{mname}="middle name";
                                    $$hahref{lname}="last name"; 
                                },
});

以下是在戴尔笔记本电脑、Windows 8、Core i7、16MB RAM上的基准测试结果:

             Rate hashref    hash
hashref 5045409/s      --    -17%
hash    6045949/s     20%      --

             Rate hashrefs  hashref     hash
hashrefs 615764/s       --      -2%     -21%
hashref  625978/s       2%       --     -19%
hash     775134/s      26%      24%       --

Output completed (1 min 6 sec consumed)

我的问题是,如果我的基准测试正确而哈希引用非常慢,为什么大多数模块(如DBI)使用哈希引用返回结果?另外,大多数模块接受哈希引用而不是哈希作为参数,并返回哈希引用而不是哈希。

2
使用哈希或哈希引用与大多数其他编程结构相比,性能差异不大。IO将是一个更耗时的问题,如果您正在执行密集算法,则可能还涉及计算。在选择数据结构时,我的主要关注点只是什么最容易。无论如何,您的基准测试并未解决有关传递数据结构的最终问题,因此该问题是一个红鲱鱼。 - Miller
4个回答

6
哈希表更快地访问元素;哈希引用更快地作为函数的参数传递或作为函数结果返回。如果你想一下,这是有道理的:
  • 哈希引用基本上是指向哈希表的指针,所以当Perl看到 $href->{xyz} 时,它需要跟随指针找到哈希表,然后在哈希表中找到元素xyz。当Perl看到 $hash{xyz} 时,它不需要执行初始的指针跟随操作,它可以直接找到元素xyz
  • 哈希表不能直接传递到函数中或从函数中返回;它们需要被展开成标量列表。如果一个哈希表有四个键和四个值,那么将其传递到函数意味着将八个标量作为列表传递给函数。在函数内部,你可能会有像 my %args = @_ 这样的代码,它将这八个标量复制到一个新的哈希表中。有很多工作要做。传递哈希引用只需要传递一个单一的标量,所以速度更快。
大多数情况下,这只是微小的优化,你应该选择对你的程序最合适的数据结构。然而,在那些你真正需要尽可能提高速度的场合,有可能同时拥有两个优点...
假设你有一个函数,需要接受一个哈希表(或者也许是一个哈希引用;你还没有决定),并需要将一些键相加。这里是你最初的两个选项:
sub add_hash {
    my %hash = @_;
    return $hash{foo} + $hash{bar} + $hash{baz};
}

sub add_hashref {
    my ($href) = @_;                                    # faster than add_hash
    return $href->{foo} + $href->{bar} + $href->{baz};  # slower than add_hash
}

现在让我们提取Data::Alias。这是一个模块,它允许我们创建一个词法变量,其作为另一个变量的别名。特别是,我们可以创建一个词法哈希变量,该变量的行为类似于由哈希引用指向的哈希的别名。
use Data::Alias;

sub add_hashref_2 {
    my ($href) = @_;                               # faster than add_hash
    alias my %hash = %$href;                       # ... magic happens ...
    return $hash{foo} + $hash{bar} + $hash{baz};   # faster than add_hashref
}

或者更好的方法是:
use Data::Alias;

sub add_hashref_3 {
    alias my %hash = %{ $_[0] };
    return $hash{foo} + $hash{bar} + $hash{baz};
}

...这样可以避免最初的列表分配。

我强调,这是微小的优化。通常有更好的方法来加速您的代码-备忘录、根本算法更改、在XS中重写选定的热代码路径等。但是在某些(非常有限的)情况下,这种魔术可能会有所帮助。


您能详细说明一下“在XS中重写选定的热代码路径”是什么意思吗?XS是什么? - Ignat

5

你的基准测试出现了问题。

你的哈希引用示例不仅使用了哈希,而且还为每个迭代创建了一个新的哈希。 哈希示例被优化为始终重复使用相同的哈希。

如果你修改第二个基准测试,强制简单哈希版本始终创建一个新的哈希,那么哈希引用版本会更快:

cmpthese(10_000_000, {
    hash => sub {
        my %hash;
        $hash{fname}="first name";
        $hash{mname}="middle name";
        $hash{lname}="last name";
        return \%hash;          
    },
    hashref => sub {
        my $hahref;
        $hahref->{fname}="first name"; 
        $hahref->{mname}="middle name";
        $hahref->{lname}="last name"; 
        return $hahref;         
    },
} );

但是这里真正的重点是停止尝试微调优化;编写您的代码使其有意义,只有当出现问题时才会狭窄地查看实际表现不佳的代码进行优化。


当考虑到这个因素时,哈希版本比参考版本慢,可能是因为需要复制哈希元素(通过Perl列表)以便传递参数。这个事实就是为什么通常优先使用哈希引用 - 存储在哈希中的数据量越大,这种影响就越大,如果您有一个要在函数之间传递的大型哈希表,那么使用引用是标准做法,而且通常不被认为是微观优化。 - Neil Slater
1
@NeilSlater 这里没有传递任何参数?但是,传递或返回一个非引用哈希通常是愚蠢的。 - ysth
你说得对,我错过了 OP 正在询问“返回”结果的问题。在实际代码中,参数传递和返回结果是同一个硬币的两面,但参数传递不在基准测试中。 - Neil Slater

4
当然,通过引用访问哈希元素要比直接访问慢。额外的工作需要更多的时间。
但是它需要花费多少时间?根据你的测试,
( 1 / (5045409/s) - 1/(6045949/s) ) / 3 derefs
= 0.000,000,011 s/deref
= 11 ns/deref

你不应该担心这个问题!

如果我的基准测试是正确的,哈希引用是如此缓慢

你的基准测试并没有显示它们很慢。

为什么大多数模块(如DBI)使用哈希引用返回结果。

相对于什么?子程序只能返回标量列表,无法返回哈希。如果使用fetch_hashref返回键值对列表,再构建哈希表,那将比在子程序中使用引用要慢得多,因为引用已经在子程序中构建好了哈希表。


1
据我理解(可能会有误导性),返回引用与实际数据结构相比最大的优点在于子程序外部的赋值,而不是访问结构的性能。返回引用不会在赋值时在内存中复制数据。
我认为第二个例子可能会更慢。
my $data = getData();

sub getData {
    return { a => '1' };
}

vs

my %data = getData();

sub getData {
    return my %hash = ( a => '1' );
}

1
关于“返回引用不会在赋值时复制内存中的数据”的问题。当使用哈希引用时,仍需要对哈希进行赋值。实际上的区别在于,如果您已经在子程序中需要一个哈希表,则可以避免构造两个哈希表。 - ikegami

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