I posted a benchmark on twitter about comparing a DateTime with a string.
This is a short blurrrggghhh post about the benchmark and why there is such a
performance discrepancy.

Here is the benchmark:

require 'benchmark/ips'
require 'active_support/all' if ENV['AS']
require 'date'

now = DateTime.now

Benchmark.ips do |x|
  x.report("lhs") { now == "foo" }
  x.report("rhs") { "foo" == now }
end

First we’ll run the benchmark without Active Support, then we’ll run the
benchmark with Active Support.

The Benchmarks

Without Active Support

[aaron@higgins rails (master)]$ bundle exec ruby argh.rb 
Calculating -------------------------------------
                 lhs     57389 i/100ms
                 rhs     76222 i/100ms
-------------------------------------------------
                 lhs  2020064.6 (±14.7%) i/s -    9870908 in   5.015172s
                 rhs  3066573.4 (±13.2%) i/s -   15091956 in   5.012879s
[aaron@higgins rails (master)]$

With Active Support

[aaron@higgins rails (master)]$ AS=1 bundle exec ruby argh.rb 
Calculating -------------------------------------
                 lhs      4786 i/100ms
                 rhs     26327 i/100ms
-------------------------------------------------
                 lhs    62858.4 (±23.6%) i/s -     296732 in   5.019005s
                 rhs  2866546.6 (±26.6%) i/s -   13031865 in   4.996482s
[aaron@higgins rails (master)]$

Numbers!

In the benchmarks without Active Support, the performance is fairly close. The
standard deviation is pretty big, but the numbers are within the ballpark of
each other.

In the benchmarks with Active Support, the difference is enormous. It’s
not even close. Why is this?

What is the deal?

This speed difference is due to a Freedom Patch
that Active Support applies to the DateTime
class
:

class DateTime
  # Layers additional behavior on DateTime#<=> so that Time and
  # ActiveSupport::TimeWithZone instances can be compared with a DateTime.
  def <=>(other)
    super other.to_datetime
  end
end

DateTime includes the Comparable module which will call the <=> method
whenever you call the == method. This Freedom Patch calls to_datetime on
whatever is on the right hand side of the comparison. Rails Monkey Patches the
String class to add a to_datetime
method
,
but “foo” is not a valid Date, so it raises an exception.

The Comparable module rescues any exception that happens inside
<=>
and returns
false. This means that any time you call DateTime#== with something that
doesn’t respond to to_datetime, an exception is raised and immediately thrown
away.

The original implementation just does object equality comparisons, returns
false, and it’s done. This is why the original implementation is so much
faster than the implementation with the Freedom Patch.

My 2 Cents

These are the dangers of Freedom Patching. As a Rails Core member, I know this
is a controversial opinion, but I am not a fan of Freedom Patching. It seems
convienient until one day you wonder why is this code:

date == "foo"

so much slower than this code?

"foo" == date

Freedom Patching hides complexity behind a familiar syntax. It flips your
world upside down; making code that seems reasonable do something unexpected.
When it comes to code, I do not like the unexpected.

EDIT: I clarified the section about strings raising an exception. The
actual exception occurrs in another monkeypatch in Rails.

Read more at the source