【问题标题】:Perl speed comparison on using hash and hash reference使用哈希和哈希引用的 Perl 速度比较
【发布时间】:2014-05-26 16:26:36
【问题描述】:

我很想比较使用散列还是引用散列更好,散列参考据我所知只是指向散列本身的指针,所以我认为应该没有速度差异。

我做了一个基本的基准测试,发现哈希引用比直接使用哈希慢 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 这样的大多数模块都使用哈希引用来返回结果。此外,大多数模块接受哈希引用而不是哈希作为参数,并且还返回哈希引用而不是哈希。

【问题讨论】:

  • 与大多数其他编程结构相比,使用散列或散列引用之间不太可能存在严重的性能差异。 IO 将是一个更耗时的问题,如果您正在执行密集算法,则可能会进行计算。在选择数据结构时,我主要关心的是什么是最简单的。无论哪种方式,您的基准测试都没有解决您关于数据结构传递的最后一个问题,所以这个问题是一个红鲱鱼。

标签: perl hash


【解决方案1】:

哈希访问元素的速度更快; hashrefs 可以更快地作为参数传递给函数,或者作为函数的结果返回。如果您考虑一下,这是有道理的:

  • hashref 基本上是一个指向hash 的指针,所以当Perl 看到$href->{xyz} 时,它需要跟随指针找到hash,然后在hash 中找到元素xyz。当 Perl 看到 $hash{xyz} 时,它不需要做那个初始的指针跟随位;它可以立即找到元素xyz

  • 哈希不能直接传入或传出 subs;它们需要被展平成一个标量列表。如果一个哈希有四个键和四个值,那么将它传递给一个子意味着将一个包含八个标量的列表传递给函数。在函数内部,您可能会有类似my %args = @_ 的内容,它将这八个标量复制到 new 哈希中。有很多工作要做。传递 hashref 只是传递单个标量的问题,因此速度更快。

这主要是微优化,您应该选择最适合您的程序的数据结构。然而,对于那些你真的需要尽可能地提高速度的场合,可以两全其美......

假设您有一个函数需要接受散列(或者可能是散列引用;您还没有决定)并且需要将一些键相加。这是您原来的两个选项:

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。这是一个模块,它允许我们创建一个词法变量,作为另一个变量的别名。特别是,我们可以创建一个词法哈希变量,它就像一个 hashref 指向的哈希的别名。

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?
【解决方案2】:

您的基准测试有问题。

您的 hashref 示例不仅使用哈希,而且还为每次迭代创建它。哈希示例经过优化,可以始终重用相同的哈希。

如果您修改第二个基准以强制简单哈希版本始终创建新哈希,则 hashref 版本会变得更快:

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 列表)以传递参数。这就是为什么通常首选散列引用的原因 - 存储在散列中的数据量越大,这种影响越大,如果你想在函数之间传递一个大散列,这是标准做法,通常不被认为是微-优化,使用参考。
  • @NeilSlater ??这里没有传递参数吗?但是,是的,不通过引用传递或返回哈希通常是愚蠢的。
  • 你说得对,我错过了 OP 正在询问 returning 结果。实际代码中同一枚硬币的两面,但参数传递不在基准测试中。
【解决方案3】:

当然,通过引用访问散列元素比直接访问散列元素要慢。额外的工作需要额外的时间。

但是需要多长时间?根据您的测试,

( 1 / (5045409/s) - 1/(6045949/s) ) / 3 derefs
= 0.000,000,011 s/deref
= 11 ns/deref

这不是你应该担心的!

如果我的基准测试是正确的并且哈希引用太慢了

您的基准测试并未显示它们运行缓慢。

为什么像 DBI 这样的大多数模块都使用哈希引用来返回结果。

相对于什么? sub 唯一可以返回的是标量列表。它不能返回哈希值。 fetch_hashref 可以返回一个键值对列表,您可以从中构建散列,但如果它已经在 sub 中构建散列,这将比使用引用慢得多。

【讨论】:

    【解决方案4】:

    据我了解(这可能会产生误导),与实际数据结构相比,返回引用的最大优势在于子例程之外的赋值——而不是访问结构的性能。返回引用不会在分配时复制内存中的数据。

    我预计第二个示例可能会慢一些。

    my $data = getData();
    
    sub getData {
        return { a => '1' };
    }
    

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

    【讨论】:

    • Re "返回引用不会复制分配时内存中的数据。"使用散列引用时,仍然需要完成对散列的分配。实际的区别在于,如果您已经在 sub 中需要哈希,它会避免构造两个哈希。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-03-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多