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

[インデックス 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->npages1 を超えるほとんどの場合でこの条件が 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` については、公開されている情報を見つけることができませんでした。