CRubyのStringの操作に関する最適化手法
monorubyの実装
code:Rust
pub struct RStringInner {
ty: Encoding,
cr: Cell<CodeRange>,
}
pub enum CodeRange {
/// Not yet computed.
Unknown,
/// Every byte is < 0x80; safe under any ASCII-compatible encoding.
SevenBit,
/// Encoding-valid (and contains at least one non-ASCII codepoint).
Valid,
/// Contains a byte sequence invalid in the declared encoding.
Broken,
}
CRuby(MRI)におけるString操作の最適化は、ほぼ全てが「メモリ割り当てとコピーをいかに避けるか」と「エンコーディング検査の再実行をいかに避けるか」の2軸に集約されます。主要な手法を体系的にまとめます。
1. オブジェクト表現レベルの最適化
1. 埋め込み文字列(Embedded String / RSTRING_EMBED)
RString構造体は共用体になっており、短い文字列は 別ヒープ領域を割り当てず、構造体内部のバッファに直接格納 されます。
code:Ruby
struct RString {
struct RBasic basic;
union {
struct { // heap 表現
long len;
char *ptr;
union {
long capa;
VALUE shared;
} aux;
} heap;
struct { // embed 表現
long len;
} embed;
} as;
};
フラグ STR_NOEMBED の有無で表現を切り替える。
閾値(GCスロットサイズに依存。VWA導入後は 40 byte スロットなら ~24 byte 程度)以下の文字列は malloc を1回も呼ばずに済む。
多くの実プログラムでは大半の文字列が短いため、効果が大きい。
2. Variable Width Allocation (VWA)(Ruby 3.x〜)
GCヒープを単一サイズのスロットではなく、複数サイズプール(40/80/160/320/640 byte)で管理。これにより 埋め込み可能なサイズの上限が広がり、本来ヒープ割り当てが必要だった中サイズ文字列も埋め込みで賄えるようになりました。malloc/freeの回数そのものを削減します。
2. コピー回避(Copy-on-Write / 共有)
1. 共有文字列(Shared String)
str.dup、String#+@、部分文字列(str[range]が一定長以上)などで、バッファを共有してポインタとオフセットだけを持つ 仕組み。
heap.aux.shared に元文字列の VALUE を保持し、STR_SHARED フラグを立てる。
元文字列のバッファをそのまま指す(substring は ptr をオフセットさせる)。
どちらかに破壊的変更(<<, gsub! など)が入った時点で初めて実体コピー(rb_str_modify → unshare)。これが文字列版 Copy-on-Write。
2. リテラルの共有元(literal shared)
コード中の文字列リテラルは命令列(ISeq)内に1個だけ実体を持ち、実行時には毎回それを 共有複製 することで、リテラル評価時のコピーを避けます。
3. Frozen String と fstring テーブル(インターン)
1. frozen string literal
code:Ruby
# frozen_string_literal: true
同一内容の凍結リテラルは グローバルな fstring テーブル(rb_fstring)で重複排除(deduplication) され、プロセス内で1インスタンスを共有します。
メモリ削減 + ハッシュキー比較が高速化。
ハッシュのキーが frozen string の場合、内部で fstring 化して同一性比較を効きやすくする最適化もある。
2. chilled string(Ruby 3.4)
frozen string literal をデフォルト化していく移行措置。マジックコメント無しのリテラルを「変更すると警告を出すが frozen ではない」中間状態にする。STR_CHILLED フラグで表現し、破壊的変更時に deprecation 警告を出します。
4. エンコーディング検査結果のキャッシュ(Coderange)
CRubyの文字列最適化で最も「Rubyらしい」部分。各文字列は coderange をフラグにキャッシュします。
table: Coderange
意味
ENC_CODERANGE_7BIT 全バイトがASCII範囲(多バイト処理不要)
ENC_CODERANGE_VALID そのエンコーディングとして妥当な多バイト列
ENC_CODERANGE_BROKEN 不正なバイト列を含む
ENC_CODERANGE_UNKNOWN 未走査
効果:
length(文字数)・index・[] などで、毎回エンコーディングを走査せずに済む。7BIT と判明していればバイト長=文字長 として O(1) で返せる。
連結(<<, concat)時に両者の coderange をマージして再走査を回避。
ASCII互換エンコーディングでの高速パスが多数存在。
これは独自Ruby実装で見落としやすい一方、ベンチで効きやすいポイントです。
5. バッファ成長戦略(capacity の過剰確保)
<< や String#*、sprintf などの追記で、必要量ぴったりではなく 余分に確保(over-allocation) して、繰り返し追記時の realloc 回数を償却的に削減。
rb_str_buf_cat 系は capacity が不足したときのみ拡張。
String.new(capacity: n) でユーザが事前確保もできる(ループ内連結の定石)。
拡張は概ね指数的増加(最低でも1.5倍前後)で O(n) 償却を保証。
6. 専用Cパスと命令最適化
1. 比較・ハッシュ
文字列比較は最終的に memcmp。長さが違えば即 false。
ハッシュ値は計算する(多くのバージョンではキャッシュせず都度計算だが、SipHash 等で高速化)。
2. よく使うメソッドの special-case
String#==, String#+, String#<<, String#[], String#freeze などはCで直接実装され、YARV命令やJIT(YJIT)でインライン化対象になっている。
String#freeze 済みオブジェクトに対する操作は再freezeを省略。
3. YJIT / ZJIT との連携
文字列連結 (opt_str_freeze, opt_str_uminus) のような 専用YARV命令 があり、"...".freeze や -"..." をリテラル段階で fstring 化する。
YJITは embedded判定・coderangeチェックを機械語に展開し分岐を減らす。
7. シンボルとの使い分け(周辺最適化)
ハッシュキーやメソッド名にはシンボル(一意・GC対象外あるいはmortal symbol)を使い、文字列割り当てを回避。
to_sym / to_s の往復を避ける設計が推奨される。