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

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

このコミットは、Go言語のリンカである5lcmd/5l)におけるメモリ消費量の削減を目的としたものです。具体的には、Adr構造体のフィールドの順序を変更することで、メモリのアライメントとパディングによる無駄を削減し、ヒープメモリの使用量を最適化しています。

コミット

commit 1e9f3085457eb911cb46a13e2766697bddd9d413
Author: Shenghou Ma <minux.ma@gmail.com>
Date:   Fri Oct 12 13:39:12 2012 +0800

    cmd/5l: reorder some struct fields to reduce memory consumption
    Valgrind Massif result when linking godoc:
    On amd64:
                        old          new         -/+
    mem_heap_B       185844612    175358047    -5.7%
    mem_heap_extra_B    773404       773137    -0.0%
    
    On 386/ARM:
                        old          new         -/+
    mem_heap_B       141775701    131289941    -7.4%
    mem_heap_extra_B    737011       736955    -0.0%
    
    R=golang-dev, r, dave
    CC=golang-dev
    https://golang.org/cl/6655045

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

https://github.com/golang/go/commit/1e9f3085457eb911cb46a13e2766697bddd9d413

元コミット内容

cmd/5l: メモリ消費量を削減するためにいくつかの構造体フィールドを並べ替える。 godocをリンクする際のValgrind Massifの結果:

amd64の場合:

oldnew-/+
mem_heap_B185844612175358047-5.7%
mem_heap_extra_B773404773137-0.0%

386/ARMの場合:

oldnew-/+
mem_heap_B141775701131289941-7.4%
mem_heap_extra_B737011736955-0.0%

レビュー担当者: golang-dev, r, dave CC: golang-dev 関連する変更リスト: https://golang.org/cl/6655045

変更の背景

このコミットの背景には、Go言語のツールチェイン、特にリンカ(5lgo tool linkの内部的な実装の一部)のメモリ効率を改善するという明確な目的があります。リンカは、プログラムのビルド時に大量のシンボル情報やアドレス情報を処理するため、多くのメモリを消費する傾向があります。特に大規模なプロジェクトや、godocのような多くの依存関係を持つツールをリンクする際には、そのメモリ使用量が顕著になります。

コミットメッセージに記載されているValgrind Massifのプロファイリング結果は、この最適化の必要性を示しています。Valgrind Massifはヒープメモリの使用状況を詳細に分析するツールであり、この結果から、構造体のフィールドの並べ替えによってヒープメモリの使用量が大幅に削減されることが確認されています(amd64で5.7%、386/ARMで7.4%)。これは、ビルド時間の短縮や、メモリが限られた環境でのビルドの成功に貢献します。

前提知識の解説

このコミットを理解するためには、以下の前提知識が必要です。

  1. 構造体(Struct): 複数の異なる型のデータを一つにまとめた複合データ型です。Go言語ではstructキーワードで定義されます。
  2. メモリのアライメント(Memory Alignment): コンピュータのプロセッサがメモリからデータを効率的に読み書きするために、特定のデータ型がメモリ上で特定のバイト境界に配置されるようにする規則です。例えば、4バイトの整数は4バイト境界に、8バイトのポインタは8バイト境界に配置されることが一般的です。これにより、プロセッサは一度のメモリアクセスでデータを取得でき、パフォーマンスが向上します。
  3. メモリのパディング(Memory Padding): メモリのアライメント要件を満たすために、構造体のフィールド間や構造体の末尾に挿入される未使用のバイトのことです。例えば、4バイト境界に配置されるべきフィールドの前に1バイトのフィールドがある場合、その間に3バイトのパディングが挿入されることがあります。このパディングはメモリを消費しますが、データアクセスを高速化するために必要です。
  4. Valgrind Massif: Valgrindは、メモリ管理エラーの検出やプロファイリングを行うためのオープンソースのツールスイートです。Massifはその一部で、プログラムのヒープメモリ使用量を詳細にプロファイリングし、どのコードがどれだけのメモリを割り当てているか、時間の経過とともにどのように変化するかを可視化します。これにより、メモリリークや過剰なメモリ使用の原因を特定するのに役立ちます。
  5. Go言語のリンカ(cmd/5l: Go言語のビルドプロセスにおいて、コンパイルされたオブジェクトファイル(.oファイル)を結合し、実行可能なバイナリを生成するツールです。5lは、Goの初期のツールチェインにおけるx86-64アーキテクチャ(amd64)向けのリンカの名称でした。Goのツールチェインは、各アーキテクチャ(例: 5はx86-64、6はARMなど)に対応するリンカを持っていました。

技術的詳細

このコミットの技術的な核心は、構造体フィールドの並べ替えによるメモリパディングの最適化です。

プロセッサは、メモリからデータを読み込む際に、特定のバイト境界(アライメント)に配置されたデータを効率的に処理します。例えば、64ビットシステムでは、8バイトのデータ(ポインタなど)は8バイトの倍数のアドレスに配置されることが理想的です。

構造体を定義する際、コンパイラは各フィールドをその型のアライメント要件に従って配置しようとします。もしフィールドの順序が適切でない場合、アライメント要件を満たすために、コンパイラはフィールド間に「パディング」と呼ばれる未使用のバイトを挿入します。このパディングはメモリを消費しますが、プログラムからはアクセスできません。

例として、以下のような構造体を考えます(Go言語の型サイズは一般的なものと仮定)。

struct Example1 {
    byte  b;  // 1バイト
    int32 i;  // 4バイト
    int64 l;  // 8バイト
}

この構造体がメモリに配置される場合、以下のようなパディングが発生する可能性があります(8バイトアライメントを仮定):

  • b (1バイト)
  • パディング (3バイト) - iを4バイト境界に配置するため
  • i (4バイト)
  • パディング (0バイト) - lは既に8バイト境界に配置可能
  • l (8バイト)

合計で1バイト + 3バイト + 4バイト + 8バイト = 16バイトとなり、実データは13バイトですが、3バイトのパディングが発生します。

一方、フィールドをサイズが大きい順に並べ替えると、パディングを最小限に抑えることができます。

struct Example2 {
    int64 l;  // 8バイト
    int32 i;  // 4バイト
    byte  b;  // 1バイト
}

この場合、メモリ配置は以下のようになります:

  • l (8バイト)
  • i (4バイト)
  • b (1バイト)
  • パディング (3バイト) - 構造体全体のサイズを8バイトの倍数にするため(構造体のアライメント要件による)

合計で8バイト + 4バイト + 1バイト + 3バイト = 16バイトとなり、この例では同じサイズですが、より複雑な構造体ではパディングが大幅に削減されることがあります。特に、ポインタ(通常8バイト)と小さな型(1バイトや4バイト)が混在する場合に効果的です。

このコミットでは、src/cmd/5l/l.h内のAdr構造体において、Sym* gotype(ポインタ、通常8バイト)とint32 offset2(4バイト)を、char type(1バイト)やchar reg(1バイト)などの小さなフィールドの前に移動させています。これにより、これらの大きなフィールドが適切なアライメントで配置されやすくなり、その結果として発生するパディングが削減され、全体のメモリ消費量が減少したと考えられます。

Valgrind Massifの結果は、この最適化が実際にヒープメモリの使用量を削減したことを定量的に示しており、特にmem_heap_B(ヒープに割り当てられたバイト数)が顕著に減少しています。これは、リンカが内部的にAdr構造体のインスタンスを多数生成・使用しているため、個々の構造体のサイズ削減が全体として大きな効果をもたらしたことを意味します。

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

変更はsrc/cmd/5l/l.hファイル内のAdr構造体の定義にあります。

--- a/src/cmd/5l/l.h
+++ b/src/cmd/5l/l.h
@@ -74,13 +74,12 @@ struct	Adr
 		char*	u0sbig;
 	} u0;
 	Sym*	sym;
+	Sym*	gotype;
+	int32	offset2; // argsize
 	char	type;
-	uchar	index; // not used on arm, required by ld/go.c
 	char	reg;
 	char	name;
-	int32	offset2; // argsize
 	char	class;
-	Sym*	gotype;
 };
 
 #define	offset	u0.u0offset

具体的には、以下のフィールドの順序が変更されています。

  • Sym* gotype;
  • int32 offset2; // argsize

これらの2つのフィールドが、元の位置(char class;の下)から、Sym* sym;の直後、かつchar type;の前に移動されています。

また、uchar index;フィールドは削除されていますが、コミットメッセージの意図はメモリ消費量の削減のためのフィールドの並べ替えであり、この削除は直接的な並べ替えとは異なる変更です。しかし、// not used on arm, required by ld/go.cというコメントから、このフィールドが特定のアーキテクチャで不要になったか、あるいは別の方法で処理されるようになったため、削除された可能性があります。この削除もまた、構造体のサイズ削減に貢献します。

コアとなるコードの解説

変更前のAdr構造体は以下のようになっていました(関連部分のみ抜粋):

struct Adr {
    // ...
    Sym*    sym;
    char    type;
    uchar   index; // not used on arm, required by ld/go.c
    char    reg;
    char    name;
    int32   offset2; // argsize
    char    class;
    Sym*    gotype;
};

変更後のAdr構造体は以下のようになりました:

struct Adr {
    // ...
    Sym*    sym;
    Sym*    gotype;    // 移動
    int32   offset2;   // 移動
    char    type;
    // uchar index; // 削除
    char    reg;
    char    name;
    char    class;
    // Sym* gotype;    // 元の位置から移動
    // int32 offset2; // 元の位置から移動
};

この変更の主な目的は、Sym* gotype(ポインタ型)とint32 offset2(32ビット整数型)という比較的サイズの大きいフィールドを、char typechar regchar namechar classといった1バイトのフィールドよりも前に配置することです。

一般的なシステムでは、ポインタは8バイト、int32は4バイトのアライメント要件を持つことが多いです。一方、charは1バイトのアライメントです。

変更前は、Sym* symの後にchar typeが来ており、その後にint32 offset2Sym* gotypeが配置されていました。これにより、charフィールドの後に、より大きなアライメント要件を持つフィールドが続く場合、コンパイラはパディングを挿入してアライメントを調整する必要がありました。

例えば、char classの後にSym* gotypeが来る場合、char classが1バイトなので、Sym* gotype(8バイト)を8バイト境界に配置するために7バイトのパディングが必要になる可能性があります。

変更後は、Sym* sym(ポインタ)の直後にSym* gotype(ポインタ)とint32 offset2(4バイト)が配置されています。これにより、これらの大きなフィールドが連続して配置され、その後に小さなcharフィールドが続く形になります。この順序は、コンパイラがパディングを最小限に抑えるのに有利に働きます。

uchar index;の削除も、構造体全体のサイズを削減する効果があります。コメントにあるように、このフィールドがARMアーキテクチャで不要になったのであれば、削除は妥当な最適化です。

結果として、このフィールドの並べ替えと不要なフィールドの削除により、Adr構造体のインスタンスあたりのメモリ消費量が削減され、Valgrind Massifのプロファイリング結果が示すように、リンカ全体のヒープメモリ使用量が大幅に減少しました。これは、Go言語のツールチェインの効率性を高めるための重要な最適化です。

関連リンク

参考にした情報源リンク