【问题标题】:Which is faster in ruby - a hash lookup or a function with a case statement?ruby 中哪个更快 - 哈希查找或带有 case 语句的函数?
【发布时间】:2011-05-09 20:37:12
【问题描述】:

我们在时间紧迫的脚本中有几个地方可以将旧 ID 转换为字符串。目前,我们在函数中使用 case 语句,如下所示:

def get_name id
  case id
    when 1
      "one thing"
    when 3
      "other thing"
    else
      "default thing"
  end
end

我正在考虑用哈希查找替换它,如下所示:

NAMES = {
  1 => "one thing",
  3 => "other thing",
}
NAMES.default = "default thing"

感觉使用NAMES[id] 应该比使用get_name(id) 更快——但是是吗?

【问题讨论】:

  • Simon,这是过早的优化。除非您有成千上万的案例,否则我不会费心找出哪一个的性能更高。只关注您的代码。
  • 我们只有几个案例,但我们有大约 7,000,000 次查找。

标签: ruby performance


【解决方案1】:

这是一个示例,它显示了符号查找更快的情况。前面的示例是基于整数的键。

https://gist.github.com/02c8f8ca0cd4c9d9ceb2

【讨论】:

    【解决方案2】:
    $ ruby -v
    ruby 1.9.2p0 (2010-08-18 revision 29036) [i686-linux]
    
    $ cat hash_vs_case.rb 
    require 'benchmark'
    
    def get_from_case(id)
      case id
        when 1
          "one thing"
        when 3
          "other thing"
        else
          "default thing"
      end
    end
    
    NAMES = {
      1 => "one thing",
      3 => "other thing",
    }
    NAMES.default = "default thing"
    
    def get_from_hash(arg)
      NAMES[arg]
    end
    
    n = 1000000
    Benchmark.bm do |test|
      test.report("case  1") { n.times do; get_from_case(1); end }
      test.report("hash  1") { n.times do; get_from_hash(1); end}
      test.report("case 42") { n.times do; get_from_case(42); end }
      test.report("hash 42") { n.times do; get_from_hash(42); end}
    end
    
    $ ruby -w hash_vs_case.rb 
          user     system      total        real
    case  1  0.330000   0.000000   0.330000 (  0.422209)
    hash  1  0.220000   0.000000   0.220000 (  0.271300)
    case 42  0.310000   0.000000   0.310000 (  0.390295)
    hash 42  0.320000   0.010000   0.330000 (  0.402647)
    

    这就是您要升级的原因:

    $ ruby -v
    ruby 1.8.7 (2009-06-12 patchlevel 174) [i486-linux]
    
    $ ruby -w hash_vs_case.rb 
          user     system      total        real
    case  1  1.380000   0.870000   2.250000 (  2.738493)
    hash  1  1.320000   0.850000   2.170000 (  2.642013)
    case 42  1.500000   0.960000   2.460000 (  3.029923)
    hash 42  1.890000   0.890000   2.780000 (  3.456969)
    

    【讨论】:

    • 您对get_name() 的调用为每个循环添加了一些额外代码,这些代码未反映在使用名称的哈希查找中。要进行同等比较,您应该使用 def 进行哈希查找,或者将 case 语句直接嵌入到 n.times 循环中。我仍然希望哈希查找速度更快,但在进行基准测试以获得准确数字时消除任何外部影响非常重要。
    • 实际上,我想将直接哈希查找与函数调用加大小写进行比较,因此将大小写包含在 def 中而不是哈希查找中是公平的。
    【解决方案3】:

    首先,有几点。一个是像这样或多或少做同样事情的低级语言结构几乎永远不会成为任何实际应用程序的瓶颈,因此(通常)关注它们是愚蠢的。其次,正如已经提到的,如果你真的关心它,你应该对它进行基准测试。 Ruby 的基准测试和分析工具当然不是编程生态系统中最先进的,但它们可以完成工作。

    我的直觉是哈希会更快,因为(我再次猜测)case 语句必须依次检查每个条件(使得找到项目 O(n) 而不是 O(1))。但是让我们检查一下!

    完整的基准测试代码位于https://gist.github.com/25 基本上,它会生成一个文件,定义适当的大小写/哈希,然后使用它们。我继续将哈希查找也放入方法调用中,这样开销就不会成为一个因素,但在现实生活中,没有理由将它卡在方法中。

    这就是我得到的。在每种情况下,我都会进行 10,000 次查找。时间是以秒为单位的用户时间

    Case statement, 10 items  0.020000
    Hash lookup, 10 items     0.010000
    
    Case statement, 100 items  0.100000
    Hash lookup, 100 items     0.010000
    
    Case statement, 1000 items  0.990000
    Hash lookup, 1000 items     0.010000
    

    因此,case 语句看起来非常像 O(n)(这并不令人震惊)。另请注意,即使在 case 语句中,10K 查找仍然只是一秒钟,因此除非您正在对这些查找进行度量但加载,否则您最好专注于其余代码。

    【讨论】:

    • Case 按顺序检查每个条件,这可以使用调试器和单步执行来证明,而且 case 正在检查类型和相等性,因为它是使用 === 实现的,所以我对您的结果并不感到惊讶。长 when 列表会减慢速度,因此值得尝试确定最常点击的列表并将它们移到顶部,或者将测试分解为某种子组并使用嵌套的 case 语句。
    【解决方案4】:

    正如 Martin 所说,这取决于您要检查多少个 ID。您是从数据库中选择 ID 和相应的字符串还是要对其进行硬编码。如果只有几张支票,那么您可以选择 CASE。但是如果需要,没有修改 ID/String 对的选项。

    另一方面,如果您要从数据库中挑选大量 ID,则可以使用 Dictionary 之类的工具来存储名称/值对。

    虽然字典在内存中,但查找速度可能很快。

    但归根结底,这完全取决于您是使用不断变化的 ID/字符串对还是仅使用少量常量。

    【讨论】:

      【解决方案5】:

      由于这取决于许多因素(您要转换多少个不同的 ID,编译器编译 case when 语句的智能程度),我的建议是:衡量它

      编写一个小型测试例程,并将例如 10.000.000 个 id 转换为字符串。使用任一实现执行此操作几次并比较结果。如果你没有显着差异,就拿你喜欢的任何东西(我认为,哈希解决方案更优雅一点......)

      【讨论】:

      • 另一个因素:哈希调用hash 和可能eql? 而case 调用===。所以速度也取决于你的散列和比较代码。
      • 顺便说一句,用 ruby​​ 1.8.7 和 3 个案例进行的快速测试表明哈希值较慢,正如 Nakilon 所说。
      • 我本来希望看到case-解决方案在三种情况下会更快。但正如我所提到的(在对似乎已重绘的回复的评论中):对于大量情况,case 可能需要对每个when-clause 进行比较(这意味着 O(n) 复杂度),而一个好的哈希实现具有恒定的复杂度(这意味着 one 调用 hashone 调用 eql?) ,正如我所说:我认为西蒙必须用他的实际数据做基准测试......
      【解决方案6】:

      为什么不在代码的时间关键部分内联使用case 语句,而不是使其成为自己的函数调用?这避免了调用堆栈的开销,也避免了哈希查找的开销。

      您也可以随时自己进行基准测试。做这样的事情:

      t = Time.now
      1_000_000.times { ...your code... }
      secs = Time.now - t
      

      用每个选项替换“您的代码”。

      【讨论】:

      • @banister,谢谢。我不知道 ruby​​ 基准库。
      【解决方案7】:

      case when 具有 n 复杂性。
      hash lookup 具有 ln(n) 复杂性,但使用附加对象(哈希)并调用其方法具有自己的惩罚。

      因此,如果您有很多案例(数千、数百万、...),则哈希查找会更好。但是在您的情况下,当您只有 3 个变体时,case when 当然会快得多。

      【讨论】:

      • 我不确定哈希查找真的有 log(n) 复杂性:如果它被实现为传统的哈希列表,那将是 O(1) ,即复杂度不变,但需要额外的步骤来计算哈希值(这对于整数来说是微不足道的......)
      • 据我所知它是作为树实现的,所以log(n) 是正确的。
      • @Baju:@Martin:Ruby 散列是散列映射(这就是为什么对象需要定义散列方法以及为什么它们被称为散列而不是树的原因)。哈希查找在一般情况下具有恒定的复杂性,而在最坏的情况下是线性的(哈希中的所有键都具有相同的哈希值)。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-10-26
      • 2015-10-15
      • 1970-01-01
      • 2012-03-21
      • 1970-01-01
      • 2012-11-29
      相关资源
      最近更新 更多