アクトインディ開発者ブログ

子供とお出かけ情報「いこーよ」を運営する、アクトインディ株式会社の開発者ブログです

Rubyの文字列連結、最速は?

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

まあ、予想通り+演算子はその都度インスタンスが生成されるので論外。
StringIOString#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#<<

最後に

アクトインディではこんな感じでレビューしあいながら開発しています。
一緒にレビューを楽しみながら開発しませんか?

エンジニア募集中です。

actindi.net