[インデックス 14335] ファイルの概要
このコミットは、Go言語のランタイムにおけるデータ競合検出器(Race Detector)のシャドウメモリ割り当て方法を、起動時の即時割り当てから遅延割り当てへと変更するものです。これにより、特にWindows環境でのメモリ使用効率が改善され、top
コマンドなどで表示される仮想メモリ消費量の見かけ上の問題も解消されます。
コミット
commit 1a19f01a683f8c62b7bd5f843a2e1b7ed6449542
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Wed Nov 7 12:48:58 2012 +0400
runtime/race: lazily allocate shadow memory
Currently race detector runtime maps shadow memory eagerly at process startup.
It works poorly on Windows, because Windows requires reservation in swap file
(especially problematic if several Go program runs at the same, each consuming GBs
of memory).
With this change race detector maps shadow memory lazily, so Go runtime must notify
about all new heap memory.
It will help with Windows port, but also eliminates scary 16TB virtual mememory
consumption in top output (which sometimes confuses some monitoring scripts).
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6811085
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1a19f01a683f8c62b7bd5f843a2e1b7ed6449542
元コミット内容
Goのデータ競合検出器は、プロセスの起動時にシャドウメモリを即座に(eagerly)マッピングしていました。この挙動はWindows環境で問題を引き起こしていました。Windowsでは、仮想メモリの予約がスワップファイルに反映されるため、複数のGoプログラムが同時に実行されると、それぞれが数ギガバイトのメモリを消費し、スワップファイルが肥大化するという問題がありました。
このコミットにより、データ競合検出器はシャドウメモリを遅延的に(lazily)マッピングするようになります。これにより、Goランタイムは新しいヒープメモリが割り当てられるたびに、その情報をデータ競合検出器に通知する必要があります。この変更はWindowsへの移植性を向上させるだけでなく、top
コマンドなどで表示される「16TB」といった見かけ上の巨大な仮想メモリ消費量(これは一部の監視スクリプトを混乱させる可能性がありました)を解消する効果もあります。
変更の背景
Goのデータ競合検出器は、Googleが開発したThreadSanitizer (TSan) というツールをベースにしています。TSanは、プログラムのメモリアクセスを監視し、データ競合を検出するために「シャドウメモリ」という概念を使用します。シャドウメモリは、プログラムの実際のメモリ領域(ヒープ、スタック、グローバル変数など)の各バイトまたはワードに対応する追加のメモリ領域で、そのメモリ位置へのアクセス履歴(読み書き、スレッドID、タイムスタンプなど)を記録します。
従来のGoランタイムでは、データ競合検出器が有効な場合、プログラム起動時に必要なシャドウメモリ領域全体を一度に仮想アドレス空間にマッピングしていました。シャドウメモリは、監視対象のメモリ領域に対して一定の比率(例えば、8バイトのメモリに対して1バイトのシャドウメモリ)で割り当てられるため、大規模なプログラムや、多くのメモリを使用するプログラムでは、非常に広大な仮想アドレス空間が必要となります。
この「即時割り当て」のアプローチは、特にWindowsオペレーティングシステムにおいて問題を引き起こしていました。Windowsのメモリ管理は、仮想メモリの予約(VirtualAlloc
のMEM_RESERVE
フラグ)であっても、対応するスワップファイル領域を事前に確保しようとする傾向があります。そのため、Goのデータ競合検出器が有効なプログラムが起動すると、たとえ実際に使用されていないシャドウメモリ領域であっても、その広大な仮想アドレス空間に対応するスワップファイル領域が予約されてしまい、システムのリソースを過剰に消費したり、スワップファイルが肥大化したりする原因となっていました。
また、LinuxなどのUnix系システムでは、仮想メモリの予約は通常、実際にページがアクセスされるまで物理メモリやスワップ領域を消費しません(オーバーコミット)。しかし、top
コマンドなどのシステム監視ツールでは、予約された仮想メモリ空間全体がプロセスに割り当てられているかのように表示されることがあります。これにより、データ競合検出器が有効なGoプログラムが「16TB」といった途方もない仮想メモリを消費しているように見え、システムの健全性を監視するスクリプトや担当者を混乱させるという、実害はないものの誤解を招く問題がありました。
これらの問題を解決するため、シャドウメモリの割り当てを「遅延的」に行う、つまり実際にヒープメモリが割り当てられ、そのシャドウメモリが必要になった時点で初めてマッピングを行うように変更する必要がありました。
前提知識の解説
-
データ競合 (Data Race): 複数のゴルーチン(またはスレッド)が、同期メカニズムなしに同じメモリ位置に同時にアクセスし、少なくとも一方のアクセスが書き込みである場合に発生するバグです。データ競合はプログラムの予測不能な動作やクラッシュを引き起こす可能性があり、デバッグが非常に困難です。
-
Go Race Detector (データ競合検出器): Go言語に組み込まれているツールで、プログラム実行中にデータ競合を動的に検出します。
go run -race
やgo build -race
のように-race
フラグを付けてビルド・実行することで有効になります。内部的には、LLVMのThreadSanitizer (TSan) を利用しています。 -
ThreadSanitizer (TSan): Googleが開発した動的データ競合検出ツールです。コンパイル時にプログラムのインストルメンテーション(計測コードの挿入)を行い、実行時にメモリアクセスを監視します。TSanは、各メモリワードに対して「シャドウメモリ」と呼ばれる追加のメタデータを保持し、これを使ってアクセス履歴を追跡し、競合を検出します。
-
シャドウメモリ (Shadow Memory): TSanの主要な概念です。プログラムの実際のメモリ(アプリケーションメモリ)の各バイトまたはワードに対して、対応するシャドウメモリが存在します。シャドウメモリには、そのアプリケーションメモリ位置への最後のアクセスに関する情報(アクセスタイプ、スレッドID、タイムスタンプなど)が記録されます。例えば、TSanでは通常、8バイトのアプリケーションメモリに対して1バイトのシャドウメモリが割り当てられます。これにより、アプリケーションメモリが1GBの場合、シャドウメモリは約128MB必要になります。
-
仮想メモリ (Virtual Memory): オペレーティングシステムが提供するメモリ管理の抽象化です。各プロセスは、物理メモリのサイズに関わらず、広大な仮想アドレス空間を持っているかのように振る舞います。仮想アドレスは、メモリ管理ユニット(MMU)によって物理アドレスに変換されます。仮想メモリは、メモリ保護、メモリ共有、そして物理メモリよりも大きなアドレス空間を提供するために使用されます。
-
スワップファイル (Swap File) / ページファイル (Page File): 物理メモリが不足した場合に、OSがメモリページを一時的にディスクに退避させるために使用するファイル(Windowsではページファイル、Linuxではスワップファイルまたはスワップパーティション)。仮想メモリが予約される際、特にWindowsでは、その仮想メモリに対応するスワップファイル領域も事前に予約されることがあります。
-
top
コマンド: Unix系システムで実行中のプロセスをリアルタイムで監視するためのコマンドです。各プロセスのCPU使用率、メモリ使用量(RES: 物理メモリ、VIRT: 仮想メモリ)、プロセスIDなどを表示します。
技術的詳細
このコミットの核心は、Goランタイムのメモリ割り当てメカニズムとデータ競合検出器(TSan)のシャドウメモリ管理の連携を変更することにあります。
変更前(即時割り当て): データ競合検出器が有効な場合、Goプログラムの起動時に、TSanが必要とするシャドウメモリ領域全体が仮想アドレス空間にマッピングされていました。これは、プログラムが将来的に使用する可能性のあるすべてのヒープメモリに対応するシャドウメモリを、事前に確保するアプローチです。
- 問題点:
- Windowsでのスワップファイル予約: Windowsでは、仮想メモリの予約がスワップファイルに反映されるため、未使用のシャドウメモリ領域に対してもスワップファイルが予約され、システムリソースを無駄に消費していました。
top
での仮想メモリ表示: 予約された広大な仮想アドレス空間がtop
などのツールで表示され、実際のメモリ使用量とはかけ離れた巨大な値(例: 16TB)となり、誤解を招いていました。
変更後(遅延割り当て): このコミットにより、シャドウメモリは「遅延的に」割り当てられるようになります。これは、Goランタイムが新しいヒープメモリを実際に割り当てる際に、そのヒープメモリに対応するシャドウメモリ領域をTSanに通知し、TSanがその時点でシャドウメモリをマッピングするというアプローチです。
- 実装の変更点:
- Goランタイムのヒープアロケータ(
runtime·MHeap_SysAlloc
)が、新しいメモリ領域をシステムから取得するたびに、runtime·racemapshadow
という新しい関数を呼び出すようになります。 runtime·racemapshadow
は、TSanの内部関数である__tsan_map_shadow
を呼び出し、新しく割り当てられたヒープメモリ領域とそのサイズをTSanに通知します。- TSanは、この通知を受け取ると、指定されたメモリ領域に対応するシャドウメモリをその場でマッピングします。
- Goランタイムのヒープアロケータ(
- 効果:
- Windowsでの改善: 実際に使用されるヒープメモリに対応するシャドウメモリのみがマッピングされるため、スワップファイルの過剰な予約が解消されます。
top
での表示改善: 仮想メモリの表示が、実際にマッピングされたシャドウメモリの量に近づき、より現実的な値になります。- リソース効率の向上: 未使用のシャドウメモリ領域に対するリソース(仮想アドレス空間、スワップファイル)の消費がなくなります。
この変更は、Goランタイムのメモリ管理とTSanの連携をより密接にし、必要な時に必要なリソースを割り当てるという効率的なアプローチを採用したものです。
コアとなるコードの変更箇所
このコミットでは、主に以下のファイルが変更されています。
-
src/pkg/runtime/malloc.goc
:runtime·MHeap_SysAlloc
関数(Goランタイムがシステムから新しいヒープメモリを割り当てる際に使用される関数)内に、raceenabled
(データ競合検出器が有効かどうかを示すフラグ)が真の場合にruntime·racemapshadow(p, n)
を呼び出すコードが追加されました。p
は割り当てられたメモリのアドレス、n
はそのサイズです。
-
src/pkg/runtime/race.c
:- 新しい関数
runtime·racemapshadow(void *addr, uintptr size)
が追加されました。この関数は、Goランタイムから呼び出され、__tsan_map_shadow
(TSanの内部関数)を呼び出すことで、指定されたメモリ領域のシャドウメモリをマッピングするようTSanに指示します。 runtime·raceinit
関数から、起動時にシャドウメモリ全体をマッピングしていた既存のruntime∕race·MapShadow
の呼び出しが削除されました。これにより、即時割り当てではなくなります。runtime∕race·MapShadow
のプロトタイプ宣言が追加されました。
- 新しい関数
-
src/pkg/runtime/race.h
:runtime·racemapshadow
関数のプロトタイプ宣言が追加されました。
-
src/pkg/runtime/race/race.go
:- CGOディレクティブに
void __tsan_map_shadow(void *addr, void *size);
が追加され、GoコードからTSanの__tsan_map_shadow
関数を呼び出せるようになりました。 - Goの
race
パッケージにMapShadow(addr, size uintptr)
関数が追加されました。この関数は、CGOを介してC.__tsan_map_shadow
を呼び出します。
- CGOディレクティブに
-
src/pkg/runtime/race0.c
:- データ競合検出器が無効なビルドの場合に、
runtime·racemapshadow
のダミー実装(何もしない関数)が追加されました。これにより、raceenabled
が偽の場合でもコンパイルエラーにならないようにします。
- データ競合検出器が無効なビルドの場合に、
コアとなるコードの解説
このコミットの主要な変更は、GoランタイムのヒープアロケータとTSanのシャドウメモリ管理の間のインターフェースを再定義した点にあります。
src/pkg/runtime/malloc.goc
の変更:
// runtime·MHeap_SysAlloc はシステムから新しいメモリ領域を割り当てる関数
runtime·MHeap_SysAlloc(MHeap *h, uintptr n)
{
// ... 既存のメモリ割り当てロジック ...
// 新しく割り当てられたメモリ p, サイズ n に対して
if(raceenabled) // レース検出器が有効な場合
runtime·racemapshadow(p, n); // シャドウメモリをマッピングするよう通知
// ... 既存のメモリ割り当てロジック ...
}
この変更により、GoランタイムがOSから新しいメモリページを取得するたびに、そのメモリ領域がデータ競合検出器に通知され、対応するシャドウメモリがマッピングされるようになります。これは、ヒープメモリの割り当てとシャドウメモリの割り当てを同期させるための重要なフックです。
src/pkg/runtime/race.c
の変更:
// runtime·racemapshadow は Go ランタイムから呼び出される関数
void
runtime·racemapshadow(void *addr, uintptr size)
{
m->racecall = true; // レース検出器の内部呼び出しであることを示すフラグ
runtime∕race·MapShadow(addr, size); // Go の race パッケージの MapShadow を呼び出す
m->racecall = false;
}
// runtime·raceinit からの変更:
// 起動時にシャドウメモリ全体をマッピングする呼び出しを削除
// runtime·raceinit(void)
// {
// m->racecall = true;
// runtime∕race·Initialize();
// runtime∕race·MapShadow(noptrdata, enoptrbss - noptrdata); // この行が削除された
// m->racecall = false;
// }
runtime·racemapshadow
は、GoランタイムのC部分とGoのrace
パッケージ(TSanのGoラッパー)をつなぐブリッジ関数です。この関数が、実際にTSanのシャドウメモリマッピング関数を呼び出す役割を担います。また、起動時の即時マッピングの削除は、このコミットの目的を達成するための直接的な変更です。
src/pkg/runtime/race/race.go
の変更:
package race
/*
// CGOディレクティブに __tsan_map_shadow の宣言を追加
void __tsan_map_shadow(void *addr, void *size);
// ... その他のTSan関数 ...
*/
import "C"
import "unsafe"
// MapShadow は Go の race パッケージから TSan の __tsan_map_shadow を呼び出す
func MapShadow(addr, size uintptr) {
C.__tsan_map_shadow(unsafe.Pointer(addr), unsafe.Pointer(size))
}
このGoファイルは、GoのコードからCで書かれたTSanのライブラリ関数を呼び出すためのCGOインターフェースを提供します。MapShadow
関数は、Goのuintptr
型のアドレスとサイズをCのvoid*
型に変換し、TSanの__tsan_map_shadow
関数に渡します。これにより、Goランタイムが割り当てたメモリ領域の情報をTSanに正確に伝えることができます。
これらの変更により、シャドウメモリの割り当てが、実際のヒープメモリの割り当てと連動するようになり、必要な時に必要な分だけシャドウメモリが確保される「遅延割り当て」が実現されました。
関連リンク
- Go Race Detector Documentation: https://go.dev/doc/articles/race_detector
- ThreadSanitizer (TSan) Overview: https://clang.llvm.org/docs/ThreadSanitizer.html
- Go Issue for this change (likely related): https://github.com/golang/go/issues/4000 (これは直接のIssueではないかもしれませんが、関連する議論がある可能性があります)
参考にした情報源リンク
- Go言語の公式ドキュメント
- ThreadSanitizerの公式ドキュメント
- Go言語のソースコード(特に
src/runtime
ディレクトリ) - オペレーティングシステム(Windows, Linux)のメモリ管理に関する一般的な知識
top
コマンドの出力に関する一般的な知識