Help us understand the problem. What is going on with this article?

Rubyの文字列連結に関して知っておくべきこと

More than 1 year has passed since last update.

Rubyだと文字列連結は足し算するだけでいいのでとってもお気楽。
でも、その裏の動きを意識しないと効率の悪いコードになってしまうかもしれない。

文字列連結の方法3つ

String#+

一番ポピュラーなやつ。

str = 'abc' + 'def'
# => "abcdef"

String#<< / String#concat

これでも同じ動きをする…かのように見える。

str = 'abc' << 'def'
# => "abcdef"
str = 'abc'.concat('def')
# => "abcdef"

Array#join

連結したい文字列を配列で持っておく必要があるけど、これでも連結できる。

str = %w(abc def).join
# => "abcdef"

たくさん繋げてみると大きな差が

文字列を2つ3つ、ちょろっと連結するだけなら好きな方法をとればいいと思う。
でもたくさん繋げるとなると、大きな違いが出てくる。

start = Time.now
str = ""
(0...10000).each do
  str += "test"
  str += "test"
  str += "test"
  str += "test"
  str += "test"
  str += "test"
  str += "test"
  str += "test"
  str += "test"
  str += "test"
end
puts "String#+   : #{Time.now - start}[sec]"

start = Time.now
str = ""
(0...10000).each do
  str << "test"
  str << "test"
  str << "test"
  str << "test"
  str << "test"
  str << "test"
  str << "test"
  str << "test"
  str << "test"
  str << "test"
end
puts "String#<<  : #{Time.now - start}[sec]"

start = Time.now
ary = []
(0...10000).each do
  ary << "test"
  ary << "test"
  ary << "test"
  ary << "test"
  ary << "test"
  ary << "test"
  ary << "test"
  ary << "test"
  ary << "test"
  ary << "test"
end
str = ary.join
puts "Array#join : #{Time.now - start}[sec]"

ループの中で同じ文をたくさん書いてるのは、計測時間に占めるループ処理時間の割合を下げるための悪あがき。
こんなことしなくても差は歴然だけれども。

String#+   : 9.117918[sec]
String#<<  : 0.013012[sec]
Array#join : 0.021162[sec]

String#+String#<<とで700倍ぐらい差がでた。

なぜこんなに差がでたのか

String#+に関しては、例えば

str = 'abc' + 'def'

と書くと、次のような処理になる。
1. 'abc''def'の文字列長を確認して、結合後の文字列の長さを求める
2. 結合後の文字列が入るサイズのメモリを確保したオブジェクトstrを作成
3. 'abc''def'strが持つメモリにコピー

先ほど処理時間を計測したコードだと、10万回のメモリ確保が行われたことになる。
メモリ確保に時間もかかるし、さらに確保したメモリをすぐに捨ててまたメモリ確保してを繰り返すので、わりとフラグメンテーションが起きているかもしれない。
そして、文字列のコピーもざっと200MB分ぐらいやってるはず。
最後の方なんてstrに40万文字の文字列が入ってるので、1回文字列結合するだけで40KB分も文字列コピーされちゃう><

それに対してString#<<はというと、

str = 'abc' << 'def'
  1. 'abc''def'の文字列長を確認して、結合後の文字列の長さを求める
  2. 'abc'のオブジェクトが持つメモリサイズを見て、'def'を追加できないか確認
    • 追加してもメモリが足りるようだったら、そのまま追加
    • 足りないなら、'abc'の持つメモリサイズの倍(それでも足りなければ更に倍、倍、と繰り返す)のサイズのメモリを持つオブジェクトを用意して、そこに両方の文字列をコピー。

ということで、毎回メモリを確保せず、できるだけ追加しようとしてくれる。
追加が無理でも、先を見越して倍のサイズのメモリを確保してくれるので、またしばらく安泰という寸法。
なので、処理時間計測コードで見ると、メモリ確保20回程度、文字列のコピーも全部で500KB程度なはず。
場合によっては無駄に多くメモリが確保されることもあるけど、今の時代気にしなくていい場合がほとんどじゃないかと思う。
それから、メモリに追加していく関係上、String#<<は破壊的メソッドになるので注意。

Array#joinは、最終的な文字列の長さが一度でわかるので、そのサイズのメモリを一度確保してやるだけで大丈夫。あとはそのメモリにどんどん文字列を書き込んでいくだけ。
どちらかというとjoinするための配列を作るのに時間がかかってる。

舞台裏を垣間見る

本当にそうなのか?
String#+String#<<の挙動を確認するために以下のコードを動かしてみた。
オブジェクトIDと、使用しているメモリサイズ(大体の値らしい)を出力している。

require 'objspace'

p "String#+"
str1 = ""
(0...10).each do
  str1 += "012345678901234567890123456789"
  puts "obj_id : #{str1.object_id} / memsize : #{ObjectSpace.memsize_of(str1)}"
end

p "String#<<"
str2 = ""
(0...10).each do
  str2 << "012345678901234567890123456789"
  puts "obj_id : #{str2.object_id} / memsize : #{ObjectSpace.memsize_of(str2)}"
end
String#+
obj_id : 70116696586100 / memsize : 31
obj_id : 70116696585960 / memsize : 61
obj_id : 70116696585840 / memsize : 91
obj_id : 70116696585720 / memsize : 121
obj_id : 70116696585600 / memsize : 151
obj_id : 70116696585480 / memsize : 181
obj_id : 70116696585360 / memsize : 211
obj_id : 70116696585240 / memsize : 241
obj_id : 70116696585120 / memsize : 271
obj_id : 70116696585000 / memsize : 301
String#<<
obj_id : 70116696584900 / memsize : 49
obj_id : 70116696584900 / memsize : 99
obj_id : 70116696584900 / memsize : 99
obj_id : 70116696584900 / memsize : 199
obj_id : 70116696584900 / memsize : 199
obj_id : 70116696584900 / memsize : 199
obj_id : 70116696584900 / memsize : 399
obj_id : 70116696584900 / memsize : 399
obj_id : 70116696584900 / memsize : 399
obj_id : 70116696584900 / memsize : 399
  • String#+では、
    • 毎回新しくオブジェクトが作られる
    • 最低限必要なサイズのメモリしか確保しない
  • String#<<では、
    • オブジェクトの中身を変更していく(破壊的メソッド)
    • メモリが足りなくなったら倍のサイズを確保する

ということが分かる。

まとめ

アレコレ書いたけど、文字列をちょっと繋げるだけなら、そんなに神経質にならなくてもいいと思う。
出した例はわりと極端だけど、もし文字列をガンガン繋げる場面に出くわしたら気をつけてもらえれば。

Kta-M
fusic
個性をかき集めて、驚きの角度から世の中をアップデートしつづける。
https://fusic.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした