【问题标题】:Why is == faster than eql?为什么 == 比 eql 快?
【发布时间】:2012-04-21 07:20:50
【问题描述】:

我在 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:

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

那么,当我为两个相似的字符串运行它时,谁能解释为什么更简单的eql? 方法比== 方法慢?

【问题讨论】:

  • 微型基准测试并不容易做到,可能有很多事情会影响您的输出。当您开始基准测试时,您确定处理器处于最新状态吗?您是否尝试过更改基准测试的顺序?您是否尝试过多次,每次在==eql? 之间切换?最后,eql? 应该比== 快(如果 C 代码是正确的)。
  • 我能够确认这些结果。尝试过切换顺序,尝试过两者交替等等。结果非常一致,== 似乎比eql? 快。
  • @mliebelt 我同意你的观点,eql? 必须比== 更快或至少不低于==,但我试图改变基准测试的顺序。结果是一样的。我没有尝试每次都切换==eql?,你能提供这种基准的例子吗?
  • 哎哟。我对你一年多没有得到答复并不感到惊讶。它看起来是一个简单的问题,但实际上非常!很高兴我偶然发现了这个问题。

标签: ruby string benchmarking equality


【解决方案1】:

您看到差异的原因==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】:

    进行基准测试时,不要使用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,只要它是一致的,就不会改变事物的相对速度。无论如何,我已经给出了为什么存在速度差异的答案。
    【解决方案3】:
    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 源代码(或在 ruby​​doc.org 上查找:==eql?)。
    • 我知道 abe,请稍微编辑一下我的答案以说明这一点。只是根据文档形成一个理论并对其进行测试,证明是错误的,可能是因为你提到的原因,我的第二个理论仍然存在直到反证,我同意它并没有真正回答为什么,但这是一个开始是吗?
    • 公平地说,我并没有试图听起来像个混蛋,只是指出 C 源代码与 == 与 eql 不匹配?假设
    • 顺便说一句,Integer#eql?String#eql? 之间没有关系,但您可能会遇到同样奇怪的速度差异。我的回答解释了原因。
    猜你喜欢
    • 2019-11-02
    • 2015-07-24
    • 2019-04-18
    • 2014-07-06
    • 2011-07-08
    • 2019-08-22
    相关资源
    最近更新 更多