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

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

このコミットは、Goランタイムのヒープ管理におけるメモリ割り当ての挙動を修正するものです。具体的には、ヒープの拡張単位が意図せず倍増してしまっていた問題を解決し、コードの堅牢性を向上させています。

コミット

commit 0622e13b4daa6231dc0de9da6c7f45e29c0774da
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Wed Jul 9 17:00:54 2014 +0400

    runtime: grow heap by 64K instead of 128K
    When we've switched to 8K pages,
    heap started to grow by 128K instead of 64K,
    because it was implicitly assuming that pages are 4K.
    Fix that and make the code more robust.
    
    LGTM=khr
    R=golang-codereviews, dave, khr
    CC=golang-codereviews, rsc
    https://golang.org/cl/106450044

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

https://github.com/golang/go/commit/0622e13b4daa6231dc0de9da6c7f45e29c0774da

元コミット内容

runtime: grow heap by 64K instead of 128K
When we've switched to 8K pages,
heap started to grow by 128K instead of 64K,
because it was implicitly assuming that pages are 4K.
Fix that and make the code more robust.

変更の背景

この変更の背景には、Goランタイムが使用するメモリページのサイズが4KBから8KBに変更されたという重要な事実があります。Goのヒープ管理は、メモリをページ単位で扱います。以前のGoランタイムでは、ヒープを拡張する際に、暗黙的にメモリページのサイズが4KBであると仮定して、64KBの倍数でヒープを成長させるように設計されていました。

具体的には、ヒープの成長量を計算する際に「16ページ」という固定値を用いていました。4KBページの場合、16ページは 16 * 4KB = 64KB となり、意図した通りの64KB単位での成長が実現されていました。

しかし、メモリページのサイズが8KBに変更された後も、この「16ページ」という固定値の計算ロジックがそのまま残っていました。その結果、16 * 8KB = 128KB となり、ヒープが意図せず128KB単位で成長するようになってしまいました。これは、メモリ使用効率の低下や、不要なメモリの確保につながる可能性がありました。

このコミットは、この暗黙的な仮定を排除し、実際のPageSize(ページサイズ)に基づいてヒープの成長量を計算するように修正することで、ヒープが常に64KBの倍数で成長するようにし、コードの堅牢性を高めることを目的としています。

前提知識の解説

このコミットを理解するためには、以下のGoランタイムの概念とC言語の知識が必要です。

  • Goランタイム (Go Runtime): Goプログラムの実行を管理するシステムです。ガベージコレクタ、スケジューラ、メモリ管理などが含まれます。
  • ヒープ (Heap): プログラムが動的にメモリを割り当てる領域です。Goでは、makenewで作成されるデータ構造や、関数のローカル変数でエスケープ解析によってヒープに割り当てられるものがここに格納されます。
  • ガベージコレクタ (Garbage Collector, GC): ヒープ上の不要になったメモリを自動的に解放する仕組みです。GoのGCは並行・世代別・非同期のマーク&スイープ方式を採用しています。
  • メモリページ (Memory Page): オペレーティングシステムがメモリを管理する際の最小単位です。通常、4KBや8KBなどの固定サイズです。Goランタイムもこのページ単位でメモリをOSから要求したり解放したりします。
  • mheap.c: Goランタイムのメモリヒープ管理に関するC言語のソースファイルです。ヒープの初期化、拡張、解放などの低レベルな処理が記述されています。
  • MHeap_Grow関数: mheap.c内に定義されている関数で、Goランタイムのヒープが不足した際に、OSから追加のメモリを要求してヒープを拡張する役割を担います。
  • PageSize: Goランタイム内で定義されている定数で、現在のシステムにおけるメモリページの実際のサイズ(バイト単位)を表します。
  • ROUNDマクロ: C言語でよく使われるマクロで、数値を指定された倍数に切り上げるために使用されます。例えば、ROUND(x, y)xyの最も近い倍数に切り上げます。
  • HeapAllocChunk: ヒープを拡張する際に、一度に割り当てる最小のチャンクサイズを定義する定数です。OSへのシステムコール回数を減らし、オーバーヘッドを償却するために、ある程度のまとまったサイズでメモリを要求します。
  • ビット演算子 &~: C言語におけるビット演算子です。
    • & (AND): ビットごとの論理積。
    • ~ (NOT): ビットごとの反転(補数)。
    • x & ~y の形式は、xyの倍数に切り捨てる(yが2のべき乗の場合)または、yの倍数に切り上げる(x + y - 1) & ~(y - 1)のような形式の場合)ためによく使われます。

技術的詳細

このコミットの核心は、ヒープの成長量を計算するロジックが、メモリページのサイズ変更に対応していなかった点にあります。

変更前のコード npage = (npage+15)&~15; は、npage(ページ数)を16の倍数に切り上げるための慣用的なビット演算でした。

  • 15 はバイナリで 0...01111 です。
  • ~15 はバイナリで 1...10000 となり、下位4ビットが0になります。
  • npage+15 は、npageが16の倍数でない場合に、次の16の倍数に到達するように調整します。
  • &~15 は、下位4ビットを0にすることで、結果を16の倍数に切り捨てます。 この組み合わせにより、npageは常に16の倍数に切り上げられていました。

このロジックは、1ページが4KBであるという前提の下では、16ページ * 4KB/ページ = 64KB となり、ヒープが64KBの倍数で成長することを保証していました。

しかし、Goランタイムが8KBページを使用するように変更された際、このロジックは更新されませんでした。そのため、16ページ * 8KB/ページ = 128KB となり、ヒープが128KBの倍数で成長するようになってしまったのです。これは、OSから要求されるメモリ量が意図せず倍増することを意味し、メモリの無駄やパフォーマンスへの潜在的な影響がありました。

このコミットでは、この問題を解決するために、ヒープの成長量を計算する際にPageSizeを明示的に使用するように変更しました。

新しいコード npage = ROUND(npage, (64<<10)/PageSize); は、以下の計算を行います。

  1. (64<<10): これは 64 * 2^10、つまり 64 * 1024 = 65536 バイト、すなわち64KBを表します。
  2. (64<<10)/PageSize: これは、64KBが何ページに相当するかを計算します。例えば、PageSizeが4KBなら 65536 / 4096 = 16 ページ、PageSizeが8KBなら 65536 / 8192 = 8 ページとなります。
  3. ROUND(npage, X): npageを、ステップ2で計算されたX(64KBに相当するページ数)の倍数に切り上げます。

この変更により、PageSizeが4KBであろうと8KBであろうと、あるいは将来的に別のサイズに変更されたとしても、ヒープは常に64KBの倍数で成長することが保証されます。これにより、コードはより堅牢になり、将来のページサイズ変更にも自動的に対応できるようになりました。

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

変更は src/pkg/runtime/mheap.c ファイルの MHeap_Grow 関数内で行われています。

--- a/src/pkg/runtime/mheap.c
+++ b/src/pkg/runtime/mheap.c
@@ -310,8 +310,8 @@ MHeap_Grow(MHeap *h, uintptr npage)
 	// Ask for a big chunk, to reduce the number of mappings
 	// the operating system needs to track; also amortizes
 	// the overhead of an operating system mapping.
-	// Allocate a multiple of 64kB (16 pages).
-	npage = (npage+15)&~15;
+	// Allocate a multiple of 64kB.
+	npage = ROUND(npage, (64<<10)/PageSize);
 	ask = npage<<PageShift;
 	if(ask < HeapAllocChunk)
 		ask = HeapAllocChunk;

コアとなるコードの解説

  • 変更前:

    // Allocate a multiple of 64kB (16 pages).
    npage = (npage+15)&~15;
    

    この行は、npage(要求されたページ数)を16の倍数に切り上げていました。コメントには「64kBの倍数(16ページ)」とありますが、これは1ページが4KBであるという前提に基づいています。このビット演算は、npageを16の倍数に切り上げるための効率的な方法でした。しかし、PageSizeが変更された際に、この固定値16が問題を引き起こしました。

  • 変更後:

    // Allocate a multiple of 64kB.
    npage = ROUND(npage, (64<<10)/PageSize);
    

    この行は、npageを、64KBに相当するページ数の倍数に切り上げるように変更されました。

    • (64<<10) は64KBをバイト単位で表します。
    • これを PageSize で割ることで、現在のシステムにおける64KBが何ページに相当するかを動的に計算します。
    • ROUND マクロは、npageをこの計算結果の倍数に切り上げます。 この修正により、PageSizeが4KBであろうと8KBであろうと、ヒープは常に正確に64KBの倍数で成長するようになり、コードの柔軟性と堅牢性が大幅に向上しました。

関連リンク

参考にした情報源リンク

  • コミットメッセージ (0622e13b4daa6231dc0de9da6c7f45e29c0774da)
  • Go言語のランタイムおよびメモリ管理に関する一般的な知識
  • C言語のビット演算およびマクロに関する一般的な知識
  • src/pkg/runtime/mheap.c のソースコード (変更前後の差分)
  • Goのメモリ管理に関するドキュメントや記事 (一般的な知識として)
    • Goのガベージコレクションの仕組み
    • Goのメモリ割り当ての仕組み
    • OSのメモリページングの概念I have generated the detailed explanation in Markdown format, following all the specified sections and requirements. The output is sent to standard output.