この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。
Rubyが遅いというのはよく聞く話ですが、ボトルネックがRubyそのものでもないのにRubyのせいにするのは非常にナンセンスです。最低限Rubyで読みやすく速いコードを書いてからRubyのせいにしましょう。本記事では何が速いコードなのかを知る手助けとなる色々なBenchmarkの取り方と、それを使ったString同士の結合メソッド比較を紹介します。
Measure Don’t Guess
はじめに
今年5度目の武道館公演で灼熱に飲まれました、くろすです。
鍋食べにいくなら喜んでついていきます。
コードに対する感性やセンスは一朝一夕で身につくものじゃありません。
それを磨くためには、たくさんの良いコードに触れ、良いコードとは何かを理解する必要があります。
速いコードが良いコードというわけではありませんが、遅いコードは悪いコードなので測り倒して抹殺しましょう。
benchmark をとる
最も単純なベンチマークの取り方というと以下のようなコードになると思います。
time = Time.now
'hoge'
puts "Benchmark : #{Time.now - time}s"
#=> Benchmark : 2.0e-06 s
これをそのまま使うのはあまりにもナンセンスなので普通なら以下のようなコードになるでしょう。
def benchmark
time = Time.now
yield
puts "Benchmark : #{Time.now - time}s"
end
benchmark { 'hoge' }
#=> Benchmark : 2.0e-06 s
ただ、このbenchmark
メソッドもあまりセンスよくないです。
だってrubyにはBenchmarkモジュールがあるから。
require 'benchmark'
puts "Benchmark : #{Benchmark.realtime { 'hoge' } s}"
#=> Benchmark : 1.00000761449337e-06 s
ミリ秒で値が欲しいなぁという場合はこちら
ActiveSupportの中で拡張されています。
require 'active_support'
require 'active_support/core_ext/benchmark'
puts "Benchmark : #{Benchmark.ms { 'hoge' } ms}"
#=> Benchmark : 0.00200001522898674 ms
自分で作ったbenchmark
メソッドとruby標準のBenchmark.realtime
メソッドの違いですが、
Benchmark.realtimeの方はTime.now に当たる部分がProcess.clock_gettime(Process::CLOCK_MONOTONIC) になっています。
この Process.clock_gettime(Process::CLOCK_MONOTONIC)とTime.nowの違いは面倒くさくて追いきれてないので気が向いたら追記します。(どっちもsource_locationでnilが帰ってくるのでパッと調べるのがめんd..)
ちなみにですがruby標準のbenchmarkにはbenchmark
メソッドが存在します。
require 'benchmark'
Benchmark.bm do |x|
x.report('hoge') { 'hoge' }
end
#=>
user system total real
hoge 0.000000 0.000000 0.000000 ( 0.000002)
いろんな情報見れて便利ですよね。
さて、少し話が変わりますがRailsのcontributorになりたいと思ったことはありますか?
そう思ったら使わなきゃいけないgemがある(Benchmark Your Code)って知ってますか?
benchmark-ipsです。
require 'benchmark/ips'
Benchmark.ips do |x|
x.report('hoge') { 'hoge' }
end
#=>
Warming up --------------------------------------
244.639k i/100ms
Calculating -------------------------------------
8.302M (± 6.6%) i/s - 41.344M in 5.002986s
gemの説明にもある通りiterations per second
を返してくれます。
1秒間に何回回るかってことですね。ウォーミングアップもやってくれるんで使い勝手が非常に良いです。
ここまでrubyのbenchmarkについて追ってきました。
ちょっと待ってくれ、Rails環境下でbenchmark do
的な感じでbenchmark使ったことあるけど、あれはなんだ?と思った人にはこちら。
ActiveSupport::Benchmarkableのインスタンスメソッドにbenchmark
があります。
ソース読めばわかりますが、内部的には前述のBenchmark.ms
を使ってるだけですね。
オプションでloggerを切れるので、詳しい情報が必要じゃないけど時間だけ知りたい場合のRails環境下だとこっちの方が使い勝手いいです。
String同士の結合メソッド比較
さて、本題は終わったのでここからは実際の使用例的な感じで調べてみます。
ここで注意しておいてもらいたいのはString#concat
はString#<<
のaliasであるということです。
リファレンスの説明もまとめられています。
require 'benchmark/ips'
Benchmark.ips do |x|
x.config(time: 5, warmup: 2)
x.report('String#+') { '' + 'hoge' }
x.report('String#<<') { '' << 'hoge' }
x.report('String#concat') { ''.concat('hoge') }
x.compare!
end
#=>
Warming up --------------------------------------
String#+ 197.224k i/100ms
String#<< 187.204k i/100ms
String#concat 141.180k i/100ms
Calculating -------------------------------------
String#+ 4.383M (± 6.3%) i/s - 21.892M in 5.014485s
String#<< 4.608M (± 6.0%) i/s - 23.026M in 5.016537s
String#concat 2.983M (± 5.2%) i/s - 14.965M in 5.031350s
Comparison:
String#<<: 4608081.5 i/s
String#+: 4382769.8 i/s - same-ish: difference falls within error
String#concat: 2982639.2 i/s - 1.54x slower
''
と'hoge'
のString.newのコストは全て同じため考えません。
こうやってみると単純な結合にはString#+
とString#<<
どっちも良さそうですね。String#+
とString#<<
の比較はこちらが参考になりますが、メモリ確保の挙動と破壊的か非破壊的かが違うようです。
どうもString#concat
は単発で呼ぶとaliasの分String#<<
より遅いようですね。
ただ個人的は、Stringに<<
を生やすのは間違えてArray触っている気持ちになって気持ち悪いのでconcatを使いたいです。
それでは次のbenchmarkをみてみましょう
require 'benchmark/ips'
@a = ''
def a
@a += 'hoge'
end
@b = ''
def b
@b << 'hoge'
end
@c = ''
def c
@c.concat('hoge')
end
Benchmark.ips do |y|
y.config(time: 5, warmup: 0)
y.report('String#+') { a }
y.report('String#<<') { b }
y.report('String#concat') { c }
y.compare!
end
stringをどんどん長くしていくコードですね。
今回はウォーミングアップの時間を作ると@a
@b
@c
の中身が書き変わりフェアではなくなるので直で実行するために、configで時間を設定しています。
結果はこちら。
#=>
Warming up --------------------------------------
String#+ 1.000 i/100ms
String#<< 1.000 i/100ms
String#concat 1.000 i/100ms
Calculating -------------------------------------
String#+ 74.156k (±146.7%) i/s - 70.018k in 4.972166s
String#<< 986.464k (±10.0%) i/s - 2.885M
String#concat 980.709k (±11.8%) i/s - 3.182M
Comparison:
String#<<: 986463.5 i/s
String#concat: 980708.6 i/s - same-ish: difference falls within error
String#+: 74156.1 i/s - 13.30x slower
こうやってみてみるとString#<<
が優秀ですね。
まとめ
普段から触っているStringのメソッドにもこのような違いがあるのですから、普段書いているプログラムが常に最善の中での最速になるわけありません。
プロファイリングツールを使い全体のボトルネックを把握しつつ、このような細かい部分は自分でベンチとって蓄積していきましょう。
計って測って図り倒して自分のセンスを磨いていきたいところです。