[インデックス 16447] ファイルの概要
このコミットは、Goランタイムのヒープ管理におけるバグ修正に関するものです。具体的には、cl/9802043
で導入された変更によって発生した、ヒープのコ coalescing (結合) が正しく行われない問題を解決しています。mheap.map
がポインタになったことで、nelem(h->map)
の挙動が変わり、その結果、後続の MSpan
との結合が阻害されていた点を修正しています。
コミット
commit 9ba551bb87892e29769b15625b5a135a402b9e8b
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Fri May 31 10:58:50 2013 +0400
runtime: fix heap coalescing bug introduced in cl/9802043
mheap.map become a pointer, so nelem(h->map) returns 1 rather than the map size.
As the result coalescing with subsequent spans does not happen.
R=golang-dev, khr
CC=golang-dev
https://golang.org/cl/9649046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9ba551bb87892e29769b15625b5a135a402b9e8b
元コミット内容
runtime: fix heap coalescing bug introduced in cl/9802043
mheap.map become a pointer, so nelem(h->map) returns 1 rather than the map size.
As the result coalescing with subsequent spans does not happen.
変更の背景
このコミットは、Goランタイムのヒープ管理における重要なバグを修正するために行われました。具体的には、以前の変更セット cl/9802043
によって導入された問題に対処しています。この cl/9802043
の詳細については、現在のところ公開されている情報を見つけることができませんでしたが、コミットメッセージから推測するに、mheap.map
の内部表現が変更され、ポインタとして扱われるようになったことが原因で、ヒープのコ coalescing (結合) 処理に不具合が生じたと考えられます。
ヒープのコ coalescing は、メモリの断片化を防ぎ、より大きな連続したメモリブロックを確保するために非常に重要なプロセスです。解放された隣接するメモリ領域を結合することで、効率的なメモリ利用を促進します。このバグにより、コ coalescing が正しく機能しなくなり、結果としてメモリの断片化が進み、パフォーマンスの低下やメモリ使用量の増加につながる可能性がありました。
このコミットは、mheap.map
がポインタになったことによる nelem
マクロの誤った評価を修正し、ヒープのコ coalescing が期待通りに動作するようにすることで、この問題を解決しています。
前提知識の解説
このコミットを理解するためには、Goランタイムのメモリ管理、特にヒープの構造と MSpan
の概念について基本的な知識が必要です。
- Goランタイムのメモリ管理: Goは独自のガベージコレクタを持つランタイムシステムを採用しており、メモリの割り当てと解放を自動的に管理します。ヒープは、プログラムが動的にメモリを割り当てる領域です。
mheap
: Goランタイムにおけるヒープ全体の管理構造体です。システムからメモリを要求し、それをMSpan
と呼ばれるチャンクに分割して管理します。MSpan
: ヒープ内の連続したメモリページのブロックを表す構造体です。Goランタイムは、オブジェクトのサイズに応じて異なるサイズのMSpan
を割り当てます。例えば、小さなオブジェクトは小さなMSpan
に、大きなオブジェクトは大きなMSpan
に割り当てられます。PageSize
: オペレーティングシステムがメモリを管理する最小単位です。Goランタイムもこのページサイズを基準にメモリを扱います。- Coalescing (結合): メモリ管理における重要な最適化手法の一つです。隣接する2つ以上の空きメモリブロックを結合して、より大きな単一の空きブロックを作成するプロセスを指します。これにより、メモリの断片化を減らし、大きなメモリ要求に対応できるようになります。例えば、あるオブジェクトが解放され、その隣のメモリブロックも空いている場合、これらを結合して一つの大きな空きブロックとすることで、将来的に大きな配列などを割り当てる際に役立ちます。
h->spans
:mheap
構造体の一部で、MSpan
へのポインタの配列です。この配列は、ヒープ内の各ページがどのMSpan
に属しているかをマッピングするために使用されます。h->spans_mapped
:h->spans
配列のうち、実際にメモリがマップされている(使用されている)バイト数を表します。nelem
マクロ: C言語の文脈で、配列の要素数を計算するために使われるマクロです。通常はsizeof(array) / sizeof(array[0])
のように定義されます。しかし、ポインタに対してこのマクロを使用すると、ポインタ自体のサイズを要素のサイズで割ることになり、結果として常に1
を返すという誤った挙動を示すことがあります。
技術的詳細
このバグの核心は、mheap.map
の型が変更されたことにあります。コミットメッセージによると、mheap.map
が「ポインタになった」とあります。C言語において、配列はしばしばポインタとして扱われますが、sizeof
演算子や nelem
マクロの挙動は、それが実際の配列であるか、それとも単なるポインタであるかによって大きく異なります。
元のコードでは、h->spans
のサイズを計算する際に、nelem(h->spans)
のような形式で配列の要素数を取得しようとしていた可能性があります。しかし、h->spans
がポインタになった場合、nelem
マクロは配列全体のサイズではなく、ポインタ自体のサイズを返すようになります。例えば、64ビットシステムではポインタのサイズは8バイトです。もし h->spans[0]
のサイズが8バイトであれば、nelem(h->spans)
は 8 / 8 = 1
を返してしまいます。
この誤った nelem
の結果が、ヒープのコ coalescing 処理に影響を与えました。コ coalescing 処理では、現在の MSpan
の後に続く MSpan
が存在するかどうか、そしてそれが結合可能かどうかをチェックするために、h->spans
配列の範囲外アクセスを防ぐための境界チェックが行われます。
具体的には、p+s->npages < nelem(h->spans)
のような条件式があった場合、nelem(h->spans)
が 1
を返すと、p+s->npages
が 1
を超えるほとんどの場合でこの条件が false
となり、後続の MSpan
が存在しないと誤って判断されてしまいます。これにより、隣接する空き MSpan
があっても結合されず、メモリの断片化が発生するという問題が生じていました。
このコミットでは、nelem(h->spans)
の代わりに、h->spans_mapped
という、実際にマップされている spans
のバイト数を使用することで、正しい境界チェックを行うように修正しています。これにより、h->spans
がポインタであっても、その実体であるマップされたメモリ領域のサイズに基づいてコ coalescing の範囲を正しく判断できるようになります。
コアとなるコードの変更箇所
diff --git a/src/pkg/runtime/mheap.c b/src/pkg/runtime/mheap.c
index 354031ad03..11d78203de 100644
--- a/src/pkg/runtime/mheap.c
+++ b/src/pkg/runtime/mheap.c
@@ -74,8 +74,7 @@ runtime·MHeap_MapSpans(MHeap *h)\n n = (uintptr)h->arena_used;\n if(sizeof(void*) == 8)\n n -= (uintptr)h->arena_start;\n- // Coalescing code reads spans past the end of mapped arena, thus +1.\n- n = (n / PageSize + 1) * sizeof(h->spans[0]);\n+ n = n / PageSize * sizeof(h->spans[0]);\n n = ROUND(n, PageSize);\n if(h->spans_mapped >= n)\n return;\n@@ -366,7 +365,7 @@ MHeap_FreeLocked(MHeap *h, MSpan *s)\n mstats.mspan_inuse = h->spanalloc.inuse;\n mstats.mspan_sys = h->spanalloc.sys;\n }\n- if(p+s->npages < nelem(h->spans) && (t = h->spans[p+s->npages]) != nil && t->state != MSpanInUse) {\n+ if((p+s->npages)*sizeof(h->spans[0]) < h->spans_mapped && (t = h->spans[p+s->npages]) != nil && t->state != MSpanInUse) {\n tp = (uintptr*)(t->start<<PageShift);\n *sp |= *tp;\t// propagate \"needs zeroing\" mark\n s->npages += t->npages;\n```
## コアとなるコードの解説
このコミットには2つの主要な変更点があります。
1. **`runtime·MHeap_MapSpans` 関数内の変更**:
* **変更前**: `n = (n / PageSize + 1) * sizeof(h->spans[0]);`
コメント `// Coalescing code reads spans past the end of mapped arena, thus +1.` が示唆するように、コ coalescing コードがマップされたアリーナの終端を超えて `spans` を読み取る可能性があるため、`+1` していました。これは、`nelem(h->spans)` が正しく機能することを前提とした、ある種のバッファリングまたは境界調整だったと考えられます。
* **変更後**: `n = n / PageSize * sizeof(h->spans[0]);`
`+1` が削除されました。これは、後述の `MHeap_FreeLocked` 関数での境界チェックが `h->spans_mapped` を使用するように変更されたため、この `+1` が不要になったことを意味します。`h->spans_mapped` は実際にマップされているバイト数を正確に反映するため、余分なオフセットは必要ありません。
2. **`MHeap_FreeLocked` 関数内の変更**:
* **変更前**: `if(p+s->npages < nelem(h->spans) && ...)`
ここで `nelem(h->spans)` が使用されていました。前述の通り、`h->spans` がポインタになったことで、この `nelem` は常に `1` を返すようになり、`p+s->npages` が `1` を超えるとすぐに条件が `false` となってしまい、後続の `MSpan` が存在しないと誤って判断されていました。これにより、コ coalescing が行われませんでした。
* **変更後**: `if((p+s->npages)*sizeof(h->spans[0]) < h->spans_mapped && ...)`
この変更がバグ修正の核心です。`nelem(h->spans)` の代わりに、`h->spans` 配列のインデックス `p+s->npages` に対応するバイトオフセット (`(p+s->npages)*sizeof(h->spans[0])`) が、実際にマップされている `spans` のバイト数 `h->spans_mapped` 未満であるかをチェックするように変更されました。
これにより、`h->spans` がポインタであっても、その実体であるマップされたメモリ領域のサイズに基づいて、後続の `MSpan` が有効な範囲内にあるかを正確に判断できるようになりました。この修正により、解放された `MSpan` の後続に結合可能な `MSpan` が存在する場合、正しくコ coalescing が行われるようになります。
これらの変更により、Goランタイムのヒープ管理におけるコ coalescing のロジックが、`mheap.map` の内部表現の変更に対応し、メモリの断片化を効果的に防ぐことができるようになりました。
## 関連リンク
* Go Change-Id: `https://golang.org/cl/9649046`
## 参考にした情報源リンク
* Go言語のソースコード (特に `src/pkg/runtime/mheap.c`)
* Go言語のメモリ管理に関する一般的なドキュメントや記事
* `cl/9802043` については、公開されている情報を見つけることができませんでした。