Benchmark::Sweet

Time tends not to be consistent across multiple runs, but numbers of queries or the number of objects allocated tend to be more similar.

This gem allows the user to collect all three of these benchmarks using a single framework similar to the benchmark and benchmark-ips syntax.

Sometimes a benchmark needs to be collected across multiple runs using different gem versions or using different ruby versions. This can be done as well.

Lastly, this allows multiple axes of comparisons to be performed. Example: instead of measuring multiple split implementations, it allows measuring these implementations using empty strings and long strings so a bigger picture can be obtained.

Installation

Add this line to your application's Gemfile:

gem 'benchmark-sweet'

And then execute:

$ bundle

Or install it yourself as:

$ gem install benchmark-sweet

Example 1

Code

require "benchmark/sweet"
require "active_support/all"

NSTRING = nil
DELIMITER='/'.freeze
STRING="ab/cd/ef/gh".freeze

Benchmark.items(metrics: %w(ips memsize)) do |x|
  x. data: "nil" do
    x.report("to_s.split") { NSTRING.to_s.split(DELIMITER)           }
    x.report("?split:[]")  { NSTRING ? NSTRING.split(DELIMITER) : [] }
  end
  x. data: "str" do
    x.report("to_s.split") { STRING.to_s.split(DELIMITER)          }
    x.report("?split:[]")  { STRING ? STRING.split(DELIMITER) : [] }
  end

  # partition the data by data value (nil vs string)
  # that way we're not comparing a split on a nil vs a split on a populated string
  compare_by :data

  # each row is a different method (via `row: :method`)
  # each column is by data type (via `column: :data` - specified via `metadata data: "nil"`)
  x.report_with grouping: :metric, sort: true, row: :method, column: :data
end

compare_by

The code takes a different amount of time to process a nil vs a string. So the values are given metadata with the appropriate data used (i.e.: metadata data: "nil"). The benchmark is then told that the results need to be partitioned by data (i.e.: compare_by :data).

The multipliers are only comparing values with the same data value and do not compare "string" values with "nil" values.

Values for method labels (e.g.: "to_s.split") and metrics (e.g.: ips) are already part of the partition.

grouping

In this example, each metric is considered distinct so each metric is given a unique table (i.e.: grouping: :metric)

Metrics of ips and memsize are calculated (i.e.: metrics: %w(ips memsize))

row

A different method is given per row (i.e. row: :method)

column

The other axis for the columns is the type of data passed (i.e.: column: :data) This is not a native value, it is specified when the items are specified (e.g.:metadata data: "nil")

example output

metric ips

method nil str
?split:[] 7090539.2 i/s 1322010.9 i/s
to_s.split 3703981.6 i/s - 1.91x 1311153.9 i/s

metric memsize

method nil str
?split:[] 40.0 bytes 360.0 bytes
to_s.split 80.0 bytes - 2.00x 360.0 bytes

Example 2

Code

require "benchmark/sweet"
require "active_support/all"

NSTRING = nil
DELIMITER='/'.freeze
STRING="ab/cd/ef/gh".freeze

Benchmark.items(metrics: %w(ips memsize), memory: 3, warmup: 1, time: 1, quiet: false, force: ENV["FORCE"] == "true") do |x|
  x. version: RUBY_VERSION
  x. data: "nil" do
    x.report("to_s.split") { NSTRING.to_s.split(DELIMITER)           }
    x.report("?split:[]")  { NSTRING ? NSTRING.split(DELIMITER) : [] }
  end
  x. data: "str" do
    x.report("to_s.split") { STRING.to_s.split(DELIMITER)          }
    x.report("?split:[]")  { STRING ? STRING.split(DELIMITER) : [] }
  end

  # partition the data by ruby version and data present
  # that way we're not comparing a split on a nil vs a split on a populated string
  x.compare_by :version, :data
if ENV["CONDENSED"].to_s == "true"
  x.report_with grouping: :version, sort: true, row: :method, column: [:data, :metric]
else
  x.report_with grouping: [:version, :metric], sort: true, row: :method, column: :data
end

  x.save_file $PROGRAM_NAME.sub(/\.rb$/, '.json')
end

save_file

Creates a json save file which saves the timings across multiple runs. This is used along with the version metadata to record different results per ruby version. Another common use is to record ActiveRecord version or a gem's version.

This is run with two different versions of ruby. Interim values are stored in the save_file.

Running with environment variable FORCE will force running this again. (i.e.: force: ENV["force"] == true)

Depending upon the environment variable CONDENSED, there are two types of output.

compare_by

We introduce version as metadata for the tests. Adding version to the comparison says that we should only compare values for the same version of ruby (along with the same data).

If you note version and data are not reserved words, instead, they are just what metadata we decided to pass in.

Example 2 output

[:version, :metric] 2.3.7_ips

method nil str
?split:[] 10146134.2 i/s 1284159.2 i/s
to_s.split 4232772.3 i/s - 2.40x 1258665.8 i/s

[:version, :metric] 2.3.7_memsize

method nil str
?split:[] 40.0 bytes 360.0 bytes
to_s.split 80.0 bytes - 2.00x 360.0 bytes

[:version, :metric] 2.4.6_ips

method nil str
?split:[] 10012873.4 i/s 1377320.5 i/s
to_s.split 4557456.3 i/s - 2.20x 1350562.6 i/s

[:version, :metric] 2.4.6_memsize

method nil str
?split:[] 40.0 bytes 360.0 bytes
to_s.split 80.0 bytes - 2.00x 360.0 bytes

[:version, :metric] 2.5.5_ips

method nil str
?split:[] 7168109.1 i/s 1357046.0 i/s
to_s.split 3779969.3 i/s - 1.90x 1328072.4 i/s

[:version, :metric] 2.5.5_memsize

method nil str
?split:[] 40.0 bytes 360.0 bytes
to_s.split 80.0 bytes - 2.00x 360.0 bytes

Example 2 CONDENSED output

running with CONDENSED=true calls with a different report_with

As you might notice, the number of objects created for the runs are the same. But for some reason, the nil split case is slower for ruby 2.5.5.

version 2.3.7

method nil_ips str_ips nil_memsize str_memsize
?split:[] 10146134.2 i/s 1284159.2 i/s 40.0 bytes 360.0 bytes
to_s.split 4232772.3 i/s - 2.40x 1258665.8 i/s 80.0 bytes - 2.00x 360.0 bytes

version 2.4.6

method nil_ips str_ips nil_memsize str_memsize
?split:[] 10012873.4 i/s 1377320.5 i/s 40.0 bytes 360.0 bytes
to_s.split 4557456.3 i/s - 2.20x 1350562.6 i/s 80.0 bytes - 2.00x 360.0 bytes

version 2.5.5

method nil_ips str_ips nil_memsize str_memsize
?split:[] 7168109.1 i/s 1357046.0 i/s 40.0 bytes 360.0 bytes
to_s.split 3779969.3 i/s - 1.90x 1328072.4 i/s 80.0 bytes - 2.00x 360.0 bytes

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/kbrock/benchmark-sweet.

License

The gem is available as open source under the terms of the MIT License.