This post originated from an RSS feed registered with Ruby Buzz
by Jamis Buck.
Original Post: Singing the Praises of Benchmark
Feed Title: the buckblogs here
Feed URL: http://weblog.jamisbuck.org/blog.cgi/programming/index.rss
Feed Description: Jamis Buck's corner of the blogging universe. Mostly about ruby, but includes ramblings on a variety of topics.
So, I wanted a way to know whether the contents of a String had been modified. Something like:
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:
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 #extend_object made a little uneasy—seemed like a lot of invocations. I wondered if it would be more efficient to collect the alias_method calls into a big string and then eval them all at once:
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?
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:
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 as fast as the native string operations! Thank goodness for the benchmark library.