pockestrap

Programmer's memo

String#gsub! は String#gsub よりも遅い

ruby-jp1のSlackで話していて面白かったのでまとめ。

f:id:Pocke:20190913215326p:plain
Slack上の発言

RubyString#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!の実装にあった。

Rubystring.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 チャンネルです。