薄いブログ

技術の雑多なことを書く場所

Ruby 2.7.7 から 3.3.1 にあげた際のメモリ使用量の変化について

最近 2.7.7 から 3.3.1 に以下のようなコードを移行した際にGCの負荷が増えたので調査を行った。

s = "foo              "
s.gsub(/ (\s+)/) { " #{' ' * Regexp.last_match(1).length}" }

https://gist.github.com/orisano/98792dee260106e9b6fcb45bbabeb1e6

before (2.7.7):

allocated memory by class
-----------------------------------
 608.00 MB  String
 168.00 MB  MatchData
   40.00 B  <svar> (IMEMO)

after (3.3.1):

allocated memory by class
-----------------------------------
 792.00 MB  String
 336.00 MB  MatchData
  240.00 B  <callcache> (IMEMO)
  200.00 B  <constcache> (IMEMO)
   40.00 B  <svar> (IMEMO)

結論

MatchData のインスタンス生成が増えていたのは以下の変更によるものだった。

github.com

3系では backref の MatchData を再利用しなくなったことによって共通のロジックを使っている gsub での MatchData のメモリ確保が増えた。 更に置換箇所ごとにメモリ確保が発生するようになった。

String のメモリ使用量が増加していたのは以下の変更によるものだった。

github.com

VWA for strings の変更によってこれまで問答無用で heap に確保されていた rb_str_buf_new が embed できる場合は embed するようになった。

USE_RVARGC の関連で rb_str_buf_new で STR_BUF_MIN_SIZE を使わなくなった。

VWA は alloc の回数は減るがバッファーの場合 realloc が使えなくなるので小さいケースにおいてはメモリ使用量が増える。

今回の場合は VWA で容量 47 (文字列長が 17 で gsub が追加する 30 をあわせたもの) で確保, 置換で 72 が追記されて容量が拡張され 112 になる。

容量拡張の際に xmalloc2 が実行され、VWA 分の領域が縮小したりしないので 47 + 112 で 159 になる。

2.7.7 では STR_BUF_MIN_SIZE が使われていて最初からヒープに確保されていたため xmalloc2(64) + xrealloc2(128) で 128 になっていた。

感想

String の件については仕方ないのかなという感じだった。

MatchData の件にツイては race condition を避けるために backref の再利用をやめたのは理解できるがうまくコンテキストごとに MatchData を再利用できると助かる。 あと gsub のコンテキストにおいては MatchData が Qnil になるまで while で回り、最後に MatchData を設定するようになっているが MatchData をうまく再利用してよい のであればメモリ確保が減って嬉しいなと思った。

今回どこでメモリ確保が発生しているのかの調査に gdb を使ったが以下の記事が参考になった。

techlife.cookpad.com

ruby_debug_breakpoint を Ruby のコードから呼び出せると今回のようなケースは楽だと思った。

リンク