Singing the Praises of Benchmark
So, I wanted a way to know whether the contents of a String
had been modified. Something like:
1 2 3 4 |
s = "hello world" assert !s.dirty? s << " and goodbye!" assert s.dirty? |
A few ideas came to mind, and I found myself gravitating towards one of them, but I began to wonder which one would perform the best. Again, I had my suspicions, but I wrote up a benchmark anyway. And I am certainly glad I did because my intuition was way off.
One trick I’d used before, where I had an object instance that I wanted to enhance, was to put the enhancement code in a module and then use Object#extend
to add the module to the instance in question. Something like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
module DirtyDetector METHODS = [:<<, :"[]=", :capitalize!, :chomp!, :chop!, :concat, :delete!, :downcase!, :gsub!, :insert, :lstrip!, :next!, :replace, :reverse!, :rstrip!, :slice!, :squeeze!, :strip!, :sub!, :succ!, :swapcase!, :tr!, :tr_s!, :upcase!] def self.extend_object(base) me = class <<base; self; end METHODS.each do |method| me.class_eval { alias_method :"unprotected_#{method}", :"#{method}" } end super end def dirty? @dirty == true end METHODS.each do |method| define_method(method) do |*args| @dirty = true send("unprotected_#{method}", *args) end end end s = "hello world" s.extend DirtyDetector s << " and goodbye!" assert s.dirty? |
This works (more or less-I haven’t actually tested the above code as I’m giving it to you). However, the whole “class_eval” thing in -it seemed like quite a few invocations. I wondered if it would be more efficient to collect the #extend_object
made me a little uneasyalias_method
calls into a big string and then eval them all at once:
1 2 3 4 5 6 7 8 9 |
ALIASES = METHODS.inject("") do |aliases, method| aliases << "alias_method :\"unprotected_#{method}\", :\"#{method}\"\n" end def self.extend_object(base) me = class <<base; self; end me.class_eval ALIASES super end |
This was actually 50% slower than the other! It was obvious, once I saw the results, though—each eval has to parse the string again, which is not going to be an inexpensive operation. So, I wondered, could I find a way to pre-compile the string?
1 2 3 4 5 6 7 |
ALIASER = eval "Proc.new do\n#{ALIASES}\nend" def self.extend_object(base) me = class <<base; self; end me.class_eval &ALIASER super end |
That was much better, roughly twice as fast—and even faster than the first version. But it was still an order of magnitude slower than doing without the DirtyDetector. Having to alias each of those methods and add the new functionality to the instance is just not a very practical way to go about this. But what other option was there?
Perhaps you saw the solution before I even started into this article. Bear with me, sometimes it takes a while before I can see the obvious, but I’ll get there in the end.
I considered using Delegator, but I wanted the new instance to still be identifiable as a String (that is to say, String === s
should still be true). The solution was to simply extend String and override the methods in question in the subclass:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class DirtyDetectorString < String METHODS = [:<<, :"[]=", :capitalize!, :chomp!, :chop!, :concat, :delete!, :downcase!, :gsub!, :insert, :lstrip!, :next!, :replace, :reverse!, :rstrip!, :slice!, :squeeze!, :strip!, :sub!, :succ!, :swapcase!, :tr!, :tr_s!, :upcase!] METHODS.each do |method| define_method(method) do |*args| @dirty = true super end end def dirty? @dirty == true end end s = DirtyDetectorString.new("hello world") assert !s.dirty? s << " and goodbye!" assert s.dirty? |
And the best news? It performed nearly as fast as the native string operations! Thank goodness for the benchmark library.