在Ruby中,哈希表查找和使用case语句的函数哪个更快?

15

我们在一个时间紧迫的脚本中有几个地方需要将旧的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)更快,但实际上呢?


2
Simon,这是过早的优化。除非你有成千上万个案例,否则我不会费心去找出哪个更高效。只专注于你的代码。 - maček
我们只有几个案例,但我们有大约700万次查找。 - Simon
7个回答

33

首先,需要指出的是,这种做法使用低级语言构造,几乎在任何实际应用中都不会成为瓶颈,因此专注于它们通常是愚蠢的。其次,如已经提到的那样,如果你真的关心这个问题,应该进行基准测试。Ruby的基准测试和分析工具当然不是编程生态系统中最先进的,但它们能完成任务。

我的直觉是哈希表会更快,因为(再次猜测)case语句必须逐个检查每个条件(使得查找项目的时间复杂度为O(n),而非O(1))。但让我们来测试一下!

完整的基准测试代码在https://gist.github.com/25。基本上,它生成一个定义适当的case/hash的文件,然后使用它们。我还将哈希表查找放在了一个方法调用中,这样开销就不会成为一个因素,但在实际情况下,没有理由将其锁定在方法内部。

这里是我的结果。在每种情况下,我都执行了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 语句中进行了10000次查找,仍然只需要一秒钟的时间,因此,除非您要执行大量此类查找操作,否则最好专注于代码的其他部分。


2
Case会按顺序检查每个条件,可以使用调试器和单步执行来证明这一点,而且case会检查类型和相等性,因为它是使用===实现的,所以我对你的结果不感到惊讶。长的when列表会减慢速度,因此值得尝试确定最常命中的列表并将其移到顶部,或者将测试分解成某种子组并使用嵌套的case语句。 - the Tin Man

7

由于这取决于许多因素(您想要转换多少个不同的ID,编译器可以多聪明地编译case when语句),我的建议是:测量它

编写一个小测试程序,并将10,000,000个id转换为字符串。使用任何一种实现进行几次比较结果。如果没有显着差异,则选择您更喜欢的那个(我认为,哈希解决方案更加优雅...)


另一个因素是:哈希调用hash和可能的eql?,而case调用===。因此,速度也取决于您的哈希和比较代码。 - Baju
顺便说一句,通过使用Ruby 1.8.7进行快速测试,只有3个案例表明哈希表比较慢,正如Nakilon所说。 - s.m.
我本来期望看到case解决方案在仅有三种情况下更快。但正如我已经提到的(在一个被重新绘制的响应评论中),对于大量情况,case可能需要为每个when子句进行比较(这意味着O(n)复杂度),而一个好的哈希实现具有恒定的复杂度(这意味着一次调用hash一次调用eql?),所以我说:Simon必须使用他的实际数据进行基准测试,我想... - MartinStettner

1
$ 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() 的调用为每次循环增加了一些额外的代码,而这在使用NAMES进行哈希查找时并没有反映出来。为了使比较相等,你应该使用哈希查找的 def,或直接将 case 语句嵌入到 n.times 循环中。我仍然期望哈希查找会更快,但在基准测试时消除任何外部影响以获得准确数字是非常重要的。 - the Tin Man
实际上,我想比较直接哈希查找和函数调用加情况的效率,因此将情况包装在def中是公平的,但不包括哈希查找。 - Simon

1

0
为什么不在代码的时间关键部分内联使用case语句,而不是将其作为单独的函数调用?这可以避免调用堆栈的开销,也可以避免哈希查找的开销。
您还可以随时对自己进行基准测试。做类似于这样的事情:
t = Time.now
1_000_000.times { ...your code... }
secs = Time.now - t

用每个选项替换“your code”。

@banister,谢谢。我不知道Ruby的Benchmark库。 - Ben Lee

0

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

另一方面,如果您从数据库中选择了很多ID,则可以使用类似字典的东西来存储名称/值对。

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

但最终,这一切都取决于您是否正在处理不断变化的ID/string对或仅有少量常量。


-1

case when 的复杂度为 n
hash lookup 的复杂度为 ln(n),但使用额外的对象(哈希表)并调用其方法会有自己的惩罚。

因此,如果您有很多情况(成千上万,...),哈希查找更好。但在您只有3个变量的情况下,case when 当然会快得多。


我不确定哈希查找是否真的具有*log(n)复杂度:如果它被实现为传统的哈希列表,那么它将是O(1)*,即常数复杂度,但需要额外的步骤来计算哈希值(对于整数来说这是微不足道的...) - MartinStettner
据我所知,它是作为树实现的,因此 log(n) 是正确的。 - Baju
1
@Baju: @Martin: Ruby的hash是哈希映射(这就是为什么对象需要定义一个哈希方法并且它们被称为hash,而不是tree)。在平均情况下,哈希查找的复杂度是常数级别的,在最坏情况下是线性的(当哈希中所有键具有相同的哈希值时)。 - sepp2k
1
请查看:https://dev59.com/rnNA5IYBdhLWcg3wk--A - Garfield

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