[大人の自由研究]Rubyのmapとmap!の違いとパフォーマンス比較

Ruby

こんにちは。
泡アハです。

仕事では主にRubyを使って開発しているのですが、
まだ始めて7ヶ月ほどなので、わからないことも多いです。

日々勉強の毎日ですが、

mapを使うとき、mapを使うのとmap!を使うのどちらがよい?

とふと疑問に思いました。

色々調べた結果、基本的には、

参照先を直接いじりたい -> map!
元の変数には変更を加えたくない -> map

という使い分けで問題ないようです。

ただ、これ動作速度的にはどういう違いがあるのだろうと疑問に思ったので、
大人の自由研究と題して、調査してみました。

そもそもmapとmap!の違い

公式サイト

公式サイトを見てみます。

map

各要素に対してブロックを評価した結果を全て含む配列を返します。
出典: https://docs.ruby-lang.org/ja/latest/class/Array.html#I_COLLECT

map!

各要素を順番にブロックに渡して評価し、その結果で要素を置き換えます。
出典: https://docs.ruby-lang.org/ja/latest/class/Array.html#I_COLLECT

つまり、

map = 各要素に対して与えたブロックを評価した結果をすべて含む配列を新規で作って返す
map! = 各要素に対して与えたブロックを評価し、要素を直接置き換える

ということになります。

Rubyのコード

Rubyにおいて、Arrayは、RArrayとよばれるオブジェクトでできています。
https://qiita.com/ksss/items/aaa222bb2f70ad10a0b6

RArrayでは、要素数に変更がある場合、reallocやmemmoveを使用して、メモリ領域を確保するようです。

要素数が大きく変わればrealloc()がかかるし、末尾以外に要素を挿入 すればmemmove()
出典: https://i.loveruby.net/ja/rhg/book/object.html

実際のコードも読んでみましょう。
map
https://github.com/ruby/ruby/blob/3874381c4483ba7794ac2abf157e265546f9bfa7/array.c#L3743
map!
https://github.com/ruby/ruby/blob/3874381c4483ba7794ac2abf157e265546f9bfa7/array.c#L3771

mapは、rb_ary_new2を利用して、新しい配列を用意しているのに対し、
map!は、rb_ary_modifyを利用して、引数に渡された配列をそのまま変更しています。

実験

ここからが本題です。
mapのように配列全体の要素数は変わらず、各要素の大きさが変わった場合どうなるのか。
検証してみることにします。

今回やってみるのは、以下の4つです。

検証方法 検証コード
メモリ領域を広げない編集 &:upcase!
メモリ領域を固定長、狭める編集 value[0, 3]
メモリ領域を固定長、広げる編集 "#{value}A"
メモリ領域を可変長、広げる編集 "#{value}#{'A' * rand(10)}"

実際の検証コードは、githubにて公開しているので、
興味がある方はご確認ください。
https://github.com/nogu3/mapcompareforruby

実行結果

実行結果は以下のとおりです。

# ruby ./main.rb
メモリ領域を広げない編集
              user     system      total        real
map:      0.159628   0.009931   0.169559 (  0.169567)
              user     system      total        real
map!:     0.154216   0.000000   0.154216 (  0.154218)
メモリ領域を固定長、狭める編集
              user     system      total        real
map:      0.093633   0.009111   0.102744 (  0.102748)
              user     system      total        real
map!:     0.109660   0.000000   0.109660 (  0.109661)
メモリ領域を固定長、広げる編集
              user     system      total        real
map:      0.329067   0.009544   0.338611 (  0.338617)
              user     system      total        real
map!:     0.180752   0.000072   0.180824 (  0.180825)
メモリ領域を可変長、広げる編集
              user     system      total        real
map:      0.395469   0.020609   0.416078 (  0.416080)
              user     system      total        real
map!:     0.503577   0.000000   0.503577 (  0.503578)

考察

メモリ領域を広げない編集

メモリの領域を広げない編集に関しては、想定どおりです。
mapの場合、新しく配列をメモリ領域に展開するため、その分遅くなっているようですね。

メモリ領域を固定長、狭める編集

これは、少し想定と違いました。
想定では、map!の方が早いと思っていました。
メモリ領域を固定長狭める場合、メモリの再展開が不要だからです。
ですが、結果としては、ほぼ同等、若干mapの方が有利となっています。

メモリ領域を固定長 or 可変長、広げる編集

元々の想定では、固定長や可変長広げる編集は、mapの方が早いと想定していました。
なぜなら、map!の場合、各要素の文字列長を広げると確保したメモリを再確保する必要があるため、最初から配列の領域を新しく確保するmapの方が早くなると考えたからです。

しかし、結果を見てみると、

固定長広げる編集 => map! < map
可変長広げる編集 => map < map!

となっていました。

う〜む。
mapの場合、可変長の文字列を追加するとき、新しい配列のメモリを確保するのに時間がかかるのかな。。

まとめ

実行結果は、いくつか想定と異なることがあって、面白かったです。

なぜ追加する文字列長が固定長と可変長で逆転するのか。。。
こればっかりは、Rubyの実行ソースみるしかないかな〜。

コメント

タイトルとURLをコピーしました