推測するな、計測せよ。
Load, Aim, Shoot
Load, Aim, Measure
(ベンチマークライブラリを読み込み、計測対象を定め、計測する。)
この記事は?
Ruby でベンチマークを計測するための機能や Gem をご紹介します。
バージョン情報
$ ruby -v
ruby 3.1.3p185 (2022-11-24 revision 1a6b16756e) [arm64-darwin21]
$ gem list | grep benchmark
benchmark (default: 0.2.0)
benchmark-ips (2.10.0)
benchmark-memory (0.2.0)
benchmark ライブラリ
Benchmark モジュール
Benchmark.bm
require 'benchmark'
# 16 はラベルの長さ。
# 'String#end_with?'.size #=> 16
Benchmark.bm(16) do |x|
x.report('String#match?') { '🐶🐱🐰'.match?(/🐰\z/) }
x.report('String#end_with?') { '🐶🐱🐰'.end_with?('🐰') }
end
user system total real
String#match? 0.000007 0.000000 0.000007 ( 0.000003)
String#end_with? 0.000002 0.000000 0.000002 ( 0.000002)
- user: ユーザ CPU 時間
- Ruby プログラム自体の実行に費やされた時間
- system: システム CPU 時間
- Ruby プログラムが発行したシステムコールなどで OS のコードが実行された時間
- total: user + system
- real: 経過時間
1 回の実行だと処理時間のブレが大きくて参考にならないので、試行回数を増やす。
require 'benchmark'
n = 100_000
Benchmark.bm(16) do |x|
x.report('String#match?') { n.times { '🐶🐱🐰'.match?(/🐰\z/) } }
x.report('String#end_with?') { n.times { '🐶🐱🐰'.end_with?('🐰') } }
end
user system total real
String#match? 0.016460 0.000097 0.016557 ( 0.016555)
String#end_with? 0.012503 0.000139 0.012642 ( 0.012654)
適切な実行回数を決めたりループしたりするのがめんどくさい 😇
Benchmark.bmbm
ベンチマークの結果は GC の影響によって歪められてしまうことがあります。このメソッドは与えられたブロックを二度実行する事によってこの影響を最小化します。一回目は実行環境を安定化するためにリハーサルとして実行します。二回目は本番として実行します。
require 'benchmark'
n = 100_000
Benchmark.bmbm(16) do |x|
x.report('String#match?') { n.times { '🐶🐱🐰'.match?(/🐰\z/) } }
x.report('String#end_with?') { n.times { '🐶🐱🐰'.end_with?('🐰') } }
end
Rehearsal ----------------------------------------------------
String#match? 0.018132 0.000103 0.018235 ( 0.018232)
String#end_with? 0.011770 0.000088 0.011858 ( 0.011859)
------------------------------------------- total: 0.030093sec
user system total real
String#match? 0.010967 0.000062 0.011029 ( 0.011041)
String#end_with? 0.008412 0.000071 0.008483 ( 0.008485)
Gem
benchmark-ips
Benchmark.ips
benchmark-ips benchmarks a blocks iterations/second. For short snippits of code, ips automatically figures out how many times to run the code to get interesting data. No more guessing at random iteration counts!
- 1 秒あたりの実行回数を計測する。
- ベンチマーク時の処理の実行回数を自動で決定してくれる 💖
require 'benchmark/ips'
Benchmark.ips do |x|
x.report('String#match?') { '🐶🐱🐰'.match?(/🐰\z/) }
x.report('String#end_with?') { '🐶🐱🐰'.end_with?('🐰') }
x.compare!
end
Warming up --------------------------------------
String#match? 923.101k i/100ms
String#end_with? 1.052M i/100ms
Calculating -------------------------------------
String#match? 9.242M (± 0.4%) i/s - 47.078M in 5.094220s
String#end_with? 10.539M (± 0.4%) i/s - 53.658M in 5.091593s
Comparison:
String#end_with?: 10538712.3 i/s
String#match?: 9241654.3 i/s - 1.14x (± 0.00) slower
benchmark-memory
Benchmark.memory
- MemoryProfiler を使用して、処理の実行時のメモリ使用量やオブジェクトの割り当て (allocated)・保持 (retained) 数を計測する。
- benchmark-ips とインターフェイスが似ており、使いやすい。
require 'benchmark/memory'
Benchmark.memory do |x|
x.report('String#match?') { '🐶🐱🐰'.match?(/🐰\z/) }
x.report('String#end_with?') { '🐶🐱🐰'.end_with?('🐰') }
x.compare!
end
Calculating -------------------------------------
String#match? 40.000 memsize ( 0.000 retained)
1.000 objects ( 0.000 retained)
1.000 strings ( 0.000 retained)
String#end_with? 80.000 memsize ( 0.000 retained)
2.000 objects ( 0.000 retained)
2.000 strings ( 0.000 retained)
Comparison:
String#match?: 40 allocated
String#end_with?: 80 allocated - 2.00x more
まとめ
- 推測するな、計測せよ 🔬
- benchmark-ips と benchmark-memory で快適なベンチマークライフを 😉