String#gsub! は String#gsub よりも遅い
ruby-jp1のSlackで話していて面白かったのでまとめ。
RubyのString#gsub!
はString#gsub
の破壊的バージョンで、置換結果でレシーバを上書きする。
当然gsub!
の方が新しいStringオブジェクトを生成しないので速いと思っていたら、どうやらそんなことはない(しかもgsub!
のほうが微妙に遅い)ようなので盛り上がった。
ベンチマーク
ベンチマークは次の通り。mameさんが出してくれたサンプルコードをそのままコピペしている。
ss = "abcde" * 100 t = Time.now 100000.times do s = ss s = s.gsub("a", "A").gsub("b", "B").gsub("c", "C").gsub("d", "D").gsub("e", "E") end p Time.now - t #=> 4.757643117 t = Time.now 100000.times do s = ss.dup s = s.gsub!("a", "A").gsub!("b", "B").gsub!("c", "C").gsub!("d", "D").gsub!("e", "E") end p Time.now - t #=> 4.895694206
どちらも大した時間差はなく、gsub!
のほうがちょっとだけ遅い。私の手元でもだいたい同じような結果になった。
原因
速度が大して変わらない原因は、gsub!
の実装にあった。
Rubyのstring.c
を見てみよう。gsub!
, gsub
ともにstr_gsub
関数を呼んでいる。
それぞれ第四引数に1, 0を渡していて、これで!
付きのメソッドかどうかを判定している。
// https://github.com/ruby/ruby/blob/7e0f56fb3dfcbc1b48f40c6c3b2c23c8e46a2341/string.c#L5191-L5192 static VALUE str_gsub(int argc, VALUE *argv, VALUE str, int bang) // https://github.com/ruby/ruby/blob/7e0f56fb3dfcbc1b48f40c6c3b2c23c8e46a2341/string.c#L5334-L5339 static VALUE rb_str_gsub_bang(int argc, VALUE *argv, VALUE str) { str_modify_keep_cr(str); return str_gsub(argc, argv, str, 1); } // https://github.com/ruby/ruby/blob/7e0f56fb3dfcbc1b48f40c6c3b2c23c8e46a2341/string.c#L5405-L5409 static VALUE rb_str_gsub(int argc, VALUE *argv, VALUE str) { return str_gsub(argc, argv, str, 0); }
そしてstr_gsub
関数内ではbang
で分岐して、レシーバを置換結果で置き換えるようになっていた。
// https://github.com/ruby/ruby/blob/7e0f56fb3dfcbc1b48f40c6c3b2c23c8e46a2341/string.c#L5307-L5310 if (bang) { str_shared_replace(str, dest); }
ちなみにこのdest
というのはRubyのStringオブジェクトで、置換結果である。
つまり、str_gsub
は分岐の有無に限らず新しいStringオブジェクトを生成していて、bangの場合は単にレシーバに新しいStringオブジェクトをコピーしているだけであった。
ほとんどのロジックはgsub
, gsub!
ともに同じなので、このコピーの分だけgsub!
の方が遅くなってしまうのだろう。
破壊的な分gsub!
のほうが高速だと無邪気に信じていたために中々の衝撃だった。
余談
sub!
とsub
だとsub!
の方が速そう- マイクロベンチマークなので、実アプリは謎(とmameさんが言っていていい話だなあと思った)
gsub!
がなにも置換しない場合はこちらのほうが速いらしい- inplaceで置換していければ速くなるのでは、そもそもそれは可能なのか、みたいな話もしていた
- 詳細は ruby-jpのslackにjoinして、 https://ruby-jp.slack.com/archives/CLWSHA76V/p1568376227317500 からどうぞ。 #ruby チャンネルです。