为什么 == 比 eql 更快?

16

我在String类的文档中读到,eql?是一个严格的等值运算符,不进行类型转换,而==是一个等值运算符,它尝试将其第二个参数转换为字符串,并且这些方法的C源代码证实了这一点:

eql?的源代码:

static VALUE
rb_str_eql(VALUE str1, VALUE str2)
{
    if (str1 == str2) return Qtrue;
    if (TYPE(str2) != T_STRING) return Qfalse;
    return str_eql(str1, str2);
}

==的源代码:

VALUE
rb_str_equal(VALUE str1, VALUE str2)
{
    if (str1 == str2) return Qtrue;
    if (TYPE(str2) != T_STRING) {
        if (!rb_respond_to(str2, rb_intern("to_str"))) {
            return Qfalse;
        }
        return rb_equal(str2, str1);
    }
    return str_eql(str1, str2);
}

然而,当我尝试对这些方法进行基准测试时,我惊讶地发现 ==eql? 快多达 20%! 我的基准测试代码如下:

require "benchmark"

RUN_COUNT = 100000000
first_string = "Woooooha"
second_string = "Woooooha"

time = Benchmark.measure do
  RUN_COUNT.times do |i|
    first_string.eql?(second_string)
  end
end
puts time

time = Benchmark.measure do
  RUN_COUNT.times do |i|
    first_string == second_string
  end
end
puts time

结果如下:

Ruby 1.9.3-p125:

26.420000   0.250000  26.670000 ( 26.820762)
21.520000   0.200000  21.720000 ( 21.843723)

Ruby 1.9.2-p290:

->

Ruby 1.9.2-p290:

25.930000   0.280000  26.210000 ( 26.318998)
19.800000   0.130000  19.930000 ( 19.991929)

那么,有人可以解释一下为什么更简单的eql?方法在我用它比较两个相似字符串时比==方法慢吗?


3
微基准测试并不容易,有很多因素可能会影响测试的结果。在开始基准测试前,您确定处理器已经处于最高速度吗?您尝试过改变基准测试的顺序吗?您曾经尝试过多次进行基准测试,并在每一次测试中交替使用 ==eql? 吗?最终,如果 C 代码正确的话,eql? 应该比 == 更快。 - mliebelt
2
我能够确认这些结果。尝试交换顺序,尝试在两者之间交替等等。结果非常一致,“==”似乎比“eql?”更快。 - robbrit
@mliebelt,我同意你的观点,即eql?必须比==更快或者至少不会更慢,但是我尝试过改变基准测试的顺序,结果还是一样。我没有尝试每次交换==eql?,你能提供这种基准测试的示例吗? - sharipov_ru
哎呀,我不惊讶你一年多来都没有得到答案。这个问题看起来很简单,但实际上非常难!很高兴我偶然发现了这个问题。 - Marc-André Lafortune
3个回答

4
您看到差异的原因与==eql?的实现无关,而是因为Ruby在可能的情况下优化运算符(例如 == )以避免正常方法查找。

我们可以通过以下两种方式验证此内容:

  • == 创建别名并调用它。 您将获得与eql?类似的结果,因此比 == 慢。

  • 使用send : == send : eql? 进行比较,您将获得类似的时间; 速度差异消失,因为Ruby只会对直接调用运算符使用优化,而不是使用 send __send__

这是显示两者的代码:

require 'fruity'
first = "Woooooha"
second = "Woooooha"
class String
  alias same_value? ==
end

compare do
  with_operator   { first == second }
  with_same_value { first.same_value? second }
  with_eql        { first.eql? second }
end

compare do
  with_send_op    { first.send :==, second }
  with_send_eql   { first.send :eql?, second }
end

结果:

with_operator is faster than with_same_value by 2x ± 0.1
with_same_value is similar to with_eql
with_send_eql is similar to with_send_op

如果您是好奇的,那么运算符的优化在insns.def中。
注意:本答案仅适用于Ruby MRI,如果在JRuby / rubinius等其他版本中存在速度差异,我会感到惊讶。

2
equal? is reference equality
== is value equality
eql? is value and type equality

第三种方法,eql? 通常用于测试两个对象是否具有相同的值和类型。例如:
puts "integer == to float: #{25 == 25.0}"
puts "integer eql? to float: #{25.eql? 25.0}"

gives:

Does integer == to float: true
Does integer eql? to float: false

我认为,由于eql?进行了更多的检查,因此它会更慢,在字符串方面至少在我的Ruby 1.93上是如此。所以我想这一定是与类型有关,并进行了一些测试。 当整数和浮点数进行比较时,eql?稍微快一些。当整数进行比较时,==要快得多,直到x2。错误的理论,回到起点。

下一个理论:如果两个值是相同类型的,则使用其中之一会更快,如果它们是相同类型,==始终更快,eql?在类型不同时更快,同样也是直到x2。

没有时间比较所有类型,但我确定你会得到不同的结果,尽管相同类型的比较总是会产生类似的结果。有人能证明我错吗?

以下是我从OP测试中得出的结果:

 16.863000   0.000000  16.863000 ( 16.903000) 2 strings with eql?
 14.212000   0.000000  14.212000 ( 14.334600) 2 strings with ==
 13.213000   0.000000  13.213000 ( 13.245600) integer and floating with eql?
 14.103000   0.000000  14.103000 ( 14.200400) integer and floating with ==
 13.229000   0.000000  13.229000 ( 13.410800) 2 same integers with eql?
  9.406000   0.000000   9.406000 (  9.410000) 2 same integers with ==
 19.625000   0.000000  19.625000 ( 19.720800) 2 different integers with eql?
  9.407000   0.000000   9.407000 (  9.405800) 2 different integers with ==
 21.825000   0.000000  21.825000 ( 21.910200) integer with string with eql?
 43.836000   0.031000  43.867000 ( 44.074200) integer with string with ==

所以我认为由于 eql? 进行更多的检查,所以会更慢 - 但事实并非如此;请查看 OP 发布的 C 源代码(或在 rubydoc.org 上查找:== vs. eql?)。 - Abe Voelker
我知道Abe,编辑一下我的答案以使其更清晰。只是根据文档形成了一个理论并进行了测试,结果被证明是错误的,也许是因为你提到的原因。我的第二个理论仍然成立,直到有反证。我同意这并没有真正回答“为什么”,但这是一个开始,不是吗? - peter
好的,我并不是想听起来像个混蛋,只是想指出C源代码与==和eql?的假设不匹配。 - Abe Voelker
顺便提一下,Integer#eql?String#eql?之间没有关系,但你可能会遇到类似的奇怪速度差异。我的答案解释了原因。 - Marc-André Lafortune

2

在进行基准测试时,不要使用times,因为这会创建一个闭包RUN_COUNT次。由此产生的额外时间在绝对意义上同样影响所有基准测试,但这使得更难以注意到相对差异:

require "benchmark"

RUN_COUNT = 10_000_000
FIRST_STRING = "Woooooha"
SECOND_STRING = "Woooooha"

def times_eq_question_mark
  RUN_COUNT.times do |i|
    FIRST_STRING.eql?(SECOND_STRING)
  end
end

def times_double_equal_sign
  RUN_COUNT.times do |i|
    FIRST_STRING == SECOND_STRING
  end
end

def loop_eq_question_mark
  i = 0
  while i < RUN_COUNT
    FIRST_STRING.eql?(SECOND_STRING)
    i += 1
  end
end

def loop_double_equal_sign
  i = 0
  while i < RUN_COUNT
    FIRST_STRING == SECOND_STRING
    i += 1
  end
end

1.upto(10) do |i|
  method_names = [:times_eq_question_mark, :times_double_equal_sign, :loop_eq_question_mark, :loop_double_equal_sign]
  method_times = method_names.map {|method_name| Benchmark.measure { send(method_name) } }
  puts "Run #{i}"
  method_names.zip(method_times).each do |method_name, method_time|
    puts [method_name, method_time].join("\t")
  end
  puts
end

提供

Run 1
times_eq_question_mark    3.500000   0.000000   3.500000 (  3.578011)
times_double_equal_sign   2.390000   0.000000   2.390000 (  2.453046)
loop_eq_question_mark     3.110000   0.000000   3.110000 (  3.140525)
loop_double_equal_sign    2.109000   0.000000   2.109000 (  2.124932)

Run 2
times_eq_question_mark    3.531000   0.000000   3.531000 (  3.562386)
times_double_equal_sign   2.469000   0.000000   2.469000 (  2.484295)
loop_eq_question_mark     3.063000   0.000000   3.063000 (  3.109276)
loop_double_equal_sign    2.109000   0.000000   2.109000 (  2.140556)

Run 3
times_eq_question_mark    3.547000   0.000000   3.547000 (  3.593635)
times_double_equal_sign   2.437000   0.000000   2.437000 (  2.453047)
loop_eq_question_mark     3.063000   0.000000   3.063000 (  3.109275)
loop_double_equal_sign    2.140000   0.000000   2.140000 (  2.140557)

Run 4
times_eq_question_mark    3.547000   0.000000   3.547000 (  3.578011)
times_double_equal_sign   2.422000   0.000000   2.422000 (  2.437422)
loop_eq_question_mark     3.094000   0.000000   3.094000 (  3.140524)
loop_double_equal_sign    2.140000   0.000000   2.140000 (  2.140557)

Run 5
times_eq_question_mark    3.578000   0.000000   3.578000 (  3.671758)
times_double_equal_sign   2.406000   0.000000   2.406000 (  2.468671)
loop_eq_question_mark     3.110000   0.000000   3.110000 (  3.156149)
loop_double_equal_sign    2.109000   0.000000   2.109000 (  2.156181)

Run 6
times_eq_question_mark    3.562000   0.000000   3.562000 (  3.562386)
times_double_equal_sign   2.407000   0.000000   2.407000 (  2.468671)
loop_eq_question_mark     3.109000   0.000000   3.109000 (  3.124900)
loop_double_equal_sign    2.125000   0.000000   2.125000 (  2.234303)

Run 7
times_eq_question_mark    3.500000   0.000000   3.500000 (  3.546762)
times_double_equal_sign   2.453000   0.000000   2.453000 (  2.468671)
loop_eq_question_mark     3.031000   0.000000   3.031000 (  3.171773)
loop_double_equal_sign    2.157000   0.000000   2.157000 (  2.156181)

Run 8
times_eq_question_mark    3.468000   0.000000   3.468000 (  3.656133)
times_double_equal_sign   2.454000   0.000000   2.454000 (  2.484296)
loop_eq_question_mark     3.093000   0.000000   3.093000 (  3.249896)
loop_double_equal_sign    2.125000   0.000000   2.125000 (  2.140556)

Run 9
times_eq_question_mark    3.563000   0.000000   3.563000 (  3.593635)
times_double_equal_sign   2.453000   0.000000   2.453000 (  2.453047)
loop_eq_question_mark     3.125000   0.000000   3.125000 (  3.124900)
loop_double_equal_sign    2.141000   0.000000   2.141000 (  2.156181)

Run 10
times_eq_question_mark    3.515000   0.000000   3.515000 (  3.562386)
times_double_equal_sign   2.453000   0.000000   2.453000 (  2.453046)
loop_eq_question_mark     3.094000   0.000000   3.094000 (  3.140525)
loop_double_equal_sign    2.109000   0.000000   2.109000 (  2.156181)

我同意微基准测试很困难,但只要使用eachwhile并保持一致,就不会改变事物的相对速度。无论如何,我已经给出了为什么存在速度差异的答案。 - Marc-André Lafortune

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