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

[インデックス 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 から指定された sizeclasssize に基づいてメモリブロックを割り当てる役割を担っています。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つの条件を同時に満たす場合に真となります。

  1. size > sizeof(uintptr): 割り当てられるメモリブロックのサイズが、uintptr のサイズよりも大きいこと。これは、ブロックが少なくとも2つの uintptr を格納できる大きさであることを意味し、((uintptr*)v)[1] へのアクセスが有効であることを保証します。
  2. ((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 が呼び出されない場合でも、その後の使用で適切に扱われることが期待されます。
      • しかし、このケースでも、もし vMLink のような構造体で、その next フィールドがゼロクリアされるべきであれば、アロケータの他の部分で既にゼロクリアされているか、または ((uintptr*)v)[1] == 0 の不変条件が適用されるような方法でメモリが提供されていると考えられます。

結論として、else ブロック内の v->next = nil; は、if ブロックで runtime·memclr が実行される場合も、else ブロックが実行される場合(特に ((uintptr*)v)[1] == 0 のケース)も、最終的に v->nextnil になることが保証されるため、冗長であると判断されました。この削除により、コードが簡素化され、実行パスがわずかに効率化されます。

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

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 に設定する役割を持っていました。

元のコードでは、以下のロジックがありました。

  1. 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] == 0else ブロックに入った場合、不変条件によりブロック全体がすでにゼロクリアされているため、v->next は既に nil です。
  • もし size <= sizeof(uintptr)else ブロックに入った場合、割り当てられるオブジェクトが非常に小さいため、next ポインタの概念が異なるか、またはアロケータの他の部分で初期化が保証されていると考えられます。いずれにせよ、この明示的な nil 設定は不要であると判断されました。

この削除は、コードの簡潔性を高め、不要な命令の実行を避けることで、ランタイムの効率をわずかに向上させます。これは、Goランタイムが継続的に最適化され、可能な限り高速で効率的なメモリ管理を目指していることの一例です。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード (特に src/runtime/mcache.go や関連するメモリ管理のファイル)
  • Goのメモリ管理に関するドキュメントやブログ記事 (例: "Go's Memory Allocator" など)
  • C言語のポインタとメモリ操作に関する一般的な知識