Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 14945] ファイルの概要

このコミットは、Go言語のリンカである cmd/5l におけるメモリ使用量の最適化に関するものです。具体的には、Adr (アドレス) および Prog (プログラム命令) 構造体のサイズを削減するために、offset2 フィールドを Adr 構造体内の u0 共用体 (union) の中に移動しています。これにより、リンカのメモリフットプリントが削減され、特に大規模なGoプログラムのリンク時におけるメモリ効率が向上します。

コミット

commit e00c9f0dbb7b53410308101311bd0a448e00e17b
Author: Shenghou Ma <minux.ma@gmail.com>
Date:   Tue Jan 22 02:50:27 2013 +0800

    cmd/5l: move offset2 into Adr.u0 union to save 4/8 bytes for Adr/Prog resp.
    sizeof(Adr) from 24 bytes down to 20 bytes.
    sizeof(Prog) from 84 bytes down to 76 bytes.
    
    5l linking cmd/godoc statistics:
    Before:
    Maximum resident set size (kbytes): 106668
    After:
    Maximum resident set size (kbytes):  99412
    
    R=golang-dev, dave
    CC=golang-dev
    https://golang.org/cl/7100059

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/e00c9f0dbb7b53410308101311bd0a448e00e17b

元コミット内容

Go言語のリンカ cmd/5l において、Adr 構造体と Prog 構造体のメモリ使用量を削減するため、offset2 フィールドを Adr 構造体内の u0 共用体 (union) の中に移動しました。この変更により、Adr 構造体のサイズは24バイトから20バイトに、Prog 構造体のサイズは84バイトから76バイトに削減されました。結果として、cmd/godoc のリンク時における最大常駐セットサイズ (Maximum resident set size) が106668KBから99412KBに減少し、約7MBのメモリ削減が達成されました。

変更の背景

この変更の主な背景は、Go言語のリンカ cmd/5l のメモリ効率を向上させることです。リンカは、コンパイルされたオブジェクトファイルを結合して実行可能ファイルを生成するプロセスにおいて、大量のデータ構造(AdrProg など)をメモリ上に保持します。これらのデータ構造のサイズがわずかでも削減されれば、リンクプロセス全体のメモリフットプリントが大幅に減少する可能性があります。

特に、cmd/godoc のような比較的大きなGoプログラムをリンクする際に、リンカが消費するメモリ量が問題となることがありました。メモリ使用量の削減は、ビルド時間の短縮、より少ないリソースでのビルド環境の実現、および大規模プロジェクトにおけるスケーラビリティの向上に貢献します。

このコミットは、Adr 構造体内の offset2 フィールドが、u0 共用体内の他のフィールド (u0offset, u0sval など) と同時に使用されることがない、あるいは特定のコンテキストでのみ使用されるという前提に基づいています。共用体を使用することで、異なる目的で使われるが同時に必要とされないフィールドが同じメモリ領域を共有できるようになり、構造体全体のサイズを削減できます。

前提知識の解説

このコミットを理解するためには、以下の概念についての知識が役立ちます。

  • Go言語のツールチェインとリンカ (cmd/5l):

    • Go言語のビルドプロセスは、ソースコードをコンパイルし、その結果生成されるオブジェクトファイルをリンカが結合して実行可能ファイルを生成します。
    • cmd/5l は、Go言語のツールチェインの一部であり、ARMアーキテクチャ (Plan 9 from Bell Labsの命名規則では5がARMを指す) 向けのリンカです。Goのツールチェインはクロスコンパイルをサポートしており、各アーキテクチャに対応するリンカが存在します(例: 6l はamd64、8l は386)。
    • リンカは、プログラム内のシンボル解決、アドレスの再配置、ライブラリの結合などを行います。この過程で、プログラムの命令やデータに関する情報を内部的なデータ構造として表現します。
  • C言語の構造体 (struct) と共用体 (union):

    • 構造体 (struct): 複数の異なる型の変数を一つにまとめた複合データ型です。構造体のサイズは、そのメンバーの型のサイズの合計(およびアライメントによるパディング)によって決まります。各メンバーは独立したメモリ領域を占めます。
    • 共用体 (union): 複数の異なる型の変数を一つにまとめた複合データ型ですが、構造体とは異なり、すべてのメンバーが同じメモリ領域を共有します。共用体のサイズは、その中で最も大きいメンバーのサイズによって決まります。共用体のメンバーは一度に一つしか有効な値を保持できません。この特性を利用して、メモリを節約することができます。
  • Adr 構造体と Prog 構造体:

    • Go言語のリンカ内部で使用されるデータ構造です。
    • Adr (Address) 構造体は、命令のオペランド(引数)やデータのアドレスを表すために使用されます。これには、オフセット、シンボルへのポインタ、型情報などが含まれます。
    • Prog (Program) 構造体は、アセンブリ命令そのものを表します。各 Prog は、命令コード、ソースとデスティネーションのオペランド(これらが Adr 構造体で表現される)、およびその他の命令固有の情報を持ちます。
    • リンカは、プログラム全体を Prog 構造体のリストとして表現し、各 Prog が参照するアドレス情報を Adr 構造体で管理します。
  • sizeof 演算子:

    • C言語において、データ型や変数が占めるメモリのバイト数を返す演算子です。構造体や共用体のサイズを計算する際に使用されます。
  • 最大常駐セットサイズ (Maximum Resident Set Size, RSS):

    • プロセスが物理メモリ(RAM)上で占有している最大量を示す指標です。これは、プロセスが実際に使用しているメモリの量であり、仮想メモリとは異なります。リンカのようなメモリを大量に消費するアプリケーションでは、この値がパフォーマンスに直結します。単位は通常キロバイト (KB) です。
  • int32:

    • 32ビット符号付き整数型です。通常4バイトのメモリを占めます。

技術的詳細

このコミットの核心は、Adr 構造体の定義変更にあります。変更前と変更後の Adr 構造体の定義を比較することで、メモリ削減のメカニズムを理解できます。

変更前の Adr 構造体 (src/cmd/5l/l.h より抜粋):

struct Adr
{
    union
    {
        int32   u0offset;
        char*   u0sval;
        Ieee    u0ieee;
        char*   u0sbig;
    } u0;
    Sym*    sym;
    Sym*    gotype;
    int32   offset2; // argsize
    char    type;
    char    reg;
    char    name;
    char    class;
};

#define offset  u0.u0offset

この定義では、u0 共用体の外に int32 offset2; というフィールドが独立して存在していました。u0 共用体は、u0offset (32ビット整数)、u0sval (ポインタ)、u0ieee (浮動小数点数)、u0sbig (ポインタ) のいずれかを保持できます。共用体のサイズは、その中で最も大きいメンバーのサイズによって決まります。仮にポインタが8バイト、int32が4バイト、Ieeeが8バイトとすると、u0共用体は8バイトを占めます。

したがって、変更前の Adr 構造体のサイズは、u0 (8バイト) + sym (ポインタ、8バイト) + gotype (ポインタ、8バイト) + offset2 (4バイト) + type (1バイト) + reg (1バイト) + name (1バイト) + class (1バイト) となり、アライメントを考慮すると合計24バイトになります。

変更後の Adr 構造体 (src/cmd/5l/l.h より抜粋):

struct Adr
{
    union
    {
        struct {
            int32   offset;
            int32   offset2; // argsize
        } u0off;
        char*   u0sval;
        Ieee    u0ieee;
        char*   u0sbig;
    } u0;
    Sym*    sym;
    Sym*    gotype;
    char    type;
    char    reg;
    char    name;
    char    class;
};

#define offset  u0.u0off.offset
#define offset2 u0.u0off.offset2

変更後では、offset2 フィールドが u0 共用体の中に新しく導入された匿名構造体 u0off のメンバーとして移動されました。u0off 構造体は int32 offset;int32 offset2; の2つの int32 メンバーを持ち、合計8バイトを占めます(アライメントを考慮しない場合)。

この変更のポイントは、offsetoffset2 が同時に使用される場合に、これらが u0 共用体内の他のメンバー (u0sval, u0ieee, u0sbig) とは排他的に使用されるという前提があることです。つまり、Adr 構造体が offsetoffset2 のペアを必要とする場合、それは文字列値 (u0sval) や浮動小数点値 (u0ieee) を必要としないコンテキストである、ということです。

これにより、u0 共用体の最大サイズは変わらず(依然としてポインタや Ieee のサイズに依存)、しかし offset2 が共用体の外から中へ移動したため、構造体全体のサイズから offset2 の4バイト分が削減されます。

結果として、Adr 構造体のサイズは24バイトから20バイトに削減されます。

Prog 構造体への影響:

Prog 構造体は、そのメンバーとして Adr 構造体を含んでいます。リンカの内部では、多くの Prog 構造体が生成されます。Prog 構造体は通常、ソースオペランドとデスティネーションオペランドのために複数の Adr 構造体を持つため、Adr 構造体1つあたりのサイズ削減が、Prog 構造体全体のサイズに累積的に影響します。

コミットメッセージによると、Prog 構造体のサイズは84バイトから76バイトに削減されたとあります。これは、Prog 構造体が2つの Adr 構造体(ソースとデスティネーション)を持つ場合、2 * 4バイト = 8バイト の削減となり、コミットメッセージの数値と一致します。

このメモリ削減は、リンカが処理する命令やアドレスの数が多いほど顕著な効果を発揮します。cmd/godoc のリンク統計が示しているように、約7MBの常駐セットサイズの削減は、大規模なGoアプリケーションのビルドにおいて無視できない改善です。

コアとなるコードの変更箇所

変更は src/cmd/5l/l.h ファイルに集中しています。

--- a/src/cmd/5l/l.h
+++ b/src/cmd/5l/l.h
@@ -68,21 +68,24 @@ struct  Adr
 {
     union
     {
-        int32   u0offset;
+        struct {
+            int32   offset;
+            int32   offset2; // argsize
+        } u0off;
         char*   u0sval;
         Ieee    u0ieee;
         char*   u0sbig;
     } u0;
     Sym*    sym;
     Sym*    gotype;
-    int32   offset2; // argsize
     char    type;
     char    reg;
     char    name;
     char    class;
 };
 
-#define offset  u0.u0offset
+#define offset  u0.u0off.offset
+#define offset2 u0.u0off.offset2
 #define sval    u0.u0sval
 #define scon    sval
 #define ieee    u0.u0ieee

コアとなるコードの解説

  1. Adr 構造体の変更:

    • 元のコードでは、u0 共用体の直後に int32 offset2; が独立したメンバーとして定義されていました。
    • 変更後のコードでは、int32 offset2;u0 共用体の中に新しく定義された匿名構造体 u0off のメンバーとして移動されました。
    • u0off 構造体は int32 offset;int32 offset2; の2つのメンバーを持ちます。これにより、offsetoffset2 が論理的に関連付けられ、かつメモリを共有する u0 共用体の一部となります。
  2. #define マクロの変更:

    • 元の #define offset u0.u0offset は、u0 共用体内の u0offset メンバーを直接参照していました。
    • 変更後の #define offset u0.u0off.offset は、u0 共用体内の u0off 構造体を経由して offset メンバーを参照するように変更されました。
    • 新しく #define offset2 u0.u0off.offset2 が追加され、offset2 メンバーも同様に u0off 構造体を経由して参照できるようになりました。
    • これらのマクロは、コードの可読性を保ちつつ、基盤となる構造体メンバーへのアクセスを簡素化するために使用されます。

この変更により、offsetoffset2 が同時に使用される場合でも、それらが u0 共用体の他のメンバー(u0svalu0ieee など)とは排他的にメモリを共有するため、Adr 構造体全体のサイズが削減されます。これは、リンカが Adr 構造体のインスタンスを多数作成する際に、メモリ使用量を効率的に削減するための典型的なC言語の最適化手法です。

関連リンク

参考にした情報源リンク

  • 特になし (提供されたコミット情報と一般的なプログラミング知識に基づいています)