はじめに
お久しぶりです!ソフトウェアエンジニアの飯塚(@hiizkk)です。前回はRidgepoleでスキーマ管理をするブログを書きました。
最近はブルーロックにハマっています。運(計画的偶発性理論)や挑戦的集中などの(たぶんエンジニアも大好きな)概念が登場しますし、めちゃめちゃ熱い作風で自分みたいにサッカーに興味がなくても楽しめる作品だと思うので興味ある方はぜひ手にとってみてください!
今回は書く内容をとても迷いましたが、SREの観点からパフォーマンスチューニングに興味がある人が多いと思いますし、個人的に数学とアルゴリズムを勉強しているので、Rubyで処理時間を計測できるBenchmarkというライブラリについて記事を書いていこうと思います。
Benchmarkとは?
https://docs.ruby-lang.org/ja/latest/class/Benchmark.html
普段開発する中で、「このバッチの実行時間遅いな…」など感じる機会があると思います。そんな時に役立つのがBenchmarkです。基本形は以下の通りになります。
1 2 3 4 5 6 7 |
require 'benchmark' Benchmark.bm do |x| x.report { # 計測対象1 } x.report { # 計測対象2 } x.report { # 計測対象3 } end |
実際に使ってみる
これでアルゴリズムの入門書でよく出てくる年齢当てゲームの線形探索と二分探索を計測してみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
require 'benchmark' ages = (0..1_000_000).to_a # 年齢(計測用に大きな数値を設定) my_age = 20 def linear_search(ages, target_age) ages.each do |age| return age if age == target_age end nil end def binary_search(ages, target_age) left = 0 right = ages.size - 1 while left <= right mid = (left + right) / 2 if ages[mid] == target_age return ages[mid] elsif ages[mid] < target_age left = mid + 1 else right = mid - 1 end end nil end Benchmark.bm do |x| x.report("Linear Search:") { linear_search(ages, my_age) } x.report("Binary Search:") { binary_search(ages, my_age) } end |
- 結果
1 2 |
# Linear Search: 0.000010 0.000005 0.000015 ( 0.000012) # Binary Search: 0.000006 0.000006 0.000012 ( 0.000011) |
1,000,000の20だともはや線形も二分も変わらないですね。ちなみに年齢的にありえませんが、my_ageを999,999にすると以下の通りです。
1 2 3 |
user system total real Linear Search: 0.037117 0.000760 0.037877 ( 0.037872) Binary Search: 0.000003 0.000000 0.000003 ( 0.000004) |
計算量 O(N)(線形探索)は計算回数が多い分だけ実行時間がかかりますし、O(log N)(二分探索)はターゲットがどこにあっても計算速度は大きく変動しないということが分かりますね。
出力結果の項目について
ここで、いくつか項目が出ているようです。
Ruby3.4リファレンスマニュアル や ただ屋ぁのブログ さんの説明を借りるとそれぞれ以下を意味するそうです。
- user: user CPU time、Rubyプログラム自体のコード実行に費やされた時間
- system: system CPU time 、OSのコード(システムコール)が実行された時間
- total: total CPU time、user+system
- real: 実経過時間
線形探索と二分探索のコードはほとんどuser CPU timeが長いみたいです。なのでRubyプログラムがメインで動いていると判断してよさそうですね。systemについては scivolaさんの記事 によりますと以下のように、Rubyの class File を使ったりすると増加すると思いましたので試してみました。
system は「システム CPU 時間」のこと。OS はファイルの読み書きなどの基本的な機能を「システムコール」という形で提供しているが,プログラムがそれを呼び出したときに,その実行に費やした CPU 時間をこう呼ぶらしい。
1 2 3 4 5 6 7 8 9 10 |
require 'benchmark' def read_file f = File.open("./test.csv", "r") f.close end Benchmark.bm do |x| x.report("result: ") { read_file } end |
- 結果
1 2 3 4 5 6 7 8 9 10 11 12 |
# 1回目 ❯ ruby demo_syscall.rb user system total real result: 0.000008 0.000018 0.000026 ( 0.000022) # 2回目 user system total real result: 0.000010 0.000010 0.000020 ( 0.000015) # 3回目 user system total real result: 0.000011 0.000010 0.000021 ( 0.000015) |
確かに線形探索と二分探索のコードよりもsystem の使用時間が長いようですね。使う機能によってここまで違いが出ていることが分かります。
まとめ
RubyのBenchmarkの簡単な解説と使ってみた記事を書いてみました。メジャーなmoduleなので調べればもっと詳しくて素晴らしい記事もたくさん出てきますが、自分でコード書いて試したりすることで、新たな発見ができることもありますね。個人的には以下の3つが大きな学びでした。
- CPUの実行時間の種類(user, system)を知ることができた
- Rubyのどんなコードでsystem CPUが使われているのかがわかった
- アルゴリズムの特徴を計算量や数値で捉えることができた
今後もBenchmarkを使ってパフォーマンスを意識して実装していきたいと思います。
注釈
※ただし、何回か実行してみると大きく変動がありました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
# 1回目 user system total real Linear Search: 0.000016 0.000002 0.000018 ( 0.000013) Binary Search: 0.000010 0.000002 0.000012 ( 0.000012) # 2回目 user system total real Linear Search: 0.000018 0.000003 0.000021 ( 0.000013) Binary Search: 0.000011 0.000002 0.000013 ( 0.000012) # 3回目 user system total real Linear Search: 0.000034 0.000005 0.000039 ( 0.000034) Binary Search: 0.000012 0.000002 0.000014 ( 0.000013) # 4回目 user system total real Linear Search: 0.000119 0.000018 0.000137 ( 0.000136) Binary Search: 0.000013 0.000002 0.000015 ( 0.000015) |
bmbmメソッドのリファレンス によると、とあるので、GC(Garbage Collection)の影響でブレに繋がっているようですね。上記リンクのbmbmメソッドはGCの影響をなくすためにあるらしいですが、実際に試しても普通にブレたのでまあ当てにならない気がします。様々な記事によると複数回実行した結果の平均で測定するようです。
ベンチマークの結果は GC の影響によって歪められてしまうことがあります。実際にはこのメソッドを使用しても、GC などの影響を分離することは保証されません。
アパレル販売員→アパレル経理として働いたのち、ソフトウェアエンジニアに転職。エンジニア転職活動の時期に自作Railsアプリを作る過程でMENTAでAWSの学習サポートを@adachin0817にフォローしてもらいTechBullに参画。アプリケーション周りの記事を執筆。