ニュース

GoのGCシェイプステンシリング:ジェネリクスの中間領域

Goのジェネリクス実装は、GCシェイプステンシリングというハイブリッド手法を用い、正確な型ではなくガベージコレクタの形状に基づいてモノモーフィゼーションを行い、バイナリサイズとパフォーマンスのバランスを取ります。

July 16, 2026· 1 min read· 出典: Redowan's Reflections
GoのGCシェイプステンシリング:ジェネリクスの中間領域

Goのジェネリクス実装は、完全なモノモーフィゼーション(Rust、C++)と型消去(Java)の間の実用的な中間経路を取ります。この手法はGCシェイプステンシリングと呼ばれ、具体的な型引数ごとではなく、GCシェイプごとに1つの関数本体をコンパイルします。これにより、バイナリサイズを管理可能に保ちながら、ボクシングやキャストのランタイムオーバーヘッドを回避します。

GCシェイプの仕組み

型のGCシェイプは、そのサイズ、アライメント、ポインタレイアウトによって決まります。つまり、ガベージコレクタがどのように見るかです。2つの型は、同じ基本型を持つ場合にGCシェイプを共有しますが、1つの重要な例外があります。すべてのポインタ型は*uint8という名前の単一のシェイプに統合されます。つまり、*User*Orderは1つのコンパイル済み本体を共有し、intfloat64はそれぞれ独自の本体を持ちます。

コンパイラはステンシリングを実行します。各異なるGCシェイプを型パラメータに置き換え、シェイプごとに1つの関数バージョンを生成します。関数本体が正確な型を知る必要がある場合(メソッド呼び出しやリフレクションなど)、Goは隠れたディクショナリ引数(各具体的なインスタンス化のランタイム型記述子を含む読み取り専用テーブル)を渡します。

他の手法とのトレードオフ

完全なモノモーフィゼーション(Rust、C++)は型ごとに最適なコードを生成しますが、バイナリを肥大化させる可能性があります。型消去(Java)は単一の本体を維持しますが、プリミティブのボクシングとランタイムキャストを強制します。GCシェイプステンシリングはその中間に位置します。完全なモノモーフィゼーションよりは少ないが複数の本体を生成し、ディクショナリを通じてランタイム型情報の喪失を回避します。

例えば、intfloat64*User*Orderidentityを呼び出すと、3つの関数本体(int用1つ、float64用1つ、ポインタ用に共有1つ)と4つのディクショナリが生成されます。ポインタ型はコードを共有しますが、各インスタンス化は型固有の操作のために独自のディクショナリを取得します。

実用的な影響

この設計により、Goのジェネリクスは型抽象化によるランタイムオーバーヘッド(ボクシングやキャスト)を発生させず、完全なモノモーフィゼーションよりもコンパイル時間とバイナリサイズを低く抑えます。トレードオフとして、コンパイラはディクショナリを管理する必要があり、型固有の動作に大きく依存するジェネリック関数では若干の間接参照が増える可能性があります。ほとんどの実用的なコードでは、この手法は適切なバランスを実現します。

Goのジェネリクス提案は実装戦略をオープンにしていました。GCシェイプステンシリングはGo 1.18で出荷され、現在もデフォルトです。これは、理論的な純粋性よりも実用的なエンジニアリングを重視する言語の哲学を反映しています。