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

[インデックス 13208] ファイルの概要

このコミットは、Goランタイムにおけるガベージコレクション(GC)とプロファイリングの挙動に関するバグ修正です。具体的には、ファイナライザが設定されたオブジェクトがプロファイリングの対象でもある場合に、「special bit」が不適切にクリアされる問題を解決します。この修正により、ファイナライズ後もプロファイリングに必要な情報が保持され、正確なプロファイリングが可能になります。

コミット

commit 348087877cc02948d062bc770a4f4d67b2819797
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Wed May 30 08:04:11 2012 +0200

    runtime: do not unset the special bit after finalization.
    
    A block with finalizer might also be profiled. The special bit
    is needed to unregister the block from the profile. It will be
    unset only when the block is freed.
    
    Fixes #3668.
    
    R=golang-dev, rsc
    CC=golang-dev, remy
    https://golang.org/cl/6249066

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/348087877cc02948d062bc770a4f4d67b2819797

元コミット内容

runtime: do not unset the special bit after finalization.

A block with finalizer might also be profiled. The special bit
is needed to unregister the block from the profile. It will be
unset only when the block is freed.

Fixes #3668.

R=golang-dev, rsc
CC=golang-dev, remy
https://golang.org/cl/6249066

変更の背景

Go言語のランタイムには、ガベージコレクション(GC)とプロファイリングという重要な機能があります。

ガベージコレクションとファイナライザ: GoのGCは、不要になったメモリを自動的に解放する仕組みです。オブジェクトがGCによって回収される直前に特定の処理を実行したい場合、runtime.SetFinalizer関数を使用してファイナライザを設定できます。ファイナライザは、オブジェクトが到達不可能になった後、GCによってメモリが解放される前に一度だけ実行される関数です。これは、ファイルハンドルやネットワーク接続などの外部リソースをクリーンアップする際に役立ちます。

プロファイリングと「special bit」: Goランタイムは、メモリ使用量やCPU使用率などのプロファイリング情報を提供します。これにより、開発者はアプリケーションのパフォーマンスボトルネックを特定できます。プロファイリングの過程で、特定のメモリブロックがプロファイリングの対象であることを示すために、内部的に「special bit」と呼ばれるフラグが設定されることがあります。このspecial bitは、そのブロックが解放される際にプロファイリングシステムから適切に登録解除されるために必要です。

問題の発生: このコミット以前のGoランタイムでは、ファイナライザが設定されたメモリブロックがGCによって処理される際に、ファイナライザの実行後に誤ってこの「special bit」がクリアされてしまう問題がありました。もしそのメモリブロックが同時にプロファイリングの対象でもあった場合、special bitが早期にクリアされることで、プロファイリングシステムがそのブロックを正しく追跡できなくなり、プロファイリングデータに不整合が生じる可能性がありました。つまり、ファイナライザの実行とプロファイリングの追跡が競合し、プロファイリングの正確性が損なわれるというバグが存在していました。

この問題はGoのIssue #3668として報告されており、このコミットはその修正を目的としています。

前提知識の解説

このコミットを理解するためには、以下のGoランタイムの内部概念について理解しておく必要があります。

  1. ガベージコレクション (GC): Goはトレース型GCを採用しており、到達可能なオブジェクトを特定し、到達不可能なオブジェクトを回収します。
  2. ファイナライザ (Finalizer): runtime.SetFinalizerによって設定される関数で、オブジェクトがGCによって回収される直前に実行されます。ファイナライザが設定されたオブジェクトは、GCのサイクルにおいて特別な扱いを受けます。ファイナライザが実行されるまで、そのオブジェクトはメモリ上に保持されます。
  3. メモリブロックとアロケーション: Goランタイムは、プログラムが使用するメモリを小さなブロックに分割して管理します。これらのブロックは、オブジェクトの割り当て(アロケーション)と解放(デアロケーション)の単位となります。
  4. プロファイリング: Goには、CPUプロファイリング、メモリプロファイリング、ブロックプロファイリングなど、様々なプロファイリングツールが組み込まれています。これらのツールは、ランタイムの内部状態を監視し、パフォーマンスに関する洞察を提供します。
  5. 「special bit」: Goランタイムの内部で、特定のメモリブロックが特別な状態にあることを示すために使用されるフラグです。この「特別な状態」には、ファイナライザが設定されていることや、プロファイリングの対象であることなどが含まれます。このビットは、ブロックが完全に解放されるまで設定されている必要があります。特にプロファイリングにおいては、このビットが設定されていることで、そのブロックがプロファイリングシステムに登録されていることを示し、解放時に適切に登録解除されるためのトリガーとなります。

技術的詳細

このコミットの技術的な核心は、ファイナライザの処理とプロファイリングの追跡における「special bit」のライフサイクル管理の改善にあります。

以前のランタイムでは、ファイナライザが実行された後、runtime.setblockspecial(p, false)という呼び出しによって、そのメモリブロックのspecial bitが明示的にクリアされていました。これは、ファイナライザの目的が達成されたため、そのブロックがもはや「特別な」状態ではないと判断されたためと考えられます。

しかし、問題は、ファイナライザが実行された後でも、そのメモリブロックがまだプロファイリングの対象である可能性があるという点でした。プロファイリングシステムは、special bitが設定されていることを前提として、メモリブロックのライフサイクルを追跡し、解放時にプロファイルから登録解除する処理を行います。もしファイナライザの実行後にspecial bitがクリアされてしまうと、そのブロックが実際にメモリから解放される前にプロファイリングシステムがその追跡を停止してしまい、結果としてプロファイリングデータに誤りが生じる可能性がありました。例えば、解放されていないメモリがプロファイル上では解放済みと見なされたり、プロファイリングの登録解除処理がスキップされたりするなどの問題が考えられます。

この修正は、この競合状態を解消するために、ファイナライザの処理パスからruntime.setblockspecial(p, false)の呼び出しを削除します。これにより、special bitはファイナライザの実行後も設定されたままとなり、そのメモリブロックが実際にGCによって完全に解放されるまで保持されます。ブロックが完全に解放される際に、ランタイムの別の部分でspecial bitが適切にクリアされ、プロファイリングシステムからの登録解除も正しく行われるようになります。

つまり、この変更は、ファイナライザとプロファイリングという二つの独立したランタイム機能が、共通の内部フラグ(special bit)のライフサイクルを巡って競合していた問題を、special bitのクリアタイミングを「ブロックが完全に解放される時」に統一することで解決しています。

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

このコミットでは、以下の2つのファイルが変更されています。

  1. src/pkg/runtime/mfinal.c
  2. src/pkg/runtime/mgc0.c

src/pkg/runtime/mfinal.c の変更

--- a/src/pkg/runtime/mfinal.c
+++ b/src/pkg/runtime/mfinal.c
@@ -150,8 +150,7 @@ runtime·addfinalizer(void *p, void (*f)(void*), int32 nret)
  	tab = TAB(p);\n \truntime·lock(tab);\n \tif(f == nil) {\n-\t\tif(lookfintab(tab, p, true, nil))\n-\t\t\truntime·setblockspecial(p, false);\n+\t\tlookfintab(tab, p, true, nil);\n \t\truntime·unlock(tab);\n \t\treturn true;\n \t}\n```

-   `runtime·addfinalizer` 関数内で、ファイナライザが`nil`(つまりファイナライザの解除要求)の場合に、`lookfintab`の呼び出し後に`runtime·setblockspecial(p, false)`を呼び出してspecial bitをクリアしていた行が削除されました。
-   変更前は、`lookfintab`が`true`を返した場合(ファイナライザが見つかり、削除された場合)にのみ`runtime·setblockspecial`が呼び出されていました。
-   変更後は、`lookfintab`を呼び出すだけで、special bitのクリアは行われません。

### `src/pkg/runtime/mgc0.c` の変更

```diff
--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -1042,7 +1042,6 @@ runfinq(void)\n \t\t\t\t\tframecap = framesz;\n \t\t\t\t}\n \t\t\t\t*(void**)frame = f->arg;\n-\t\t\t\truntime·setblockspecial(f->arg, false);\n \t\t\t\treflect·call((byte*)f->fn, frame, sizeof(uintptr) + f->nret);\n \t\t\t\tf->fn = nil;\n \t\t\t\tf->arg = nil;\n```

-   `runfinq` 関数内で、ファイナライザが実行される直前に`runtime·setblockspecial(f->arg, false)`を呼び出してspecial bitをクリアしていた行が削除されました。
-   `runfinq`は、GCによって回収されるオブジェクトのファイナライザを実行する役割を担う関数です。

## コアとなるコードの解説

### `src/pkg/runtime/mfinal.c` の変更点

`runtime·addfinalizer`関数は、Goの`runtime.SetFinalizer`に対応するランタイム内部関数です。この関数は、オブジェクトにファイナライザを設定したり、既存のファイナライザを解除したりするために使用されます。

変更前のコードでは、`if(f == nil)`のブロック、つまりファイナライザを解除する際に、`lookfintab`(ファイナライザテーブルからエントリを探して削除する関数)が成功した場合に`runtime·setblockspecial(p, false)`を呼び出して、そのオブジェクトのspecial bitをクリアしていました。これは、ファイナライザが解除されたので、もはやそのオブジェクトは「特別な」状態ではないという意図があったと考えられます。

しかし、このコミットの背景で述べたように、オブジェクトがファイナライザを持つだけでなく、プロファイリングの対象でもある場合、ファイナライザの解除時にspecial bitをクリアしてしまうと、プロファイリングシステムがそのオブジェクトを正しく追跡できなくなる問題がありました。この修正により、ファイナライザの解除時であってもspecial bitはクリアされず、オブジェクトが完全に解放されるまで保持されるようになります。

### `src/pkg/runtime/mgc0.c` の変更点

`runfinq`関数は、Goランタイムのガベージコレクタの一部であり、GCによって回収対象となったオブジェクトに設定されているファイナライザを実際に実行する役割を担っています。

変更前のコードでは、`reflect·call`によってファイナライザ関数が呼び出される直前に、`runtime·setblockspecial(f->arg, false)`を呼び出して、ファイナライズされるオブジェクトのspecial bitをクリアしていました。これは、ファイナライザが実行されることで、そのオブジェクトの「特別な」状態(ファイナライザが設定されている状態)が終了すると見なされていたためです。

しかし、ここでも同様に、オブジェクトがプロファイリングの対象である場合、ファイナライザの実行直後にspecial bitをクリアしてしまうと、プロファイリングシステムがそのオブジェクトのライフサイクルを最後まで追跡できなくなる問題がありました。この修正により、ファイナライザの実行時であってもspecial bitはクリアされず、オブジェクトがメモリから完全に解放されるまでspecial bitが保持されるようになります。これにより、プロファイリングシステムはオブジェクトの完全なライフサイクルを追跡し、正確なプロファイリングデータを提供できるようになります。

これらの変更は、Goランタイムの内部におけるメモリ管理とプロファイリングの連携を改善し、より堅牢で正確なシステムを実現するための重要な修正です。

## 関連リンク

*   Go Issue #3668: [https://golang.org/issue/3668](https://golang.org/issue/3668) (コミットメッセージに記載されているIssue番号)
*   Go CL 6249066: [https://golang.org/cl/6249066](https://golang.org/cl/6249066) (Goのコードレビューシステムにおける変更セットのリンク)

## 参考にした情報源リンク

*   Go言語の公式ドキュメント (runtimeパッケージ、GC、プロファイリングに関する情報)
*   Go言語のソースコード (特に`src/runtime`ディレクトリ)
*   Go言語のIssueトラッカー (Issue #3668の詳細)
*   Go言語のコードレビューシステム (CL 6249066の詳細)
*   ガベージコレクションとファイナライザに関する一般的な知識
*   プロファイリングの概念と実装に関する一般的な知識