benchmark.rb   [plain text]


#
# benchmark.rb
#
=begin
= benchmark.rb

== NAME
((*benchmark.rb*)) - a benchmark utility

== SYNOPSIS
  ----------
       require "benchmark"
       include Benchmark
  ----------

== DESCRIPTION 

benchmark.rb provides some utilities to measure and report the
times used and passed to execute.  

== SIMPLE EXAMPLE

=== EXAMPLE 0
To ((<measure>)) the times to make (({"a"*1_000_000})):

  ----------
       puts measure{ "a"*1_000_000 }
  ----------

On my machine (FreeBSD 3.2 on P5100MHz) this reported as follows:

  ----------
         1.166667   0.050000   1.216667 (  0.571355)
  ----------

The above shows user time, system time, user+system, and really passed
time.  The unit of time is second.

=== EXAMPLE 1
To do some experiments sequentially, ((<bm>)) is useful:

  ----------
       n = 50000
       bm do |x|
         x.report{for i in 1..n; a = "1"; end}
         x.report{n.times do   ; a = "1"; end}
         x.report{1.upto(n) do ; a = "1"; end}
       end
  ----------

The result:
  ----------
             user     system      total        real
         1.033333   0.016667   1.016667 (  0.492106)
         1.483333   0.000000   1.483333 (  0.694605)
         1.516667   0.000000   1.516667 (  0.711077)
  ----------

=== EXAMPLE 2
To put a label in each ((<report>)):

  ----------
       n = 50000
       bm(7) do |x|
         x.report("for:")   {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 option (({7})) specifies the offset of each report accoding to the
longest label.

This reports as follows:

  ----------
                    user     system      total        real
       for:     1.050000   0.000000   1.050000 (  0.503462)
       times:   1.533333   0.016667   1.550000 (  0.735473)
       upto:    1.500000   0.016667   1.516667 (  0.711239)
  ----------

=== EXAMPLE 3

By the way, benchmarks might seem to depend on the order of items.  It
is caused by the cost of memory allocation and the garbage collection.
To prevent this boresome, Benchmark::((<bmbm>)) is provided, e.g., to
compare ways for sort array of strings:

  ----------
       require "rbconfig"
       include Config
       def file
         open("%s/lib/ruby/%s.%s/tk.rb" % 
              [CONFIG['prefix'],CONFIG['MAJOR'],CONFIG['MINOR']]).read
       end

       n = 10
       bmbm do |x|
         x.report("destructive!"){ 
           t = (file*n).to_a; t.each{|line| line.upcase!}; t.sort!
         }
         x.report("method chain"){ 
           t = (file*n).to_a.collect{|line| line.upcase}.sort
         }
       end
  ----------

This reports:

  ----------
       Rehearsal ------------------------------------------------
       destructive!   2.664062   0.070312   2.734375 (  2.783401)
       method chain   5.257812   0.156250   5.414062 (  5.736088)
       --------------------------------------- total: 8.148438sec
       
                          user     system      total        real
       destructive!   2.359375   0.007812   2.367188 (  2.381015)
       method chain   3.046875   0.023438   3.070312 (  3.085816)
  ----------

=== EXAMPLE 4
To report statistics of sequential experiments with unique label,
((<benchmark>)) is available:

  ----------
       n = 50000
       benchmark(" "*7 + CAPTION, 7, FMTSTR, ">total:", ">avg:") do |x|
         tf = x.report("for:")  {for i in 1..n; a = "1"; end}
         tt = x.report("times:"){n.times do   ; a = "1"; end}
         tu = x.report("upto:") {1.upto(n) do ; a = "1"; end}
         [tf+tt+tu, (tf+tt+tu)/3]
       end
  ----------

The result:

  ----------
                    user     system      total        real
       for:     1.016667   0.016667   1.033333 (  0.485749)
       times:   1.450000   0.016667   1.466667 (  0.681367)
       upto:    1.533333   0.000000   1.533333 (  0.722166)
       >total:  4.000000   0.033333   4.033333 (  1.889282)
       >avg:    1.333333   0.011111   1.344444 (  0.629761)
  ----------

== Benchmark module

=== CONSTANT
:CAPTION
  CAPTION is a caption string which is used in Benchmark::((<benchmark>)) and 
  Benchmark::Report#((<report>)). 
:FMTSTR
  FMTSTR is a format string which is used in Benchmark::((<benchmark>)) and 
  Benchmark::Report#((<report>)). See also Benchmark::Tms#((<format>)). 
:BENCHMARK_VERSION
  BENCHMARK_VERSION is version string which statnds for the last modification
  date (YYYY-MM-DD). 

=== INNER CLASS
* ((<Benchmark::Job>))
* ((<Benchmark::Report>))
* ((<Benchmark::Tms>))

=== MODULE FUNCTION
==== benchmark
  ----------
       benchmark([caption [, label_width [, fmtstr]]]) do |x| ... end
       benchmark([caption [, label_width [, fmtstr]]]) do array_of_Tms end
       benchmark([caption [, label_width [, fmtstr [, labels...]]]]) do 
         ...
         array_of_Tms
       end
  ----------

(({benchmark})) reports the times. In the first form the block variable x is
treated as a ((<Benchmark::Report>)) object, which has ((<report>)) method.
In the second form, each member of array_of_Tms is reported in the
specified form if the member is a ((<Benchmark::Tms>)) object.  The
last form provides combined above two forms (See ((<EXAMPLE 3>))). 

The following lists the meaning of each option. 

:caption
 A string ((|caption|)) is printed once before execution of the given block. 

:label_width
 An integer ((|label_width|)) is used as an offset in each report. 

:fmtstr
 An string ((|fmtstr|)) is used to format each measurement. 
 See ((<format>))

:labels
 The rest parameters labels is used as prefix of the format to the
 value of block, that is array_of_Tms.

==== bm
  ----------
       bm([label_width [, labels ...]) do ... end
  ----------

(({bm})) is a simpler interface of ((<benchmark>)). 
(({bm})) acts as same as follows:

  benchmark(" "*label_width + CAPTION, label_width, FMTSTR, *labels) do 
    ... 
  end

==== bmbm
  ----------
       bmbm([label_width]) do |x|
         x.item("label1") { .... } 
         ....
       end
  ----------

(({bmbm})) is yet another ((<benchmark>)).  This utility function is
provited to prevent a kind of job order dependency, which is caused
by memory allocation and object creation.  The usage is similar to
((<bm>)) but has less options and does extra three things:

  (1) ((*Rehearsal*)): runs all items in the job ((<list>)) to allocate
      enough memory.
  (2) ((*GC*)): before each ((<measure>))ment, invokes (({GC.start})) to
      prevent the influence of previous job. 
  (3) If given ((|label_width|)) is less than the maximal width of labels
      given as ((|item|))'s argument, the latter is used.  
      Because (({bmbm})) is a 2-pass procedure, this is possible. 

(({bmbm})) returns an array which consists of Tms correspoding to each
(({item})). 
==== measure 
  ----------
       measure([label]) do ... end
  ----------

measure returns the times used and passed to execute the given block as a
Benchmark::Tms object. 

==== realtime
  ----------
       realtime do ... end
  ----------

realtime returns the times passed to execute the given block. 

== Benchmark::Report

=== CLASS METHOD

==== Benchmark::Report::new(width)
  ----------
       Benchmark::Report::new([width [, fmtstr]])
  ----------

Usually, one doesn't have to use this method directly, 
(({Benchmark::Report::new})) is called by ((<benchmark>)) or ((<bm>)). 
((|width|)) and ((|fmtstr|)) are the offset of ((|label|)) and 
format string responsively; Both of them are used in ((<format>)). 

=== METHOD

==== report

  ----------
       report(fmt, *args)
  ----------

This method reports label and time formated by ((|fmt|)).  See
((<format>)) of Benchmark::Tms for formatting rule.

== Benchmark::Tms

=== CLASS METHOD

== Benchmark::Job

=== CLASS METHOD

==== Benchmark::Job::new
  ----------
       Benchmark::Job::new(width)
  ----------

Usually, one doesn't have to use this method directly,
(({Benchmark::Job::new})) is called by ((<bmbm>)). 
((|width|)) is a initial value for the offset ((|label|)) for formatting. 
((<bmbm>)) passes its argument ((|width|)) to this constructor. 

=== METHOD

==== item
  ----------
       item(((|lable|))){ .... }
  ----------

(({item})) registers a pair of (((|label|))) and given block as job ((<list>)). 
==== width

Maximum length of labels in ((<list>)) plus one.  

==== list

array of array which consists of label and jop proc. 

==== report

alias to ((<item>)).

==== Benchmark::Tms::new
  ----------
       Benchmark::Tms::new([u [, s [, cu [, cs [, re [, l]]]]]])
  ----------

returns new Benchmark::Tms object which has
((|u|))  as ((<utime>)), 
((|s|))  as ((<stime>)), 
((|cu|)) as ((<cutime>))
((|cs|)) as ((<cstime>)), 
((|re|)) as ((<real>)) and
((|l|))  as ((<label>)). 

The default value is assumed as 0.0 for ((|u|)), ((|s|)), ((|cu|)),
((|cs|)) and ((|re|)). The default of ((|l|)) is null string ((({""}))). 

==== operator +
  ----------
       tms1 + tms2
  ----------

returns a new Benchmark::Tms object as memberwise summation. 
This method and ((<(('operator /'))>)) is useful to take statistics. 

==== operator /
  ----------
       tms / num
  ----------

returns a new Benchmark::Tms object as memberwise division by ((|num|)). 
This method and ((<operator +>)) is useful to take statistics. 

==== add
  ----------
       add do ... end
  ----------

returns a new Benchmark::Tms object which is the result of additional
execution which is given by block. 

==== add!
  ----------
       add! do ... end
  ----------

do additional execution which is given by block. 

==== format
  ----------
       format([fmtstr [, *args]])
  ----------

(({format})) returns formatted string of (({self})) according to a
((|fmtstr|)) like (({Kernel::format})). In addition, (({format})) accepts
some extentions as follows:
  :%u
    ((<utime>)).
  :%y
    ((<stime>)). (Mnemonic: y of ``s((*y*))stem'')
  :%U
    ((<cutime>)). 
  :%Y
    ((<cstime>)). 
  :%t
    ((<total>)). 
  :%r
    ((<real>)). 
  :%n
    ((<label>)). (Mnemonic: n of ``((*n*))ame'')

If fmtstr is not given ((<FMTSTR>)) is used as default value. 

==== utime

returns user time. 

==== stime

returns system time. 

==== cutime

returns user time of children. 

==== cstime

returns system time of children. 

==== total

returns total time, that is 
((<utime>)) + ((<stime>)) + ((<cutime>)) + ((<cstime>)). 

==== real

returns really passed time. 

==== label

returns label. 

==== to_a

returns a new array as follows

  [label, utime, stime, cutime, cstime, real]

==== to_s

same as (({format()})). See also ((<format>)).

== HISTORY

A benchmark.rb appeared in RAA January 1st 2000. 

== AUTHOR

Gotoken (gotoken@notwork.org). 
=end

module Benchmark
  BENCHMARK_VERSION = "2002-04-25"

  def Benchmark::times()
      Process::times()
  end

  def benchmark(caption = "", label_width = nil, fmtstr = nil, *labels)
    sync = STDOUT.sync
    STDOUT.sync = true
    label_width ||= 0
    fmtstr ||= FMTSTR
    raise ArgumentError, "no block" unless iterator?
    print caption
    results = yield(Report.new(label_width, fmtstr))
    Array === results and results.grep(Tms).each {|t|
      print((labels.shift || t.label || "").ljust(label_width), 
	    t.format(fmtstr))
    }
    STDOUT.sync = sync
  end

  def bm(label_width = 0, *labels)
    benchmark(" "*label_width + CAPTION, label_width, FMTSTR, *labels){|*x|yield(*x)}
  end

  def bmbm(width = 0)
    job = Job.new(width)
    yield(job)
    width = job.width
    sync = STDOUT.sync
    STDOUT.sync = true

    # rehearsal
    print "Rehearsal "
    puts '-'*(width+CAPTION.length - "Rehearsal ".length)
    list = []
    job.list.each{|label,item|
      print(label.ljust(width))
      res = Benchmark::measure(&item)
      print res.format()
      list.push res
    }
    sum = Tms.new; list.each{|i| sum += i}
    ets = sum.format("total: %tsec")
    printf("%s %s\n\n",
	   "-"*(width+CAPTION.length-ets.length-1), ets)
    
    # take
    print ' '*width, CAPTION
    list = []
    ary = []
    job.list.each{|label,item|
      GC::start
      print label.ljust(width)
      res = Benchmark::measure(&item)
      print res.format()
      ary.push res
      list.push [label, res]
    }

    STDOUT.sync = sync
    ary
  end

  def measure(label = "")
    t0, r0 = Benchmark.times, Time.now
    yield
    t1, r1 = Benchmark.times, Time.now
    Benchmark::Tms.new(t1.utime  - t0.utime, 
		       t1.stime  - t0.stime, 
		       t1.cutime - t0.cutime, 
		       t1.cstime - t0.cstime, 
		       r1.to_f - r0.to_f,
		       label)
  end

  def realtime(&blk)
    Benchmark::measure(&blk).real
  end

  class Job
    def initialize(width)
      @width = width
      @list = []
    end

    def item(label = "", &blk)
      raise ArgmentError, "no block" unless block_given?
      label.concat ' '
      w = label.length
      @width = w if @width < w
      @list.push [label, blk]
      self
    end

    alias report item
    attr_reader :list, :width
  end

  module_function :benchmark, :measure, :realtime, :bm, :bmbm

  class Report
    def initialize(width = 0, fmtstr = nil)
      @width, @fmtstr = width, fmtstr
    end

    def item(label = "", *fmt, &blk)
      print label.ljust(@width)
      res = Benchmark::measure(&blk)
      print res.format(@fmtstr, *fmt)
      res
    end

    alias report item
  end

  class Tms
    CAPTION = "      user     system      total        real\n"
    FMTSTR = "%10.6u %10.6y %10.6t %10.6r\n"

    attr_reader :utime, :stime, :cutime, :cstime, :real, :total, :label

    def initialize(u = 0.0, s = 0.0, cu = 0.0, cs = 0.0, real = 0.0, l = nil)
      @utime, @stime, @cutime, @cstime, @real, @label = u, s, cu, cs, real, l
      @total = @utime + @stime + @cutime + @cstime
    end

    def add(&blk)
      self + Benchmark::measure(&blk) 
    end

    def add!
      t = Benchmark::measure(&blk) 
      @utime  = utime + t.utime
      @stime  = stime + t.stime
      @cutime = cutime + t.cutime
      @cstime = cstime + t.cstime
      @real   = real + t.real
      self
    end

    def +(x); memberwise(:+, x) end
    def -(x); memberwise(:-, x) end
    def *(x); memberwise(:*, x) end
    def /(x); memberwise(:/, x) end

    def format(arg0 = nil, *args)
      fmtstr = (arg0 || FMTSTR).dup
      fmtstr.gsub!(/(%[-+\.\d]*)n/){"#{$1}s" % label}
      fmtstr.gsub!(/(%[-+\.\d]*)u/){"#{$1}f" % utime}
      fmtstr.gsub!(/(%[-+\.\d]*)y/){"#{$1}f" % stime}
      fmtstr.gsub!(/(%[-+\.\d]*)U/){"#{$1}f" % cutime}
      fmtstr.gsub!(/(%[-+\.\d]*)Y/){"#{$1}f" % cstime}
      fmtstr.gsub!(/(%[-+\.\d]*)t/){"#{$1}f" % total}
      fmtstr.gsub!(/(%[-+\.\d]*)r/){"(#{$1}f)" % real}
      arg0 ? Kernel::format(fmtstr, *args) : fmtstr
    end

    def to_s
      format
    end

    def to_a
      [@label, @utime, @stime, @cutime, @cstime, @real]
    end

    protected
    def memberwise(op, x)
      case x
      when Benchmark::Tms
	Benchmark::Tms.new(utime.__send__(op, x.utime),
			   stime.__send__(op, x.stime),
			   cutime.__send__(op, x.cutime),
			   cstime.__send__(op, x.cstime),
			   real.__send__(op, x.real)
			   )
      else
	Benchmark::Tms.new(utime.__send__(op, x),
			   stime.__send__(op, x),
			   cutime.__send__(op, x),
			   cstime.__send__(op, x),
			   real.__send__(op, x)
			   )
      end
    end
  end

  CAPTION = Benchmark::Tms::CAPTION
  FMTSTR = Benchmark::Tms::FMTSTR
end

if __FILE__ == $0
  include Benchmark

  n = ARGV[0].to_i.nonzero? || 50000
  puts %Q([#{n} times iterations of `a = "1"'])
  benchmark("       " + CAPTION, 7, FMTSTR) do |x|
    x.report("for:")   {for i in 1..n; a = "1"; end} # Benchmark::measure
    x.report("times:") {n.times do   ; a = "1"; end}
    x.report("upto:")  {1.upto(n) do ; a = "1"; end}
  end

  benchmark do
    [
      measure{for i in 1..n; a = "1"; end},  # Benchmark::measure
      measure{n.times do   ; a = "1"; end},
      measure{1.upto(n) do ; a = "1"; end}
    ]
  end
end