[インデックス 12839] ファイルの概要
src/pkg/runtime/mcache.c
はGo言語のランタイムの一部であり、メモリ管理、特にGoのガベージコレクタと連携して動作するメモリキャッシュ (MCache
) の実装を含んでいます。MCache
は、GoのP (Processor) ごとに割り当てられるローカルなメモリキャッシュで、小さなオブジェクトの高速なアロケーションを可能にします。これにより、グローバルなヒープロックを回避し、並行性を向上させます。このファイルには、MCache
からメモリブロックを割り当てるための主要な関数 runtime·MCache_Alloc
が定義されています。
コミット
runtime: remove redundant code
R=rsc
CC=golang-dev
https://golang.org/cl/5987046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a28a10e1a2352736fa8bbf6def02517f42260e34
元コミット内容
commit a28a10e1a2352736fa8bbf6def02517f42260e34
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Thu Apr 5 18:37:46 2012 +0400
runtime: remove redundant code
R=rsc
CC=golang-dev
https://golang.org/cl/5987046
---
src/pkg/runtime/mcache.c | 5 -----
1 file changed, 5 deletions(-)
diff --git a/src/pkg/runtime/mcache.c b/src/pkg/runtime/mcache.c
index 518e00c123..7ead5e5b66 100644
--- a/src/pkg/runtime/mcache.c
+++ b/src/pkg/runtime/mcache.c
@@ -43,11 +43,6 @@ runtime·MCache_Alloc(MCache *c, int32 sizeclass, uintptr size, int32 zeroed)\n \t// block is zeroed iff second word is zero ...\n \tif(size > sizeof(uintptr) && ((uintptr*)v)[1] != 0)\n \t\truntime·memclr((byte*)v, size);\n-\t\telse {\n-\t\t\t// ... except for the link pointer\n-\t\t\t// that we used above; zero that.\n-\t\t\tv->next = nil;\n-\t\t}\n \t}\n \tc->local_cachealloc += size;\n \tc->local_objects++;\n```
## 変更の背景
このコミットの背景は、Goランタイムのメモリ割り当てロジックにおける冗長なコードの削除です。具体的には、`runtime·MCache_Alloc` 関数内で、割り当てられたメモリブロックの `next` ポインタを `nil` に設定する処理が、特定の条件下で不要であることが判明したため削除されました。
Goのメモリ管理では、新しく割り当てられたメモリブロックは、その内容がゼロクリアされることが期待される場合があります。このゼロクリアは、セキュリティ上の理由(以前のデータが残らないようにするため)や、Goのゼロ値のセマンティクス(新しい変数はデフォルトでゼロ値を持つ)を保証するために重要です。
削除されたコードは、メモリブロックが完全にゼロクリアされていない場合に、明示的に `v->next = nil;` を実行していました。しかし、Goランタイムのメモリ割り当ての内部的な不変条件として、「ブロックの2番目のワードがゼロであれば、そのブロック全体がゼロクリアされている」という保証が存在します。この不変条件により、`v->next` が `nil` に設定されるべき状況では、すでに `nil` になっているか、または `runtime·memclr` によってゼロクリアされるため、明示的な設定が不要になったと判断されました。
この変更は、コードの簡素化と効率化を目的としています。冗長な処理を削除することで、ランタイムのフットプリントをわずかに減らし、実行パスを最適化します。
## 前提知識の解説
このコミットを理解するためには、以下のGoランタイムのメモリ管理に関する概念とC言語のポインタ操作に関する知識が必要です。
### Goランタイムのメモリ管理
* **MCache (Memory Cache)**: Goのランタイムは、各論理プロセッサ (P) ごとにローカルなメモリキャッシュ `MCache` を持ちます。これは、小さなオブジェクトの割り当てを高速化し、グローバルなヒープロックの競合を減らすために使用されます。`MCache` は、`MHeap` (グローバルヒープ) から取得した `MSpan` (連続したメモリページ) を、さらに小さなサイズのオブジェクトに分割して管理します。
* **MSpan**: `MSpan` は、Goのメモリ管理における基本的なメモリブロック単位です。連続したメモリページで構成され、特定のサイズのオブジェクトを割り当てるために使用されます。
* **MHeap (Memory Heap)**: グローバルなヒープであり、`MSpan` を管理します。`MCache` が必要なメモリを使い果たした場合、`MHeap` から新しい `MSpan` を取得します。
* **ゼロクリア (Zeroing)**: Goでは、新しく割り当てられたメモリは通常、ゼロ値で初期化されます。これは、ポインタが `nil` に、数値が `0` に、ブール値が `false` になるなど、Goの型システムのセマンティクスを保証するために重要です。
* **`runtime·memclr`**: Goランタイム内部で使用される関数で、指定されたメモリ領域をゼロクリアします。C言語の `memset` に似ています。
* **`uintptr`**: Goの `uintptr` 型は、ポインタを保持できる符号なし整数型です。C言語のコードでは、メモリアドレスを整数として扱う際によく使用されます。
* **`nil`**: Goにおけるゼロ値のポインタです。C言語の `NULL` に相当し、通常はメモリアドレス `0` を表します。
### C言語のポインタ操作
* **ポインタのキャスト**: `(uintptr*)v` のように、ある型のポインタを別の型のポインタに変換することです。これにより、メモリを異なる型として解釈できます。
* **ポインタ演算**: `((uintptr*)v)[1]` は、`v` が指すメモリ領域を `uintptr` の配列として解釈し、その2番目の要素(インデックス1)にアクセスすることを意味します。これは、`v` のアドレスから `sizeof(uintptr)` バイトオフセットした位置にある `uintptr` 値を読み取ることに相当します。
* **構造体ポインタとメンバーアクセス**: `v->next` は、`v` が指す構造体の `next` メンバーにアクセスすることを意味します。この場合、`v` は `MLink` のような構造体へのポインタであると推測されます。
### `MLink` 構造体
Goランタイムのメモリ管理では、解放されたオブジェクトを連結リストで管理するために `MLink` という構造体がよく使われます。これは通常、以下のように定義されます。
```c
struct MLink {
MLink *next;
};
割り当てられたメモリブロックの先頭にこの MLink
構造体が埋め込まれている場合、v
はそのブロックの先頭を指し、v->next
は次のフリーオブジェクトへのポインタを保持します。
技術的詳細
変更が行われた runtime·MCache_Alloc
関数は、MCache
から指定された sizeclass
と size
に基づいてメモリブロックを割り当てる役割を担っています。zeroed
引数は、割り当てられたブロックがゼロクリアされるべきかどうかを示します。
削除されたコードは、以下の if
文の else
ブロック内にありました。
if(size > sizeof(uintptr) && ((uintptr*)v)[1] != 0)
runtime·memclr((byte*)v, size);
else {
// ... except for the link pointer
// that we used above; zero that.
v->next = nil;
}
この if
文の条件 size > sizeof(uintptr) && ((uintptr*)v)[1] != 0
は、以下の2つの条件を同時に満たす場合に真となります。
size > sizeof(uintptr)
: 割り当てられるメモリブロックのサイズが、uintptr
のサイズよりも大きいこと。これは、ブロックが少なくとも2つのuintptr
を格納できる大きさであることを意味し、((uintptr*)v)[1]
へのアクセスが有効であることを保証します。((uintptr*)v)[1] != 0
: 割り当てられたメモリブロックの2番目のワード(uintptr
単位で)がゼロではないこと。
この if
文の直前には、// block is zeroed iff second word is zero ...
というコメントがあります。これは、Goランタイムのメモリ割り当てにおける重要な不変条件を示しています。つまり、「メモリブロックの2番目のワードがゼロであるならば、そのブロック全体はすでにゼロクリアされている」という保証です。
この不変条件を考慮すると、if
文のロジックは次のように解釈できます。
-
if
ブロックが実行される場合:size > sizeof(uintptr)
かつ((uintptr*)v)[1] != 0
の場合。- この条件は、「ブロックが十分に大きく、かつ2番目のワードがゼロではない(つまり、ブロックがゼロクリアされていない)」ことを意味します。
- この場合、
runtime·memclr((byte*)v, size);
が呼び出され、メモリブロック全体が明示的にゼロクリアされます。v->next
もこのゼロクリアによってnil
になります。
-
else
ブロックが実行される場合:if
条件が偽の場合、つまりsize <= sizeof(uintptr)
または((uintptr*)v)[1] == 0
の場合。- ケース1:
((uintptr*)v)[1] == 0
の場合:- 前述の不変条件「ブロックは2番目のワードがゼロである場合にのみゼロクリアされる」により、このブロックはすでに完全にゼロクリアされていることが保証されます。
- したがって、
v->next
はすでにnil
になっています。この場合、v->next = nil;
は冗長な操作です。
- ケース2:
size <= sizeof(uintptr)
の場合:- 割り当てられるオブジェクトが非常に小さい場合です。このような小さなオブジェクトは、
MLink
構造体のようにnext
ポインタを持つことがありますが、その初期化はアロケータによって保証されているか、またはruntime·memclr
が呼び出されない場合でも、その後の使用で適切に扱われることが期待されます。 - しかし、このケースでも、もし
v
がMLink
のような構造体で、そのnext
フィールドがゼロクリアされるべきであれば、アロケータの他の部分で既にゼロクリアされているか、または((uintptr*)v)[1] == 0
の不変条件が適用されるような方法でメモリが提供されていると考えられます。
- 割り当てられるオブジェクトが非常に小さい場合です。このような小さなオブジェクトは、
結論として、else
ブロック内の v->next = nil;
は、if
ブロックで runtime·memclr
が実行される場合も、else
ブロックが実行される場合(特に ((uintptr*)v)[1] == 0
のケース)も、最終的に v->next
が nil
になることが保証されるため、冗長であると判断されました。この削除により、コードが簡素化され、実行パスがわずかに効率化されます。
コアとなるコードの変更箇所
src/pkg/runtime/mcache.c
ファイルの runtime·MCache_Alloc
関数から以下の5行が削除されました。
- else {
- // ... except for the link pointer
- // that we used above; zero that.
- v->next = nil;
- }
コアとなるコードの解説
削除されたコードは、runtime·MCache_Alloc
関数内でメモリブロック v
を割り当てた後、その next
ポインタを nil
に設定する役割を持っていました。
元のコードでは、以下のロジックがありました。
if(size > sizeof(uintptr) && ((uintptr*)v)[1] != 0)
:- もし割り当てられたブロックが十分に大きく、かつ2番目のワードがゼロでない(つまり、ブロックがゼロクリアされていない)場合、
runtime·memclr((byte*)v, size);
を呼び出してブロック全体をゼロクリアします。この処理により、v->next
も自動的にnil
になります。
- もし割り当てられたブロックが十分に大きく、かつ2番目のワードがゼロでない(つまり、ブロックがゼロクリアされていない)場合、
else
ブロック:- 上記の
if
条件が偽の場合に実行されます。つまり、ブロックが小さすぎる (size <= sizeof(uintptr)
) か、または2番目のワードがすでにゼロである (((uintptr*)v)[1] == 0
) 場合です。 - この
else
ブロック内で、v->next = nil;
が明示的に実行されていました。コメントには「リンクポインタを除いて...それをゼロにする」とありますが、これはruntime·memclr
が実行されない場合にv->next
を確実にゼロにする意図があったことを示唆しています。
- 上記の
しかし、Goランタイムのメモリ割り当ての不変条件「ブロックは2番目のワードがゼロである場合にのみゼロクリアされる」を考慮すると、この else
ブロック内の v->next = nil;
は冗長になります。
- もし
((uintptr*)v)[1] == 0
でelse
ブロックに入った場合、不変条件によりブロック全体がすでにゼロクリアされているため、v->next
は既にnil
です。 - もし
size <= sizeof(uintptr)
でelse
ブロックに入った場合、割り当てられるオブジェクトが非常に小さいため、next
ポインタの概念が異なるか、またはアロケータの他の部分で初期化が保証されていると考えられます。いずれにせよ、この明示的なnil
設定は不要であると判断されました。
この削除は、コードの簡潔性を高め、不要な命令の実行を避けることで、ランタイムの効率をわずかに向上させます。これは、Goランタイムが継続的に最適化され、可能な限り高速で効率的なメモリ管理を目指していることの一例です。
関連リンク
- Go CL (Change List): https://golang.org/cl/5987046
参考にした情報源リンク
- Go言語のソースコード (特に
src/runtime/mcache.go
や関連するメモリ管理のファイル) - Goのメモリ管理に関するドキュメントやブログ記事 (例: "Go's Memory Allocator" など)
- C言語のポインタとメモリ操作に関する一般的な知識