在哈希查找中,符号如何比字符串快?

Posted

技术标签:

【中文标题】在哈希查找中,符号如何比字符串快?【英文标题】:How are Symbols faster than Strings in Hash lookups? 【发布时间】:2017-12-21 06:08:26 【问题描述】:

我理解为什么应该在哈希中使用符号而不是字符串的一个方面。也就是说,内存中只有一个给定符号的实例,而给定字符串可能有多个具有相同值的实例。

我不明白在哈希查找中符号如何比字符串快。我已经看过答案here,但我还是不太明白。

如果:foo.hash == :foo.object_id 返回true,那么它就会有一定的意义,因为这样它就可以使用对象 id 作为散列的值,而不必每次都计算它。然而事实并非如此,:foo.object_id 不等于:foo.hash。因此我很困惑。

【问题讨论】:

具体哪一部分,你不明白? 特别是为什么哈希查找对于字符串是 O(n),而对于符号是 O(1)。这是参考我分享的链接上的第二个答案。 :foo.hash == :foo.object_id # => false 【参考方案1】:

hash 没有义务等同于 object_id。这两件事的目的完全不同。 hash 的要点是尽可能地具有确定性和随机性,以便您插入哈希的值均匀分布。 object_id 的重点是定义一个唯一的对象标识符,尽管不要求它们是随机的或均匀分布的。事实上,将它们随机化会适得其反,只会无缘无故地让事情变慢。

符号往往更快的原因是因为它们的内存被分配一次(除了垃圾收集问题)并为同一符号的所有实例回收。字符串不是这样的。它们可以通过多种方式构造,甚至两个字节对字节相同的字符串也可能是不同的对象。事实上,除非您确定它们是同一个对象,否则假设它们是比其他对象更安全。

现在在计算hash 时,即使字符串变化很小,该值也必须是随机不同的。由于符号不能改变计算,因此可以进行更多优化。您可以只计算 object_id 的哈希值,因为它不会改变,例如,字符串需要考虑其自身的内容,这可能是动态的。

尝试基准测试:

require 'benchmark'

count = 100000000

Benchmark.bm do |bm|
  bm.report('Symbol:') do
    count.times  :symbol.hash 
  end
  bm.report('String:') do
    count.times  "string".hash 
  end
end

这给了我这样的结果:

       user     system      total        real
Symbol:  6.340000   0.020000   6.360000 (  6.420563)
String: 11.380000   0.040000  11.420000 ( 11.454172)

在这个最简单的情况下,这很容易快 2 倍。根据一些基本测试,字符串代码的性能会随着字符串变长而降低O(N),但符号时间保持不变。

【讨论】:

所以String#hash 的平均运行时间比Symbol#hashFixnum#hash 的运行时间长,因为由于其动态特性,它执行更多计算来计算散列。对吗? 字符串的哈希值是根据字符串的全部内容计算的,并且随着内容的变化,哈希值也会发生变化。相比之下,像 Symbol 这样的“单例”的哈希应该是微不足道的。我不确定 Fixnum/Integer,但如果你好奇,你也可以尝试对其进行基准测试。应该是相似的。 是的,Fixnum#hashSymbol#hash 非常相似。 使用基于符号的键是您应该渴望做的事情除非您正在处理完全任意的数据。就内存而言,符号是一次性成本,但如果您将它们用于所有内容,则该成本会累积。想象“符号”就像“缓存(内部化)字符串”。您只想对经常使用的东西执行此操作,例如 name: "Bob", age: 21 、方法参数等。 非常有趣且内容丰富(并且呈现良好)。一个很好的答案!【参考方案2】:

只是想补充一点,我不完全同意@tadman 提出的数字。在我的测试中,使用 calcualte '#hash' 最多快 1.5 倍。我使用benchmark/ips 来测试性能。

require 'benchmark/ips'

Benchmark.ips do |bm|
  bm.compare!
  bm.report('Symbol:') do
    :symbol.hash
  end
  bm.report('String:') do
    'string'.hash
  end
end

这导致

Comparison:
             Symbol:: 10741305.8 i/s
             String::  7051559.3 i/s - 1.52x slower

此外,如果您启用“冻结字符串文字”(这将在未来的 ruby​​ 版本中默认设置),则差异会降至 1.2 倍:

# frozen_string_literal: true

Comparison:
             Symbol::  9014176.3 i/s
             String::  7532196.9 i/s - 1.20x slower

【讨论】:

【参考方案3】:

字符串作为散列键的额外开销是,由于字符串是可变的,并且通常使用具有散列键,因此 Hash 类会复制所有字符串键(可能使用 dup 或 clone 之类的方法)以保护哈希的完整性免受密钥损坏。

考虑:

irb(main):001:0> a = 
=> 
irb(main):002:0> b = "fred"
=> "fred"
irb(main):003:0> a[b] = 42
=> 42
irb(main):004:0> a
=> "fred"=>42
irb(main):005:0> b << " flintstone"
=> "fred flintstone"
irb(main):006:0> a
=> "fred"=>42
irb(main):007:0> b
=> "fred flintstone"
irb(main):008:0>
irb(main):008:0> b.object_id
=> 17350536
irb(main):009:0> a.keys[0].object_id
=> 15113052
irb(main):010:0>

符号是不可变的,不需要如此激烈的措施。

【讨论】:

我不确定你想用你的代码证明什么。关键不是变量b,而是字符串"fred"。之后你可以修改ba 不再关心了。不过,您是对的,密钥被复制并冻结。看到这个thread 这就是我的意思。对原始字符串的更改不会反映在哈希键中,因为它被复制(或克隆?)。符号不需要这种处理,这说明了关于速度优势的问题的原点。也许我应该展示一下钥匙是如何被冻结的?但是,我被引导相信冻结不像复制那样耗时。

以上是关于在哈希查找中,符号如何比字符串快?的主要内容,如果未能解决你的问题,请参考以下文章

哈希表的理解

哈希表的理解

哈希表查找速度为什么那么快?快在哪里了?

程序员,你应该知道的数据结构之哈希表

查找算法系列之复杂算法:哈希查找

哈希表