This is a story of that time I banged my head against the table.
I was writing a procedure for finding out the number of times the method String#size was called during the runtime of a snippet. When the snippet was run using Rubinius, it resulted in a count of n+1 where n was the result under MRI (both 2.0). After fiddling with the code for a bit, I realized that the “require” call in Rubinius internally calls the method String#size. I forgot that much of Rubinius is written in Ruby.
Run 1
The following is a simpler version of the call counter I implemented.
# counter.rb
# For readability, we will have the incrementer code inside
# a lambda. More improvements, if made in the future, will
# confined to this block
CALL_COUNT_INCREMENT = lambda do
@counter ||= 0
@counter += 1
end
# For now, we'll hard-code the method we want to inspect.
method_to_be_checked = "String#size"
# The modification we make to the String#size code where
# we shove in the call to the CALL_COUNT_INCREMENT lambda
class String
alias_method :old_size, :size
def size
CALL_COUNT_INCREMENT.call
self.old_size
end
end
# This file is 'required' before the snippet we want to measure.
# We will use the at_exit {} block to specify the number of times
# the method was called during the runtime.
at_exit {
puts "String#size was called #{@counter} times"
} The snippet I wanted to measure was in another file and the above counter.rb file was required in the snippet file:
# snippet.rb
require_relative "checker.rb"
(1..100000).each do |x|
x.to_s.size if x.odd?
end When the file is run using ruby snippet.rb, it results in the following output:
MRI: String#size was called 50000 times.
rbx: String#size was called 50001 times. Note that the snippet has nothing to do with threading-related errors.
After some digging around and blaming the interpolation (#{}) literal initially, I later realized I was wrong. I added some print statements here and there and realized that the @counter was getting incremented right before the (1..100000).each {} block.
Run 2
Instead of including the counter.rb file, I copied the contents and pasted them into snippet.rb and removed the require clause. Boom!
MRI: String#size was called 50000 times.
rbx: String#size was called 50000 times. Just as it should be.
Are we there yet?
I did see a call to #size method in the puts method and to test the effect of that call, I added another call to puts inside the at_exit{} block like so:
at_exit {
puts "String#size was called #{@counter} times"
puts "String#size was called #{@counter} times"
} Output:
(Rubinius)
String#size was called 50000 times
String#size was called 50001 times (MRI)
String#size was called 50000 times
String#size was called 50000 times In Rubinius, IO#puts has a call to the String#suffix? method which has a call to String#size and so, our counter is incremented after the first puts call. Refer to Rubinius to the IO and String classes.
So, what’s the point?
I always wondered why tools like ruby-prof or rblineprof were unavailable for Ruby implementations other than MRI and how difficult it would be to port those to other implementations. Now I guess the above behaviour can be cited as one reason. I now realize that performance monitoring applications are not at all straight-forward and that these kinds of subtleties should be kept in mind.
Also, this is fun.