morishitaです。
先日、文字列を繰り返し結合するようなコードを書いていました。
文字列の連結するのに StringIO を利用していました。
イメージとしてはこんな感じですね。
io = String.new io.write '文字列' io.write '文字列' # 〜中略: たくさんの繰り返し 〜 str = io.string
そのコードをレビューしてもらっていたときに次のようなツッコミをもらいました。
String ではなく StringIO を使ってるのってなんでです?
(効率良かったりするのかしら…?)
私の Web エンジニア人生の最初の言語は Java だったのですが、
Java では大量に文字列を連携する場合には StringBuffer
を使うのが鉄則です。
(しばらく Java から遠ざかっているのですが、今でもそうですよね? )
で、Ruby のStringIO
は Java のStringBuffer
みたいなものかなと思ってこれまでもなんとなく使っていました。
そのときに議論になったのは +
演算子による文字列結合はインスタンスがその都度生成するのでダメだろう。
では、StringIO#write; StringIO#string
って String#concat
より速いの? ということでした。
で、調べました。
インスタンスの生成はどうなっているのか
まずは、インスタンスが変わるのか変わらないのかを次のコードで確認しました。
puts '-- a += b --' a = 'a' b = 'b' puts "a.object_id: #{a.object_id}" a += b puts "(a += b).object_id: #{a.object_id}" puts "\n-- a.concat b --" a = 'a' b = 'b' puts "a.object_id: #{a.object_id}" a.concat b puts "(a.concat b).object_id: #{a.object_id}" puts "\n-- StringIO --" a = StringIO.new a.write 'a' b = 'b' puts "a.object_id: #{a.object_id}" a.write b puts "(a.write b).object_id: #{a.object_id}"
その結果は次の通り。
-- a += b -- a.object_id: 70242235991440 (a += b).object_id: 70242235991280 -- a.concat b -- a.object_id: 70242235990920 (a.concat b).object_id: 70242235990920 -- StringIO -- a.object_id: 70242235990600 (a.write b).object_id: 70242235990600
まあ、予想通り+
演算子はその都度インスタンスが生成されるので論外。
StringIO
とString#concat
は変わりません。
ベンチマーク
次のコードで文字列連結の速度を計測。
require 'benchmark' n = 10000 m = 1000 Benchmark.bm do |r| r.report 'String#+' do n.times do x = '' m.times do x += 'a' end end end r.report 'String#concat' do n.times do x = '' m.times do x.concat('a') end end end r.report 'StringIO#write' do n.times do x = StringIO.new m.times do x.write('a') end x.string end end end
この結果は次の通り。
user system total real String#+ 4.120000 0.170000 4.290000 ( 4.286334) String#concat 2.110000 0.000000 2.110000 ( 2.112879) StringIO#write 1.520000 0.010000 1.530000 ( 1.528513)
ほほー、StringIO#write
速いね。ということでそのプルリクは LGTM となり、マージしてデプロイされました。
でも...
このエントリを書きながら、ふと気になって次も確認してみました。
require 'benchmark' n = 10000 m = 1000 Benchmark.bm do |r| r.report 'String#<< ' do n.times do x = '' m.times do x << 'a' end end end end
で、その結果。
user system total real String#<< 1.180000 0.000000 1.180000 ( 1.185162)
あ…。
まとめ
Ruby の文字列連結の最速は String#<<
。
最後に
アクトインディではこんな感じでレビューしあいながら開発しています。
一緒にレビューを楽しみながら開発しませんか?
エンジニア募集中です。