One Danger of Freedom Patches
May 21, 2013 @ 3:04 pmI 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.