├── .gitignore ├── .github ├── dependabot.yml └── workflows │ ├── test.yml │ └── push_gem.yml ├── Gemfile ├── bin ├── setup └── console ├── Rakefile ├── BSDL ├── benchmark.gemspec ├── COPYING ├── README.md ├── test └── benchmark │ └── test_benchmark.rb └── lib └── benchmark.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | Gemfile.lock 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | group :development do 6 | gem "bundler" 7 | gem "rake" 8 | gem "test-unit" 9 | end 10 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.test_files = FileList["test/**/test_*.rb"] 6 | end 7 | 8 | task :default => :test 9 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "benchmark" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | ruby-versions: 7 | uses: ruby/actions/.github/workflows/ruby_versions.yml@master 8 | with: 9 | min_version: 2.5 10 | 11 | test: 12 | needs: ruby-versions 13 | name: build (${{ matrix.ruby }} / ${{ matrix.os }}) 14 | strategy: 15 | matrix: 16 | ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }} 17 | os: [ ubuntu-latest, macos-latest, windows-latest ] 18 | exclude: 19 | - { os: macos-latest, ruby: 2.5 } 20 | - { os: windows-latest, ruby: truffleruby-head } 21 | - { os: windows-latest, ruby: truffleruby } 22 | runs-on: ${{ matrix.os }} 23 | steps: 24 | - uses: actions/checkout@v6 25 | - name: Set up Ruby 26 | uses: ruby/setup-ruby@v1 27 | with: 28 | ruby-version: ${{ matrix.ruby }} 29 | - name: Install dependencies 30 | run: bundle install 31 | - name: Run test 32 | run: rake test 33 | -------------------------------------------------------------------------------- /.github/workflows/push_gem.yml: -------------------------------------------------------------------------------- 1 | name: Publish gem to rubygems.org 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | push: 13 | if: github.repository == 'ruby/benchmark' 14 | runs-on: ubuntu-latest 15 | 16 | environment: 17 | name: rubygems.org 18 | url: https://rubygems.org/gems/benchmark 19 | 20 | permissions: 21 | contents: write 22 | id-token: write 23 | 24 | steps: 25 | - name: Harden Runner 26 | uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0 27 | with: 28 | egress-policy: audit 29 | 30 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 31 | 32 | - name: Set up Ruby 33 | uses: ruby/setup-ruby@d5126b9b3579e429dd52e51e68624dda2e05be25 # v1.267.0 34 | with: 35 | bundler-cache: true 36 | ruby-version: ruby 37 | 38 | - name: Publish to RubyGems 39 | uses: rubygems/release-gem@1c162a739e8b4cb21a676e97b087e8268d8fc40b # v1.1.2 40 | 41 | - name: Create GitHub release 42 | run: | 43 | tag_name="$(git describe --tags --abbrev=0)" 44 | gh release create "${tag_name}" --verify-tag --generate-notes 45 | env: 46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 47 | -------------------------------------------------------------------------------- /BSDL: -------------------------------------------------------------------------------- 1 | Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions 5 | are met: 6 | 1. Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | 2. Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 14 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 15 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 16 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 17 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 18 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 19 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 20 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 21 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 22 | SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /benchmark.gemspec: -------------------------------------------------------------------------------- 1 | name = File.basename(__FILE__, ".gemspec") 2 | version = ["lib", Array.new(name.count("-")+1, ".").join("/")].find do |dir| 3 | break File.foreach(File.join(__dir__, dir, "#{name.tr('-', '/')}.rb")) do |line| 4 | /^\s*VERSION\s*=\s*"(.*)"/ =~ line and break $1 5 | end rescue nil 6 | end 7 | 8 | Gem::Specification.new do |spec| 9 | spec.name = name 10 | spec.version = version 11 | spec.authors = ["Yukihiro Matsumoto"] 12 | spec.email = ["matz@ruby-lang.org"] 13 | 14 | spec.summary = %q{a performance benchmarking library} 15 | spec.description = spec.summary 16 | spec.homepage = "https://github.com/ruby/benchmark" 17 | spec.licenses = ["Ruby", "BSD-2-Clause"] 18 | 19 | spec.required_ruby_version = ">= 2.1.0" 20 | 21 | spec.metadata["homepage_uri"] = spec.homepage 22 | spec.metadata["source_code_uri"] = spec.homepage 23 | spec.metadata["changelog_uri"] = "#{spec.homepage}/releases" 24 | 25 | # Specify which files should be added to the gem when it is released. 26 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 27 | spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do 28 | `git ls-files -z 2>#{IO::NULL}`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 29 | end 30 | spec.bindir = "exe" 31 | spec.executables = [] 32 | spec.require_paths = ["lib"] 33 | end 34 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Ruby is copyrighted free software by Yukihiro Matsumoto . 2 | You can redistribute it and/or modify it under either the terms of the 3 | 2-clause BSDL (see the file BSDL), or the conditions below: 4 | 5 | 1. You may make and give away verbatim copies of the source form of the 6 | software without restriction, provided that you duplicate all of the 7 | original copyright notices and associated disclaimers. 8 | 9 | 2. You may modify your copy of the software in any way, provided that 10 | you do at least ONE of the following: 11 | 12 | a. place your modifications in the Public Domain or otherwise 13 | make them Freely Available, such as by posting said 14 | modifications to Usenet or an equivalent medium, or by allowing 15 | the author to include your modifications in the software. 16 | 17 | b. use the modified software only within your corporation or 18 | organization. 19 | 20 | c. give non-standard binaries non-standard names, with 21 | instructions on where to get the original software distribution. 22 | 23 | d. make other distribution arrangements with the author. 24 | 25 | 3. You may distribute the software in object code or binary form, 26 | provided that you do at least ONE of the following: 27 | 28 | a. distribute the binaries and library files of the software, 29 | together with instructions (in the manual page or equivalent) 30 | on where to get the original distribution. 31 | 32 | b. accompany the distribution with the machine-readable source of 33 | the software. 34 | 35 | c. give non-standard binaries non-standard names, with 36 | instructions on where to get the original software distribution. 37 | 38 | d. make other distribution arrangements with the author. 39 | 40 | 4. You may modify and include the part of the software into any other 41 | software (possibly commercial). But some files in the distribution 42 | are not written by the author, so that they are not under these terms. 43 | 44 | For the list of those files and their copying conditions, see the 45 | file LEGAL. 46 | 47 | 5. The scripts and library files supplied as input to or produced as 48 | output from the software do not automatically fall under the 49 | copyright of the software, but belong to whomever generated them, 50 | and may be sold commercially, and may be aggregated with this 51 | software. 52 | 53 | 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR 54 | IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED 55 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 56 | PURPOSE. 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Benchmark 2 | 3 | The Benchmark module provides methods for benchmarking Ruby code, giving detailed reports on the time taken for each task. 4 | 5 | ## Installation 6 | 7 | Add this line to your application's Gemfile: 8 | 9 | ```ruby 10 | gem 'benchmark' 11 | ``` 12 | 13 | And then execute: 14 | 15 | $ bundle 16 | 17 | Or install it yourself as: 18 | 19 | $ gem install benchmark 20 | 21 | ## Usage 22 | 23 | The Benchmark module provides methods to measure and report the time used to execute Ruby code. 24 | 25 | Measure the time to construct the string given by the expression "a"*1_000_000_000: 26 | 27 | ```ruby 28 | require 'benchmark' 29 | puts Benchmark.measure { "a"*1_000_000_000 } 30 | ``` 31 | 32 | On my machine (OSX 10.8.3 on i5 1.7 GHz) this generates: 33 | 34 | ``` 35 | 0.350000 0.400000 0.750000 ( 0.835234) 36 | ``` 37 | 38 | This report shows the user CPU time, system CPU time, the total time (sum of user CPU time, system CPU time, children's user CPU time, and children's system CPU time), and the elapsed real time. The unit of time is seconds. 39 | 40 | Do some experiments sequentially using the #bm method: 41 | 42 | ```ruby 43 | require 'benchmark' 44 | n = 5000000 45 | Benchmark.bm do |x| 46 | x.report { for i in 1..n; a = "1"; end } 47 | x.report { n.times do ; a = "1"; end } 48 | x.report { 1.upto(n) do ; a = "1"; end } 49 | end 50 | ``` 51 | 52 | The result: 53 | 54 | ``` 55 | user system total real 56 | 1.010000 0.000000 1.010000 ( 1.014479) 57 | 1.000000 0.000000 1.000000 ( 0.998261) 58 | 0.980000 0.000000 0.980000 ( 0.981335) 59 | ``` 60 | 61 | Continuing the previous example, put a label in each report: 62 | 63 | ```ruby 64 | require 'benchmark' 65 | n = 5000000 66 | Benchmark.bm(7) do |x| 67 | x.report("for:") { for i in 1..n; a = "1"; end } 68 | x.report("times:") { n.times do ; a = "1"; end } 69 | x.report("upto:") { 1.upto(n) do ; a = "1"; end } 70 | end 71 | ``` 72 | 73 | The result: 74 | 75 | ``` 76 | user system total real 77 | for: 1.010000 0.000000 1.010000 ( 1.015688) 78 | times: 1.000000 0.000000 1.000000 ( 1.003611) 79 | upto: 1.030000 0.000000 1.030000 ( 1.028098) 80 | ``` 81 | 82 | The times for some benchmarks depend on the order in which items are run. These differences are due to the cost of memory allocation and garbage collection. To avoid these discrepancies, the #bmbm method is provided. For example, to compare ways to sort an array of floats: 83 | 84 | ```ruby 85 | require 'benchmark' 86 | array = (1..1000000).map { rand } 87 | Benchmark.bmbm do |x| 88 | x.report("sort!") { array.dup.sort! } 89 | x.report("sort") { array.dup.sort } 90 | end 91 | ``` 92 | 93 | The result: 94 | 95 | ``` 96 | Rehearsal ----------------------------------------- 97 | sort! 1.490000 0.010000 1.500000 ( 1.490520) 98 | sort 1.460000 0.000000 1.460000 ( 1.463025) 99 | -------------------------------- total: 2.960000sec 100 | user system total real 101 | sort! 1.460000 0.000000 1.460000 ( 1.460465) 102 | sort 1.450000 0.010000 1.460000 ( 1.448327) 103 | ``` 104 | 105 | Report statistics of sequential experiments with unique labels, using the #benchmark method: 106 | 107 | ```ruby 108 | require 'benchmark' 109 | include Benchmark # we need the CAPTION and FORMAT constants 110 | n = 5000000 111 | Benchmark.benchmark(CAPTION, 7, FORMAT, ">total:", ">avg:") do |x| 112 | tf = x.report("for:") { for i in 1..n; a = "1"; end } 113 | tt = x.report("times:") { n.times do ; a = "1"; end } 114 | tu = x.report("upto:") { 1.upto(n) do ; a = "1"; end } 115 | [tf+tt+tu, (tf+tt+tu)/3] 116 | end 117 | ``` 118 | 119 | The result: 120 | 121 | ``` 122 | user system total real 123 | for: 0.950000 0.000000 0.950000 ( 0.952039) 124 | times: 0.980000 0.000000 0.980000 ( 0.984938) 125 | upto: 0.950000 0.000000 0.950000 ( 0.946787) 126 | >total: 2.880000 0.000000 2.880000 ( 2.883764) 127 | >avg: 0.960000 0.000000 0.960000 ( 0.961255) 128 | ``` 129 | 130 | ## Development 131 | 132 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 133 | 134 | 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](https://rubygems.org). 135 | 136 | ## Contributing 137 | 138 | Bug reports and pull requests are welcome on GitHub at https://github.com/ruby/benchmark. 139 | -------------------------------------------------------------------------------- /test/benchmark/test_benchmark.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'test/unit' 3 | require 'benchmark' 4 | 5 | class TestBenchmark < Test::Unit::TestCase 6 | BENCH_FOR_TIMES_UPTO = lambda do |x| 7 | n = 1000 8 | tf = x.report("for:") { for _ in 1..n; '1'; end } 9 | tt = x.report("times:") { n.times do ; '1'; end } 10 | tu = x.report("upto:") { 1.upto(n) do ; '1'; end } 11 | [tf+tt+tu, (tf+tt+tu)/3] 12 | end 13 | 14 | BENCH_FOR_TIMES_UPTO_NO_LABEL = lambda do |x| 15 | n = 1000 16 | x.report { for _ in 1..n; '1'; end } 17 | x.report { n.times do ; '1'; end } 18 | x.report { 1.upto(n) do ; '1'; end } 19 | end 20 | 21 | def labels 22 | %w[first second third] 23 | end 24 | 25 | def bench(type = :bm, *args, &block) 26 | if block 27 | Benchmark.send(type, *args, &block) 28 | else 29 | Benchmark.send(type, *args) do |x| 30 | labels.each { |label| 31 | x.report(label) {} 32 | } 33 | end 34 | end 35 | end 36 | 37 | def capture_bench_output(type, *args, &block) 38 | capture_output { bench(type, *args, &block) }.first.gsub(/[ \-]\d\.\d{6}/, ' --time--') 39 | end 40 | 41 | def test_tms_outputs_nicely 42 | assert_equal(" 0.000000 0.000000 0.000000 ( 0.000000)\n", Benchmark::Tms.new.to_s) 43 | assert_equal(" 1.000000 2.000000 10.000000 ( 5.000000)\n", Benchmark::Tms.new(1,2,3,4,5).to_s) 44 | assert_equal("1.000000 2.000000 3.000000 4.000000 10.000000 (5.000000) label", 45 | Benchmark::Tms.new(1,2,3,4,5,'label').format('%u %y %U %Y %t %r %n')) 46 | assert_equal("1.000000 2.000", Benchmark::Tms.new(1).format('%u %.3f', 2)) 47 | assert_equal("100.000000 150.000000 250.000000 (200.000000)\n", 48 | Benchmark::Tms.new(100, 150, 0, 0, 200).to_s) 49 | end 50 | 51 | def test_tms_wont_modify_the_format_String_given 52 | format = "format %u" 53 | Benchmark::Tms.new.format(format) 54 | assert_equal("format %u", format) 55 | end 56 | 57 | BENCHMARK_OUTPUT_WITH_TOTAL_AVG = <total: --time-- --time-- --time-- ( --time--) 63 | >avg: --time-- --time-- --time-- ( --time--) 64 | BENCH 65 | 66 | def test_benchmark_does_not_print_any_space_if_the_given_caption_is_empty 67 | assert_equal(<<-BENCH, capture_bench_output(:benchmark)) 68 | first --time-- --time-- --time-- ( --time--) 69 | second --time-- --time-- --time-- ( --time--) 70 | third --time-- --time-- --time-- ( --time--) 71 | BENCH 72 | end 73 | 74 | def test_benchmark_makes_extra_calculations_with_an_Array_at_the_end_of_the_benchmark_and_show_the_result 75 | assert_equal(BENCHMARK_OUTPUT_WITH_TOTAL_AVG, 76 | capture_bench_output(:benchmark, 77 | Benchmark::CAPTION, 7, 78 | Benchmark::FORMAT, ">total:", ">avg:", 79 | &BENCH_FOR_TIMES_UPTO)) 80 | end 81 | 82 | def test_bm_returns_an_Array_of_the_times_with_the_labels 83 | [:bm, :bmbm].each do |meth| 84 | capture_output do 85 | results = bench(meth) 86 | assert_instance_of(Array, results) 87 | assert_equal(labels.size, results.size) 88 | results.zip(labels).each { |tms, label| 89 | assert_instance_of(Benchmark::Tms, tms) 90 | assert_equal(label, tms.label) 91 | } 92 | end 93 | end 94 | end 95 | 96 | def test_bm_correctly_output_when_the_label_width_is_given 97 | assert_equal(<<-BENCH, capture_bench_output(:bm, 6)) 98 | user system total real 99 | first --time-- --time-- --time-- ( --time--) 100 | second --time-- --time-- --time-- ( --time--) 101 | third --time-- --time-- --time-- ( --time--) 102 | BENCH 103 | end 104 | 105 | def test_bm_correctly_output_when_no_label_is_given 106 | assert_equal(<<-BENCH, capture_bench_output(:bm, &BENCH_FOR_TIMES_UPTO_NO_LABEL)) 107 | user system total real 108 | --time-- --time-- --time-- ( --time--) 109 | --time-- --time-- --time-- ( --time--) 110 | --time-- --time-- --time-- ( --time--) 111 | BENCH 112 | end 113 | 114 | def test_bm_can_make_extra_calcultations_with_an_array_at_the_end_of_the_benchmark 115 | assert_equal(BENCHMARK_OUTPUT_WITH_TOTAL_AVG, 116 | capture_bench_output(:bm, 7, ">total:", ">avg:", 117 | &BENCH_FOR_TIMES_UPTO)) 118 | end 119 | 120 | BMBM_OUTPUT = <"a"*1_000_000_000: 24 | # 25 | # require 'benchmark' 26 | # 27 | # puts Benchmark.measure { "a"*1_000_000_000 } 28 | # 29 | # On my machine (OSX 10.8.3 on i5 1.7 GHz) this generates: 30 | # 31 | # 0.350000 0.400000 0.750000 ( 0.835234) 32 | # 33 | # This report shows the user CPU time, system CPU time, the total time 34 | # (sum of user CPU time, system CPU time, children's user CPU time, 35 | # and children's system CPU time), and the elapsed real time. The unit 36 | # of time is seconds. 37 | # 38 | # * Do some experiments sequentially using the #bm method: 39 | # 40 | # require 'benchmark' 41 | # 42 | # n = 5000000 43 | # Benchmark.bm do |x| 44 | # x.report { for i in 1..n; a = "1"; end } 45 | # x.report { n.times do ; a = "1"; end } 46 | # x.report { 1.upto(n) do ; a = "1"; end } 47 | # end 48 | # 49 | # The result: 50 | # 51 | # user system total real 52 | # 1.010000 0.000000 1.010000 ( 1.014479) 53 | # 1.000000 0.000000 1.000000 ( 0.998261) 54 | # 0.980000 0.000000 0.980000 ( 0.981335) 55 | # 56 | # * Continuing the previous example, put a label in each report: 57 | # 58 | # require 'benchmark' 59 | # 60 | # n = 5000000 61 | # Benchmark.bm(7) do |x| 62 | # x.report("for:") { for i in 1..n; a = "1"; end } 63 | # x.report("times:") { n.times do ; a = "1"; end } 64 | # x.report("upto:") { 1.upto(n) do ; a = "1"; end } 65 | # end 66 | # 67 | # The result: 68 | # 69 | # user system total real 70 | # for: 1.010000 0.000000 1.010000 ( 1.015688) 71 | # times: 1.000000 0.000000 1.000000 ( 1.003611) 72 | # upto: 1.030000 0.000000 1.030000 ( 1.028098) 73 | # 74 | # * The times for some benchmarks depend on the order in which items 75 | # are run. These differences are due to the cost of memory 76 | # allocation and garbage collection. To avoid these discrepancies, 77 | # the #bmbm method is provided. For example, to compare ways to 78 | # sort an array of floats: 79 | # 80 | # require 'benchmark' 81 | # 82 | # array = (1..1000000).map { rand } 83 | # 84 | # Benchmark.bmbm do |x| 85 | # x.report("sort!") { array.dup.sort! } 86 | # x.report("sort") { array.dup.sort } 87 | # end 88 | # 89 | # The result: 90 | # 91 | # Rehearsal ----------------------------------------- 92 | # sort! 1.490000 0.010000 1.500000 ( 1.490520) 93 | # sort 1.460000 0.000000 1.460000 ( 1.463025) 94 | # -------------------------------- total: 2.960000sec 95 | # 96 | # user system total real 97 | # sort! 1.460000 0.000000 1.460000 ( 1.460465) 98 | # sort 1.450000 0.010000 1.460000 ( 1.448327) 99 | # 100 | # * Report statistics of sequential experiments with unique labels, 101 | # using the #benchmark method: 102 | # 103 | # require 'benchmark' 104 | # include Benchmark # we need the CAPTION and FORMAT constants 105 | # 106 | # n = 5000000 107 | # Benchmark.benchmark(CAPTION, 7, FORMAT, ">total:", ">avg:") do |x| 108 | # tf = x.report("for:") { for i in 1..n; a = "1"; end } 109 | # tt = x.report("times:") { n.times do ; a = "1"; end } 110 | # tu = x.report("upto:") { 1.upto(n) do ; a = "1"; end } 111 | # [tf+tt+tu, (tf+tt+tu)/3] 112 | # end 113 | # 114 | # The result: 115 | # 116 | # user system total real 117 | # for: 0.950000 0.000000 0.950000 ( 0.952039) 118 | # times: 0.980000 0.000000 0.980000 ( 0.984938) 119 | # upto: 0.950000 0.000000 0.950000 ( 0.946787) 120 | # >total: 2.880000 0.000000 2.880000 ( 2.883764) 121 | # >avg: 0.960000 0.000000 0.960000 ( 0.961255) 122 | 123 | module Benchmark 124 | 125 | VERSION = "0.5.0" 126 | 127 | BENCHMARK_VERSION = "2002-04-25" # :nodoc: 128 | 129 | # Invokes the block with a Benchmark::Report object, which 130 | # may be used to collect and report on the results of individual 131 | # benchmark tests. Reserves +label_width+ leading spaces for 132 | # labels on each line. Prints +caption+ at the top of the 133 | # report, and uses +format+ to format each line. 134 | # (Note: +caption+ must contain a terminating newline character, 135 | # see the default Benchmark::Tms::CAPTION for an example.) 136 | # 137 | # Returns an array of Benchmark::Tms objects. 138 | # 139 | # If the block returns an array of 140 | # Benchmark::Tms objects, these will be used to format 141 | # additional lines of output. If +labels+ parameter are 142 | # given, these are used to label these extra lines. 143 | # 144 | # _Note_: Other methods provide a simpler interface to this one, and are 145 | # suitable for nearly all benchmarking requirements. See the examples in 146 | # Benchmark, and the #bm and #bmbm methods. 147 | # 148 | # Example: 149 | # 150 | # require 'benchmark' 151 | # include Benchmark # we need the CAPTION and FORMAT constants 152 | # 153 | # n = 5000000 154 | # Benchmark.benchmark(CAPTION, 7, FORMAT, ">total:", ">avg:") do |x| 155 | # tf = x.report("for:") { for i in 1..n; a = "1"; end } 156 | # tt = x.report("times:") { n.times do ; a = "1"; end } 157 | # tu = x.report("upto:") { 1.upto(n) do ; a = "1"; end } 158 | # [tf+tt+tu, (tf+tt+tu)/3] 159 | # end 160 | # 161 | # Generates: 162 | # 163 | # user system total real 164 | # for: 0.970000 0.000000 0.970000 ( 0.970493) 165 | # times: 0.990000 0.000000 0.990000 ( 0.989542) 166 | # upto: 0.970000 0.000000 0.970000 ( 0.972854) 167 | # >total: 2.930000 0.000000 2.930000 ( 2.932889) 168 | # >avg: 0.976667 0.000000 0.976667 ( 0.977630) 169 | # 170 | 171 | def benchmark(caption = "", label_width = nil, format = nil, *labels) # :yield: report 172 | sync = $stdout.sync 173 | $stdout.sync = true 174 | label_width ||= 0 175 | label_width += 1 176 | format ||= FORMAT 177 | report = Report.new(label_width, format) 178 | results = yield(report) 179 | 180 | print " " * report.width + caption unless caption.empty? 181 | report.list.each { |i| 182 | print i.label.to_s.ljust(report.width) 183 | print i.format(report.format, *format) 184 | } 185 | 186 | Array === results and results.grep(Tms).each {|t| 187 | print((labels.shift || t.label || "").ljust(label_width), t.format(format)) 188 | } 189 | report.list 190 | ensure 191 | $stdout.sync = sync unless sync.nil? 192 | end 193 | 194 | 195 | # A simple interface to the #benchmark method, #bm generates sequential 196 | # reports with labels. +label_width+ and +labels+ parameters have the same 197 | # meaning as for #benchmark. 198 | # 199 | # require 'benchmark' 200 | # 201 | # n = 5000000 202 | # Benchmark.bm(7) do |x| 203 | # x.report("for:") { for i in 1..n; a = "1"; end } 204 | # x.report("times:") { n.times do ; a = "1"; end } 205 | # x.report("upto:") { 1.upto(n) do ; a = "1"; end } 206 | # end 207 | # 208 | # Generates: 209 | # 210 | # user system total real 211 | # for: 0.960000 0.000000 0.960000 ( 0.957966) 212 | # times: 0.960000 0.000000 0.960000 ( 0.960423) 213 | # upto: 0.950000 0.000000 0.950000 ( 0.954864) 214 | # 215 | 216 | def bm(label_width = 0, *labels, &blk) # :yield: report 217 | benchmark(CAPTION, label_width, FORMAT, *labels, &blk) 218 | end 219 | 220 | 221 | # Sometimes benchmark results are skewed because code executed 222 | # earlier encounters different garbage collection overheads than 223 | # that run later. #bmbm attempts to minimize this effect by running 224 | # the tests twice, the first time as a rehearsal in order to get the 225 | # runtime environment stable, the second time for 226 | # real. GC.start is executed before the start of each of 227 | # the real timings; the cost of this is not included in the 228 | # timings. In reality, though, there's only so much that #bmbm can 229 | # do, and the results are not guaranteed to be isolated from garbage 230 | # collection and other effects. 231 | # 232 | # Because #bmbm takes two passes through the tests, it can 233 | # calculate the required label width. 234 | # 235 | # require 'benchmark' 236 | # 237 | # array = (1..1000000).map { rand } 238 | # 239 | # Benchmark.bmbm do |x| 240 | # x.report("sort!") { array.dup.sort! } 241 | # x.report("sort") { array.dup.sort } 242 | # end 243 | # 244 | # Generates: 245 | # 246 | # Rehearsal ----------------------------------------- 247 | # sort! 1.440000 0.010000 1.450000 ( 1.446833) 248 | # sort 1.440000 0.000000 1.440000 ( 1.448257) 249 | # -------------------------------- total: 2.890000sec 250 | # 251 | # user system total real 252 | # sort! 1.460000 0.000000 1.460000 ( 1.458065) 253 | # sort 1.450000 0.000000 1.450000 ( 1.455963) 254 | # 255 | # #bmbm yields a Benchmark::Job object and returns an array of 256 | # Benchmark::Tms objects. 257 | # 258 | def bmbm(width = 0) # :yield: job 259 | job = Job.new(width) 260 | yield(job) 261 | width = job.width + 1 262 | sync = $stdout.sync 263 | $stdout.sync = true 264 | 265 | # rehearsal 266 | puts 'Rehearsal '.ljust(width+CAPTION.length,'-') 267 | ets = job.list.inject(Tms.new) { |sum,(label,item)| 268 | print label.ljust(width) 269 | res = Benchmark.measure(&item) 270 | print res.format 271 | sum + res 272 | }.format("total: %tsec") 273 | print " #{ets}\n\n".rjust(width+CAPTION.length+2,'-') 274 | 275 | # take 276 | print ' '*width + CAPTION 277 | job.list.map { |label,item| 278 | GC.start 279 | print label.ljust(width) 280 | Benchmark.measure(label, &item).tap { |res| print res } 281 | } 282 | ensure 283 | $stdout.sync = sync unless sync.nil? 284 | end 285 | 286 | # 287 | # Returns the time used to execute the given block as a 288 | # Benchmark::Tms object. Takes +label+ option. 289 | # 290 | # require 'benchmark' 291 | # 292 | # n = 1000000 293 | # 294 | # time = Benchmark.measure do 295 | # n.times { a = "1" } 296 | # end 297 | # puts time 298 | # 299 | # Generates: 300 | # 301 | # 0.220000 0.000000 0.220000 ( 0.227313) 302 | # 303 | def measure(label = "") # :yield: 304 | t0, r0 = Process.times, Process.clock_gettime(Process::CLOCK_MONOTONIC) 305 | yield 306 | t1, r1 = Process.times, Process.clock_gettime(Process::CLOCK_MONOTONIC) 307 | Benchmark::Tms.new(t1.utime - t0.utime, 308 | t1.stime - t0.stime, 309 | t1.cutime - t0.cutime, 310 | t1.cstime - t0.cstime, 311 | r1 - r0, 312 | label) 313 | end 314 | 315 | # 316 | # Returns the elapsed real time used to execute the given block. 317 | # The unit of time is seconds. 318 | # 319 | # Benchmark.realtime { "a" * 1_000_000_000 } 320 | # #=> 0.5098029999935534 321 | # 322 | def realtime # :yield: 323 | r0 = Process.clock_gettime(Process::CLOCK_MONOTONIC) 324 | yield 325 | Process.clock_gettime(Process::CLOCK_MONOTONIC) - r0 326 | end 327 | 328 | # 329 | # Returns the elapsed real time used to execute the given block. 330 | # The unit of time is milliseconds. 331 | # 332 | # Benchmark.ms { "a" * 1_000_000_000 } 333 | # #=> 509.8029999935534 334 | # 335 | def ms # :yield: 336 | r0 = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) 337 | yield 338 | Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - r0 339 | end 340 | 341 | module_function :benchmark, :measure, :realtime, :ms, :bm, :bmbm 342 | 343 | # 344 | # A Job is a sequence of labelled blocks to be processed by the 345 | # Benchmark.bmbm method. It is of little direct interest to the user. 346 | # 347 | class Job # :nodoc: 348 | # 349 | # Returns an initialized Job instance. 350 | # Usually, one doesn't call this method directly, as new 351 | # Job objects are created by the #bmbm method. 352 | # +width+ is a initial value for the label offset used in formatting; 353 | # the #bmbm method passes its +width+ argument to this constructor. 354 | # 355 | def initialize(width) 356 | @width = width 357 | @list = [] 358 | end 359 | 360 | # 361 | # Registers the given label and block pair in the job list. 362 | # 363 | def item(label = "", &blk) # :yield: 364 | raise ArgumentError, "no block" unless block_given? 365 | label = label.to_s 366 | w = label.length 367 | @width = w if @width < w 368 | @list << [label, blk] 369 | self 370 | end 371 | 372 | alias report item 373 | 374 | # An array of 2-element arrays, consisting of label and block pairs. 375 | attr_reader :list 376 | 377 | # Length of the widest label in the #list. 378 | attr_reader :width 379 | end 380 | 381 | # 382 | # This class is used by the Benchmark.benchmark and Benchmark.bm methods. 383 | # It is of little direct interest to the user. 384 | # 385 | class Report # :nodoc: 386 | # 387 | # Returns an initialized Report instance. 388 | # Usually, one doesn't call this method directly, as new 389 | # Report objects are created by the #benchmark and #bm methods. 390 | # +width+ and +format+ are the label offset and 391 | # format string used by Tms#format. 392 | # 393 | def initialize(width = 0, format = nil) 394 | @width, @format, @list = width, format, [] 395 | end 396 | 397 | # 398 | # Prints the +label+ and measured time for the block, 399 | # formatted by +format+. See Tms#format for the 400 | # formatting rules. 401 | # 402 | def item(label = "", *format, &blk) # :yield: 403 | w = label.to_s.length 404 | @width = w if @width < w 405 | @list << res = Benchmark.measure(label, &blk) 406 | res 407 | end 408 | 409 | alias report item 410 | 411 | # An array of Benchmark::Tms objects representing each item. 412 | attr_reader :width, :format, :list 413 | end 414 | 415 | 416 | 417 | # 418 | # A data object, representing the times associated with a benchmark 419 | # measurement. 420 | # 421 | class Tms 422 | 423 | # Default caption, see also Benchmark::CAPTION 424 | CAPTION = " user system total real\n" 425 | 426 | # Default format string, see also Benchmark::FORMAT 427 | FORMAT = "%10.6u %10.6y %10.6t %10.6r\n" 428 | 429 | # User CPU time 430 | attr_reader :utime 431 | 432 | # System CPU time 433 | attr_reader :stime 434 | 435 | # User CPU time of children 436 | attr_reader :cutime 437 | 438 | # System CPU time of children 439 | attr_reader :cstime 440 | 441 | # Elapsed real time 442 | attr_reader :real 443 | 444 | # Total time, that is +utime+ + +stime+ + +cutime+ + +cstime+ 445 | attr_reader :total 446 | 447 | # Label 448 | attr_reader :label 449 | 450 | # 451 | # Returns an initialized Tms object which has 452 | # +utime+ as the user CPU time, +stime+ as the system CPU time, 453 | # +cutime+ as the children's user CPU time, +cstime+ as the children's 454 | # system CPU time, +real+ as the elapsed real time and +label+ as the label. 455 | # 456 | def initialize(utime = 0.0, stime = 0.0, cutime = 0.0, cstime = 0.0, real = 0.0, label = nil) 457 | @utime, @stime, @cutime, @cstime, @real, @label = utime, stime, cutime, cstime, real, label.to_s 458 | @total = @utime + @stime + @cutime + @cstime 459 | end 460 | 461 | # 462 | # Returns a new Tms object whose times are the sum of the times for this 463 | # Tms object, plus the time required to execute the code block (+blk+). 464 | # 465 | def add(&blk) # :yield: 466 | self + Benchmark.measure(&blk) 467 | end 468 | 469 | # 470 | # An in-place version of #add. 471 | # Changes the times of this Tms object by making it the sum of the times 472 | # for this Tms object, plus the time required to execute 473 | # the code block (+blk+). 474 | # 475 | def add!(&blk) 476 | t = Benchmark.measure(&blk) 477 | @utime = utime + t.utime 478 | @stime = stime + t.stime 479 | @cutime = cutime + t.cutime 480 | @cstime = cstime + t.cstime 481 | @real = real + t.real 482 | self 483 | end 484 | 485 | # 486 | # Returns a new Tms object obtained by memberwise summation 487 | # of the individual times for this Tms object with those of the +other+ 488 | # Tms object. 489 | # This method and #/() are useful for taking statistics. 490 | # 491 | def +(other); memberwise(:+, other) end 492 | 493 | # 494 | # Returns a new Tms object obtained by memberwise subtraction 495 | # of the individual times for the +other+ Tms object from those of this 496 | # Tms object. 497 | # 498 | def -(other); memberwise(:-, other) end 499 | 500 | # 501 | # Returns a new Tms object obtained by memberwise multiplication 502 | # of the individual times for this Tms object by +x+. 503 | # 504 | def *(x); memberwise(:*, x) end 505 | 506 | # 507 | # Returns a new Tms object obtained by memberwise division 508 | # of the individual times for this Tms object by +x+. 509 | # This method and #+() are useful for taking statistics. 510 | # 511 | def /(x); memberwise(:/, x) end 512 | 513 | # 514 | # Returns the contents of this Tms object as 515 | # a formatted string, according to a +format+ string 516 | # like that passed to Kernel.format. In addition, #format 517 | # accepts the following extensions: 518 | # 519 | # %u:: Replaced by the user CPU time, as reported by Tms#utime. 520 | # %y:: Replaced by the system CPU time, as reported by Tms#stime (Mnemonic: y of "s*y*stem") 521 | # %U:: Replaced by the children's user CPU time, as reported by Tms#cutime 522 | # %Y:: Replaced by the children's system CPU time, as reported by Tms#cstime 523 | # %t:: Replaced by the total CPU time, as reported by Tms#total 524 | # %r:: Replaced by the elapsed real time, as reported by Tms#real 525 | # %n:: Replaced by the label string, as reported by Tms#label (Mnemonic: n of "*n*ame") 526 | # 527 | # If +format+ is not given, FORMAT is used as default value, detailing the 528 | # user, system, total and real elapsed time. 529 | # 530 | def format(format = nil, *args) 531 | str = (format || FORMAT).dup 532 | str.gsub!(/(%[-+.\d]*)n/) { "#{$1}s" % label } 533 | str.gsub!(/(%[-+.\d]*)u/) { "#{$1}f" % utime } 534 | str.gsub!(/(%[-+.\d]*)y/) { "#{$1}f" % stime } 535 | str.gsub!(/(%[-+.\d]*)U/) { "#{$1}f" % cutime } 536 | str.gsub!(/(%[-+.\d]*)Y/) { "#{$1}f" % cstime } 537 | str.gsub!(/(%[-+.\d]*)t/) { "#{$1}f" % total } 538 | str.gsub!(/(%[-+.\d]*)r/) { "(#{$1}f)" % real } 539 | format ? str % args : str 540 | end 541 | 542 | # 543 | # Same as #format. 544 | # 545 | def to_s 546 | format 547 | end 548 | 549 | # 550 | # Returns a new 6-element array, consisting of the 551 | # label, user CPU time, system CPU time, children's 552 | # user CPU time, children's system CPU time and elapsed 553 | # real time. 554 | # 555 | def to_a 556 | [@label, @utime, @stime, @cutime, @cstime, @real] 557 | end 558 | 559 | # 560 | # Returns a hash containing the same data as `to_a`. 561 | # 562 | def to_h 563 | { 564 | label: @label, 565 | utime: @utime, 566 | stime: @stime, 567 | cutime: @cutime, 568 | cstime: @cstime, 569 | real: @real 570 | } 571 | end 572 | 573 | protected 574 | 575 | # 576 | # Returns a new Tms object obtained by memberwise operation +op+ 577 | # of the individual times for this Tms object with those of the other 578 | # Tms object (+x+). 579 | # 580 | # +op+ can be a mathematical operation such as +, -, 581 | # *, / 582 | # 583 | def memberwise(op, x) 584 | case x 585 | when Benchmark::Tms 586 | Benchmark::Tms.new(utime.__send__(op, x.utime), 587 | stime.__send__(op, x.stime), 588 | cutime.__send__(op, x.cutime), 589 | cstime.__send__(op, x.cstime), 590 | real.__send__(op, x.real) 591 | ) 592 | else 593 | Benchmark::Tms.new(utime.__send__(op, x), 594 | stime.__send__(op, x), 595 | cutime.__send__(op, x), 596 | cstime.__send__(op, x), 597 | real.__send__(op, x) 598 | ) 599 | end 600 | end 601 | end 602 | 603 | # The default caption string (heading above the output times). 604 | CAPTION = Benchmark::Tms::CAPTION 605 | 606 | # The default format string used to display times. See also Benchmark::Tms#format. 607 | FORMAT = Benchmark::Tms::FORMAT 608 | end 609 | --------------------------------------------------------------------------------