Ruby の benchmark ライブラリと愉快な仲間たち

> _推測するな、計測せよ。_

<iframe width="560" height="315" src="https://www.youtube.com/embed/GDHPBX8Wt2Y" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

_~~Load, Aim, Shoot~~_
_Load, Aim, **Measure**_
(ベンチマークライブラリを読み込み、計測対象を定め、計測する。)

# この記事は？

Ruby でベンチマークを計測するための機能や Gem をご紹介します。

# バージョン情報

```shell
$ 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_driver (0.16.5)
```

#  benchmark ライブラリ

## [Benchmark モジュール](https://docs.ruby-lang.org/ja/latest/class/Benchmark.html)

### [Benchmark.bm](https://docs.ruby-lang.org/ja/latest/method/Benchmark/m/bm.html)

```rb
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 回の実行だと処理時間のブレが大きくて参考にならないので、試行回数を増やす。

```rb
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](https://docs.ruby-lang.org/ja/latest/method/Benchmark/m/bmbm.html)

> ベンチマークの結果は GC の影響によって歪められてしまうことがあります。このメソッドは与えられたブロックを二度実行する事によってこの影響を最小化します。一回目は実行環境を安定化するためにリハーサルとして実行します。二回目は本番として実行します。

```rb
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](https://github.com/evanphx/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 秒あたりの実行回数を計測する。
- ベンチマーク時の処理の実行回数を自動で決定してくれる 💖

```rb
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-driver](https://github.com/benchmark-driver/benchmark-driver)

### Benchmark.driver

> Low overhead benchmark by running generated script instead of calling Proc

- ブロックの呼び出しのオーバーヘッドを取り除くことで、より正確な計測が可能。
- Benchmark や benchmark-ips とインターフェイスが似ている。
- 詳しくは以下の記事を参照。
  - [Ruby高速化のためのベンチマークツール benchmark_driver.gem #Ruby - Qiita](https://qiita.com/k0kubun/items/82196240d3a7f987d1be)

```rb
require 'benchmark_driver'

Benchmark.driver do |x|
  x.prelude(<<~RUBY)
    animals = '🐶🐱🐰'.freeze
  RUBY

  x.report('String#match?', %{animals.match?(/🐰\z/)})
  x.report('String#end_with?', %{animals.end_with?('🐰')})
end
```
```
Warming up --------------------------------------
       String#match?    17.653M i/s -     17.846M times in 1.010925s (56.65ns/i)
    String#end_with?    17.863M i/s -     18.022M times in 1.008928s (55.98ns/i)
Calculating -------------------------------------
       String#match?    27.113M i/s -     52.960M times in 1.953310s (36.88ns/i)
    String#end_with?    26.728M i/s -     53.589M times in 2.004922s (37.41ns/i)

Comparison:
       String#match?:  27113060.9 i/s 
    String#end_with?:  26728481.7 i/s - 1.01x  slower
```

## [benchmark-memory](https://github.com/michaelherold/benchmark-memory)

### Benchmark.memory

- [MemoryProfiler](https://github.com/SamSaffron/memory_profiler) を使用して、処理の実行時のメモリ使用量やオブジェクトの割り当て (allocated)・保持 (retained) 数を計測する。
- benchmark-ips とインターフェイスが似ており、使いやすい。

```rb
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](https://github.com/evanphx/benchmark-ips) と [benchmark-memory](https://github.com/michaelherold/benchmark-memory) で快適なベンチマークライフを 😉

# 参考

- [How Benchmarking Your Code Will Improve Your Ruby Skills | HackerNoon](https://hackernoon.com/how-benchmarking-your-code-will-improve-your-ruby-skills-cre3ugd)
- [Ruby でベンチマークを取る方法 - Qiita](https://qiita.com/scivola/items/c5b2aeaf7d67a9ef310a)
- [Performance :: RuboCop Docs](https://docs.rubocop.org/rubocop-performance/cops_performance.html)