Ruby の Range#include? を使うよりも Range#cover? を使うほうが高速になることがある

次のように DateRange では #include? よりも #cover? の方が高速で動作します。

require 'benchmark'
require "date"

range = (Date.parse("2020/01/01")..Date.parse("2021/01/01"))
date = Date.parse("2020/05/01")

Benchmark.bm(20) do |b|
  b.report('Date include?') { 10_000.times { range.include?(date) } }
  b.report('Date cover?') { 10_000.times { range.cover?(date) } }
end
__END__
                           user     system      total        real
Date include?          0.720505   0.000000   0.720505 (  0.720700)
Date cover?            0.001932   0.000000   0.001932 (  0.001932)

なぜパフォーマンスに差が出るのか

これは Range#include?Range を『離散値』として扱うのに対して Range#cover? は『連続値』として扱うためです。
内部的な挙動でいうと Range#include?Enumerable#include? を呼び出しており線形的に値を探査します。
一方で Range#cover? は始端と終端を <=> で比較しているだけなのでより高速に動作します。
なので Range が『連続値』であることが保証されているのであれば Range#cover? を使ったほうがより高速に動作します。

require 'benchmark'
require "date"

range = (Date.parse("2020/01/01")..Date.parse("2021/01/01"))
date = Date.parse("2020/05/01")

Benchmark.bm(20) do |b|
  b.report('Date include?') { 10_000.times { range.include?(date) } }
  # Range#include? は実質 Enumerable#include? と同じ
  b.report('Date each.include?') { 10_000.times { range.each.include?(date) } }
end
__END__
                           user     system      total        real
Date include?          0.730970   0.000163   0.731133 (  0.731358)
Date each.include?     0.743971   0.000008   0.743979 (  0.744230)

ちなみに Range#include?Range#cover? で挙動が違うケースもあるので注意しましょう。

p ("a" .. "c").include?("ba") # => false
p ("a" .. "c").cover?("ba")   # => true
# これは
#  "a" <=> "ba" # => -1
#  "c" <=> "ba" # => 1
# となるため "a" .. "c" の範囲に含まれてしまう

Range が数値だった場合は?

ちなみに Range の要素が数値の場合は #include?#cover? と同等の動きがするのでパフォーマンス的な懸念点はありません。

require 'benchmark'
require "date"

range = (Date.parse("2020/01/01")..Date.parse("2021/01/01"))
date = Date.parse("2020/05/01")

Benchmark.bm(20) do |b|
  b.report('Integer include?') { 10_000.times { (1..1000).include?(500) } }
  b.report('Integer cover?') { 10_000.times { (1..1000).cover?(500) } }
end
__END__
                           user     system      total        real
Integer include?       0.001725   0.000000   0.001725 (  0.001724)
Integer cover?         0.001854   0.000000   0.001854 (  0.001855)

参照