The maze book for programmers!
mazesforprogrammers.com

Algorithms, circle mazes, hex grids, masking, weaving, braiding, 3D and 4D grids, spheres, and more!

DRM-Free Ebook

The Buckblog

assorted ramblings by Jamis Buck

Singing the Praises of Benchmark

10 June 2005 — 3-minute read

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 #extend_object made me a little uneasy-it seemed like quite a few 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:

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.