Showing posts with label benchmarking. Show all posts
Showing posts with label benchmarking. Show all posts

Thursday, March 24, 2011

Ruby and Protocol Buffers, Take One

At work, we're moving from XML to protocol buffers.  While we're mostly a Java shop, the operations/sysadmin team I'm on does a lot of Ruby. I was interested in how we might use the same technology for some of our stuff. After a bit of looking, I found two libraries that looked mature enough to investigate:

ruby-protobuf, by MATSUYAMA Kengo (@macks_jp), was straightforward to install and use.  It has a good online tutorial and the redme has all I needed to get started.
ruby-protocol-buffers, by Brian Palmer was also easy to install and use.  It seems a bit lacking in the online documentation, but does have some examples to follow.  (If Brian's name rings a bell, it might be because I interviewed him some time ago about winning a programming contest sponsored by Mozy's former incarnation.)
I started out with a very simple proto file:

package bench;

message Person {
  required string name = 1;
  required int32 id = 2;
  optional string email = 3;
}

I compiled this with rprotoc for ruby-protobuf and with ruby-protoc for ruby-protocol-buffers. This generated the following (which I edited lightly).  For ruby-protof:

### Generated by rprotoc. DO NOT EDIT!
### <proto file: bench.proto>
# package bench;
#
# message Person {
#   required string name = 1;
#   required int32 id = 2;
#   optional string email = 3;
# }
require 'protobuf/message/message'
require 'protobuf/message/enum'
require 'protobuf/message/service'
require 'protobuf/message/extend'

module Bench1
  class Person1 < ::Protobuf::Message
    defined_in __FILE__
    required :string, :name, 1
    required :int32, :id, 2
    optional :string, :email, 3
  end
end
for ruby-protocol-buffer
#!/usr/bin/env ruby
# Generated by the protocol buffer compiler. DO NOT EDIT!

require 'protocol_buffers'

# Reload support
Object.__send__(:remove_const, :Bench2) if defined?(Bench2)

module Bench2
  # forward declarations
  class Person2 < ::ProtocolBuffers::Message; end

  class Person2 < ::ProtocolBuffers::Message
    required :string, :name, 1
    required :int32, :id, 2
    optional :string, :email, 3

    gen_methods! # new fields ignored after this point
  end
end

Then I pulled out the statistical benchmarking I wrote about a while ago (Since no one else has taken the bait, maybe I should bundle up a gem for that.).  Instead of quoting the whole thing at you, here are the pertinent loops.  For ruby-protobuf:

msg = Bench1::Person1.new(:name => idx.to_s,
                          :id => idx,
                          :email => idx.to_s)
msg_str = msg.serialize_to_string
msg == msg.parse_from_string(msg_str)

for ruby-protocol-buffer:

msg = Bench2::Person2.new(:name => idx.to_s,
                          :id => idx,
                          :email => idx.to_s)
 msg == Bench2::Person2.parse(msg.to_s)
And here are the results:

$ rvm 1.9.2
$ ruby -v
ruby 1.9.2p0 (2010-08-18 revision 29036) [i686-linux]
$ time ruby ProtoBufBench
testing ruby-protobuff against ruby-protocol-buffer
The deviation in the deltas was 0.021731
The mean delta was 0.198301
max = 0.241761921640842 :: min = 0.154839326146633
ruby-protocol-buffer was better

real 1m50.672s
user 1m50.599s
sys 0m0.092s

$ rvm 1.8.7
$ ruby -v
ruby 1.8.7 (2010-08-16 patchlevel 302) [i686-linux]
$ time ruby ProtoBufBench
testing ruby-protobuff against ruby-protocol-buffer
The deviation in the deltas was 0.009414
The mean delta was -2.205984
max = -2.18715483485056 :: min = -2.22481263341116
There's no statistical difference

real 3m8.131s
user 3m7.996s
sys 0m0.056s

I didn't try compiling the c extension for ruby-protocol-buffers, and I haven't tried any more involved .proto files yet.  I'll work on those in the next couple of days and post results as I see them.

Thursday, December 04, 2008

Benchmarking and Refactoring

This blog post is pulled from the Profiling and Optimizing Ruby tutorial I wrote for IBM DeveloperWorks a couple of years ago. If you'd like to reuse it, please note that the initial publication at DeveloperWorks must be attributed. I've reposted it here so that people can access it without signing up for a DeveloperWorks account.

Another section of this tutorial, Benchmarking Makes it Better was posted yesterday, it might be worth reading first if you don't have a good handle on benchmarking already.

If you're not comfortable with profiling Ruby Code, you might want to look at some of my other articles on Profiling:

The original tutorial used the following "shell_grabber.rb" script as example code:


file = "/etc/passwd"
File.open(file).each do |line|
  if line.match('/bin/bash') then
    print line.split(':')[0]
    puts " uses bash"
  end
end

Following the profiler

Now that you've seen profiling and benchmarking at work, it's time to put it all together. Back in listing 5 () you saw the profiling results of profiling our shell_grabber.rb script. The first five lines provide the biggest opportunities for optimization, so I've repeated them in


  %   cumulative   self              self     total
 time   seconds   seconds    calls  ms/call  ms/call  name
 42.55     0.20      0.20        1   200.00   470.00  IO#each
 21.28     0.30      0.10      690     0.14     0.20  Kernel.puts
 10.64     0.35      0.05     2070     0.02     0.02  IO#write
  8.51     0.39      0.04      690     0.06     0.07  Kernel.print
  6.38     0.42      0.03     1242     0.02     0.04  String#match

We've got two different places to try to improve our code; the use of a print and a puts in the same block (lines 4 and 5 in listing 12) which take up .69 seconds and 1380 total calls in our profiling run, and the construction of our match method which we call 1242 times (using .42 seconds). If we can clean either of these up, it will represent a win. If we can get both — so much the better.

Since these are two separate refactorings, we'll walk through them individually. Both follow the same pattern though:

  • Isolate the code you want to change
  • Benchmark your options
  • Make the change (if appropriate)
  • Test your new version
  • Check in your changes
In this tutorial, you actually get to cheat a little bit. Since this example only has one method, there's only a very small isolation step to worry about.

Cleaning up your match method

One way to explain the pain you're feeling here is that line.match('/bin/bash') is being repeated on each iteration. Ruby allows you to build a Regex object once then refer to it multiple times later. Listing 13 shows a benchmarking script which tests the impact of building our Regex prior to instead of during our loop. Here's what the benchmarking code would look like:


require 'benchmark'

n = 1000
file = "fixture"

Benchmark.bmbm(25) do |x|

x.report("build regex in block") do
    for i in 1..n do
      File.open(file).each do |line|
        if line.match('/bin/bash') then
          $stderr.print line.split(':')[0]
          $stderr.puts " uses /bin/bash"
        end
      end
    end
  end

  x.report("build regex prior") do
    for i in 1..n do
      re = /\/bin\/bash/
      File.open(file).each do |line|
        if line.match(re) then
          $stderr.print line.split(':')[0]
          $stderr.puts "uses /bin/bash"
        end
      end
    end
  end

end

Here are the results:


$ ./re_bench.rb 2> /dev/null
Rehearsal ------------------------------------------------------------
build regex in block      17.610000   0.820000  18.430000 ( 18.692166)
build regex prior          5.100000   0.340000   5.440000 (  5.465884)
-------------------------------------------------- total: 23.870000sec

                               user     system      total        real
build regex in block      17.550000   0.790000  18.340000 ( 18.421562)
build regex prior          5.170000   0.340000   5.510000 (  5.514125)

There's a pretty obvious win (over a 70% reduction in running time) by moving the algorithm out of the loop. This is a simple change to make:


file = "/etc/passwd"
File.open(file).each do |line|
  re = /\/bin\/bash/
  if line.match(re) then
    print line.split(':')[0]
    puts " uses bash"
  end
end

Having made the change, we need to test it to verify that everything works as planned. The simplest way to test a script this short is to verify the output. In this case we can run the output of the original script and of our new version through diff to ensure that they're the same. diff orig_output new_output exits with a return value of 0, meaning that there's no difference between the output of our two versions. For any real code, you really want to run a real test suite written with Test::Unit or Rspec.

While it's not strictly necessary, we can also look at the real time reductions in our new version. Using time with both versions of the script shows 0m0.064s of real time for the original version and 0m0.046s of real time for the new version — not quite the 70% reduction we saw in the benchmark, but a healthy change nonetheless. (Don't worry too much about not seeing the full improvement, time results can have hidden impacts from all manner of system activity — see also my blog post Benchmarking, Lies, and Statistics.)

Once you've run your tests and timed the results again, you should check it into your source control system so that you can revert if you later find a better way to do things.

Another Approach

It turns out that there's another way to improve the performance of your regex handling. Using the match method turns out to be slower than the using =~, but don't take my word for it. Let's benchmark it. A good benchmarking script is here:


require 'benchmark'

n = 1000
file = "fixture"


Benchmark.bmbm(25) do |x|

  x.report("build regex in block") do
    for i in 1..n do
      File.open(file).each do |line|
        if line.match('/bin/bash') then
          $stderr.print line.split(':')[0]
          $stderr.puts " uses /bin/bash"
        end
      end
    end
   end
  
   x.report("build regex prior") do
      for i in 1..n do
        re = %r{/bin/bash}
        File.open(file).each do |line|
          if line.match(re) then
            $stderr.print #{line.split(':')[0]
            $stderr.puts "uses /bin/bash"
          end
        end
      end
    end
  
    x.report("use =~ pattern") do
      for i in 1..n do
        File.open(file).each do |line|
          if line =~ /\/bin\/bash/ then
            $stderr.print #{line.split(':')[0]
            $stderr.puts "uses /bin/bash"
          end
      end
    end
  end
end

And the results are:


Rehearsal ------------------------------------------------------------
build regex in block      17.480000   0.470000  17.950000 ( 17.997961)
build regex prior          4.980000   0.330000   5.310000 (  5.322441)
use =~ pattern             4.230000   0.300000   4.530000 (  4.629864)
-------------------------------------------------- total: 27.790000sec

                               user     system      total        real
build regex in block      17.590000   0.480000  18.070000 ( 18.620850)
build regex prior          5.090000   0.310000   5.400000 (  5.498786)
use =~ pattern             4.300000   0.310000   4.610000 (  4.652404)

Our first change represents a 70% improvement over the original, but our new change is a 75% improvement according to the benchmark — a change worth making. Since you're keeping everything in some kind of source control, your first step will be to revert your last change. Then you can change the script to match this:


file = "fixture"
File.open(file).each do |line|
  if line =~ /\/bin\/bash/ then
    print line.split(':')[0]
    puts " uses bash"
  end
end

Once the changes have been made, you can go back and verify that the scripts generate the same output using diff as shown previously. Once they pass, you can check your new code in and move on.

Printing Once

We have two issues to consider with the way we're printing lines in our script. First, we we've got two distinct printing calls. Second we're using puts which adds the overhead of adding a \n to every line that doesn't already have one. Before we start making changes, let's do some benchmarking. Here's a banchmarking script:


require 'benchmark'

n = 50_000

Benchmark.bmbm(15) do |x|

  x.report("original version") do
    for i in 1..n do
      $stderr.print i, "and"
      $stderr.puts i
    end
  end

  x.report("single puts") do
    for i in 1..n do
     $stderr.puts "#{i} and #{i}"
    end
  end

  x.report("single print") do
    for i in 1..n do
     $stderr.print "#{i} and #{i}\n"
    end
  end

end

And here are the results:


$ ./print_benchmark.rb 2> /dev/null
Rehearsal ----------------------------------------------------
original version   0.370000   0.050000   0.420000 (  0.443343)
single puts        0.210000   0.020000   0.230000 (  0.240232)
single print       0.170000   0.020000   0.190000 (  0.181233)
------------------------------------------- total: 0.840000sec

                       user     system      total        real
original version   0.230000   0.030000   0.260000 (  0.272866)
single puts        0.220000   0.020000   0.240000 (  0.239319)
single print       0.170000   0.010000   0.180000 (  0.180624)
$

Both single printing call versions show an improvement over the original two printing calls version of the code, but the print version is significantly better than the puts version (a 33% improvement versus a 12% improvement). That makes your decision easy, the new version of the script (using a single print method) is shown below:


file = "fixture"
File.open(file).each do |line|
  if line =~ /\/bin\/bash/ then
    print "#{line.split(':')[0]} uses bash\n"
  end
end

Again, run your tests so that you know you've got a good version. time let's us see just how well our optimization worked. In this case, we got 0m0.046s for the previous version and 0m0.026s for our new version — a 40% speedup.

Refactoring Roundup

In the course of this post, we've made a reverted one change, and made two others. Our code is a bit more than 10% smaller, and runs much faster. Although you don't have unit tests, you've been able to verify your output using a functional test (and thus verified your code quality) at every step of the way, so you also know that you've not introduced any new errors.

While the script that you've been working on is small, you'll follow the same methodology on bigger programs.

First, make sure that your spending your time wisely. Going through a lot of work to speed up a script that takes under a second to run is probably not worth your time for a command line tool, but if you're building a program that will be part of a heavily used website even nano-seconds may count. (You'll need to make sure you've got enough iterations to be able to measure differences above the nano-second scale though, since any variation that small is liable to be caused by outside factors.) time and profile are the tools you'll rely on here.

Second, isolate the code you want to change. In our case, the isolation was pretty simple, but when you're working with an object with multiple public and private methods it will be a bit more difficult. There aren't really any tools to help with this, it all comes down to reading the profiler output and knowing your code.

Third, benchmark your options. Once you've identified and isolated the code your going to change, you can move it (perhaps with small changes) into a benchmarking script. Once you have benchmarking output you can make informed decisions about how to change your code.

Fourth, make the changes indicated by profiling and benchmarking. Now that you know what works the best, you can transplant that back into your actual code.

Fifth, test your new code. Always test your code. Test your code before every commit. Unit tests make this better and easier for almost all of your code, but be ready to roll your own tests in the odd cases that it doesn't.

Sixth, check in your changes. Revision control is incredibly important, it will save you at some point.

Wednesday, December 03, 2008

Benchmarking Makes it Better

This blog post is pulled from the Profiling and Optimizing Ruby tutorial I wrote for IBM DeveloperWorks a couple of years ago. If you'd like to reuse it, please note that the initial publication at DeveloperWorks must be attributed. I've reposted it here so that people can access it without signing up for a DeveloperWorks account.

The original tutorial used the following "shell_grabber.rb" script as example code:


file = "/etc/passwd"
File.open(file).each do |line|
  if line.match('/bin/bash') then
    print line.split(':')[0]
    puts " uses bash"
  end
end

A Benchmarking Primer

Before you can start replacing code, you should have an idea about how your intended replacement performs. The benchmark library is the tool you'll want to use for this. It's simple to use, you'll just write a benchmarking script that includes the methods you want to compare. The Benchmark class provides three instance methods to make this easier; bm, bmbm, and measure.

The bm method provides basic interface to the benchmark method. It takes an optional label_width argument. Each method tested can have a label specified. You define individual methods to be tested as shown here:


require 'benchmark'
n = 5_000_000
Benchmark.bm(15) do |x|
  x.report("for loop:")   { for i in 1..n; a = "1"; end }
  x.report("times:")      { n.times do   ; a = "1"; end }
  x.report("upto:")       { 1.upto(n) do ; a = "1"; end }
end

The bmbm method runs your benchmarking code twice, to help avoid complications from garbage collection. The first run is called the rehearsal, and is reported as part of the results. bmbm methods are defined in the same way that bm methods are. The only thing you'd need to change from listing 6 to use bmbm instead of bm methods is the call itself. To use the bmbm method, you'd write Benchmark.bmbm(15) do |x|.

Simple timing information can also be gathered using the measure method. (measure is used behind the scenes by the bm and bmbm methods to create their reports.) measure returns a Benchmark::Tms object that can be converted to a string for printing. A Ruby script using Benchmark#measure to time the construction of a string is shown below:


require 'benchmark'
puts Benchmark.measure { "foo" << "bar" << "baz" }

Comparing Code With benchmark.rb

Now that you've seen the basics of benchmarking, let's put it to use. We've already used time and profile.rb to look at the performance of our shell_grabber.rb script. Just pull out the part that does the work and put it into a new script like this.


require 'benchmark'
Benchmark.bm(25) do |x|
  x.report("original code") { 
    File.open("/etc/passwd").each do |line|
      if line.match('/bin/bash') then
        $stderr.print line.split(':')[0]
        $stderr.puts " uses /bin/bash"
      end
    end
  }
end

There's a change to the original code that are worth discussing. Instead of using bare print and puts statements, they're replaced with $stderr.print and $stderr.puts. You don't want to clutter the screen with hundreds or thousands of lines of output. This has the potential to dramaticly affect the timing of your code, so if you make a change like this to one method in your benchmark you'll want to make the same change to all of them. Take a look at just how much of a difference doing a $stdout.puts instead of a puts can make:


$stdout.puts  0.320000   0.040000   0.360000 (  0.396455)
naked puts    0.200000   0.020000   0.220000 (  0.262174)

Using Benchmarking Results

Using benchmarking results will help you choose which algorithm or which idiom will give you the best results. It's important that you don't throw off your results with improper tests though. For example, the benchmarking script shown above with the for, upto, and times methods only measured the performance of different forms of iteration. In this case it really doesn't matter what we do inside the iteration, as long as it's the same for each version. The output from that code modified to use Benchmark#bmbm is shown here:


Rehearsal --------------------------------------------------
for:             2.210000   0.030000   2.240000 (  2.288663)
times:           2.570000   0.060000   2.630000 (  2.623878)
upto:            2.530000   0.050000   2.580000 (  2.650115)
----------------------------------------- total: 7.450000sec

                     user     system      total        real
for:             2.080000   0.050000   2.130000 (  2.148350)
times:           2.590000   0.010000   2.600000 (  2.743801)
upto:            2.550000   0.030000   2.580000 (  2.588629)

Reading the output, you can see that there's a small but noticeable difference between the upto and times constructions, but that the real win comes with the for version — in this case, nearly a 20% improvement.

Notes

The importance of large datasets and/or multiple benchmarking runs was underscored while I was writing this section. In my initial version of the script, I only iterated through the loop 50,000 times. Then, when I ran the results the first time, the upto version of the iterator ran the fastest. When I moved up to 5,000,000 times through the loop and ran it a couple more times, I ended up hitting a consistent result of the for loop being better.

Dropping the number of iterations down to 5,000 continued to yield the best results for the for loop version in the non-rehearsal run, but would occasionally show the upto version as faster. Dropping down to 500 iterations showed near random results. In these cases, it appears that the time required for the run was so small (0.000606 seconds in one run) that external processes had an overwhelming influence on the benchmark.

Another dirty secret of benchmarking is that you'll need to do some extra work to ensure that your results are statistically valid. Please see my blog post Benchmarking, Lies, and Statistics for more details about this.

Finally, tune in tomorrow for a follow-up post on Benchmarking and Refactoring.

Monday, December 04, 2006

Benchmarking, Lies, and Statistics

Work on my book hasn't been going as well as I'd wanted (it's amazing how procrastination and laziness can throw you off a schedule). Recently, I've been doing a little bit better though. Last week I was working on some material on benchmarking Ruby code, and suddenly I started to channel Zed.

I realized that in my discussion of how to use the Benchmark library, I was completely ignoring all of Zed's great advice about applying statistics to performance testing. I really wanted to get that advice in front of anyone reading my book, so I wrote this little sidebar (below).

When I was done with it, I realized two things. First, I wanted to get the information out to a wider audience sooner, rather than later; and second, I think there's an opportunity for an enterprising tool writer to take the idea (and sample code) below and create a tremendously useful benchmarking tool. I talked with my editor, and he agreed to let me post the sidebar here — that takes care of the first realization. Hopefully the lazyweb will take care of the second.

I don't have a deep background in statistics, so I relied on a lot of advice while writing this (any mistakes are certainly mine though). If you think I'm all wet, I'd love to collect some feedback. Please drop a comment here explaining why you think the concept, method, or math needs work. (Of course, if you want to say something nice about it, I certainly won't mind.)

Statistics and Benchmarking

It's important to note (but beyond the scope of this book to do much more than that) that running a single benchmark of one method against another is not sufficient to determine whether or not you've really made a performance improvement. Zed Shaw has talked about this at length.

A quick way to do this is to run a series of tests for your original and new code and calculate the deltas between the results. Then you can calculate a mean and the standard deviation to see if 0 is in the range of the mean +/- 2 times the standard deviation.

The Benchmark library actually makes it fairly easy to build something like this. Here's a quick-n-dirty example:


require 'benchmark'
iterations = 10
loops = 100_000
deltas = []

def mean(ary)
  ary.inject(0) { |sum, i| sum += i }/ary.length.to_f 
end
def std_dev(ary, mean)
  Math.sqrt( (ary.inject(0) { |dev, i| 
                dev += (i - mean) ** 2}/ary.length.to_f) )
end

iterations.times do
  original_result = Benchmark.realtime do
    1.upto(loops) { |num| num+1 }
  end
  new_result = Benchmark.realtime do
    for num in (1..loops) do num+1 end
  end
  deltas << (original_result - new_result)
end

deviation = std_dev(deltas, mean(deltas))
mean_delta = mean(deltas) 

printf "The deviation in the deltas was %f\n", deviation
printf "The mean delta was %f\n", mean_delta
max = mean_delta + 2.0 * deviation
min = mean_delta - 2.0 * deviation
if min <= 0 || 0 >= max
 puts "There's no statistical difference"
end

With this example, you can see a more accurate picture of the performance differences between the for and upto methods:


The deviation in the deltas was 0.005296
The mean delta was 0.006870
There's no statistical difference

For more information (and better tests) go cuddle up with a good book on statistics because my brain hurts from doing this much.