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

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

コミット

commit ec59b840f93fff4631b60323e28a44967c704e7d
Author: Russ Cox <rsc@golang.org>
Date:   Sat Dec 22 16:42:22 2012 -0500

    runtime: coalesce 0-size allocations
    
    Fixes #3996.
    
    R=ken2
    CC=golang-dev
    https://golang.org/cl/7001052

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

https://github.com/golang/go/commit/ec59b840f93fff4631b60323e28a44967c704e7d

元コミット内容

このコミットは、Goランタイムにおけるゼロサイズ(0バイト)のアロケーション(メモリ割り当て)を統合(coalesce)することを目的としています。具体的には、new および cnew 関数がゼロサイズの型に対して呼び出された際に、実際に新しいメモリを割り当てるのではなく、runtime·zerobase という単一のグローバルポインタを返すように変更されています。これにより、不要なメモリ割り当てとガベージコレクションのオーバーヘッドが削減されます。

コミットメッセージには「Fixes #3996」とあり、これはGoのIssueトラッカーにおけるバグ修正を指しています。

変更の背景

Go言語では、new(T)make([]T, 0) のように、サイズがゼロのオブジェクトやスライスを生成することが可能です。従来のランタイムでは、これらのゼロサイズのアロケーションに対しても、たとえ0バイトであっても、runtime·mallocgc を呼び出してメモリ割り当てを試みていました。これは、以下のような問題を引き起こしていました。

  1. 不要なメモリ割り当て: 0バイトのオブジェクトに対して実際にメモリを割り当てる必要はありません。ポインタ自体は必要ですが、その指す先は実質的に意味を持ちません。
  2. ガベージコレクションのオーバーヘッド: ゼロサイズのアロケーションであっても、ガベージコレクタはそれらを追跡し、管理する必要がありました。これにより、GCのサイクルにおいて不要な処理が発生し、パフォーマンスに影響を与えていました。
  3. ポインタの同一性: Go言語の仕様では、ゼロサイズのアロケーションが異なるポインタ値を返すことを要求していません。つまり、new(struct{}) を複数回呼び出した際に、常に同じポインタが返されても問題ないのです。

これらの問題を解決し、ランタイムの効率を向上させるために、ゼロサイズのアロケーションを統合する変更が導入されました。これにより、すべてのゼロサイズのアロケーションが単一の既知のポインタ(runtime·zerobase)を指すようになり、メモリ使用量とGCの負荷が軽減されます。

前提知識の解説

Goランタイム (Go Runtime)

Goランタイムは、Goプログラムの実行を管理するシステムです。これには、ガベージコレクタ(GC)、スケジューラ、メモリ割り当て器(アロケータ)、およびその他の低レベルの機能が含まれます。Goプログラムは、オペレーティングシステム上で直接実行されるのではなく、このランタイム上で動作します。

メモリ割り当て (Memory Allocation)

プログラムが実行時にメモリを要求するプロセスです。Goでは、newmake などの組み込み関数を使用してメモリを割り当てます。Goランタイムのメモリ割り当て器は、ヒープからメモリブロックを効率的に取得し、プログラムに提供する役割を担います。

ガベージコレクション (Garbage Collection, GC)

不要になったメモリを自動的に解放するプロセスです。GoのGCは、到達可能性(reachability)に基づいてオブジェクトを追跡し、どのオブジェクトも参照していないメモリを「ガベージ」と判断して回収します。これにより、開発者は手動でのメモリ管理から解放されますが、GCの実行自体が一時的にプログラムの実行を停止させたり、CPUリソースを消費したりする可能性があります。

newmake

Goにおけるメモリ割り当ての主要な組み込み関数です。

  • new(Type): Type 型のゼロ値が格納された新しいメモリ領域を割り当て、そのポインタを返します。常にポインタを返します。
  • make(Type, ...): スライス、マップ、チャネルなどの組み込み型を初期化し、それらの型に応じた値を返します。new とは異なり、ポインタではなく、初期化された型の値を返します。

ゼロサイズ型 (Zero-sized Types)

Go言語には、サイズが0バイトの型が存在します。最も一般的なのは空の構造体 struct{} です。これらの型は、メモリを消費しないため、セマフォやイベント通知など、値自体ではなくその存在が意味を持つような用途で利用されます。

runtime·mallocgc

Goランタイム内部で使用されるメモリ割り当て関数です。これは、ガベージコレクタによって管理されるヒープからメモリを割り当てます。

runtime·zerobase

このコミットで導入された概念で、Goランタイムがゼロサイズのアロケーションに対して返す単一のグローバルポインタです。これは、実際のメモリ領域を指すのではなく、ゼロサイズのアロケーションがすべて同じ「場所」を指すことを保証するための特別なポインタです。

技術的詳細

このコミットの核心は、Goランタイムのメモリ割り当てロジック、特に runtime·newruntime·cnew 関数におけるゼロサイズアロケーションのハンドリングの変更です。

変更前は、new(T)cnew(T) が呼び出された際、typ->size が0であっても、無条件に runtime·mallocgc(typ->size, ...) が呼び出されていました。runtime·mallocgc は、たとえ要求サイズが0であっても、内部的には何らかの処理を行い、有効なポインタを返していました。これは、システムコールや内部的な簿記処理を伴うため、オーバーヘッドが発生していました。

変更後は、typ->size == 0 の条件が追加され、この条件が真の場合には、runtime·mallocgc の呼び出しをスキップし、代わりに (uint8*)&runtime·zerobase という固定のポインタを返すようになりました。

runtime·zerobase は、Goランタイムのどこかで定義されているグローバルなアドレスであり、ゼロサイズのアロケーションがすべてこのアドレスを指すことで、以下の利点が得られます。

  • メモリ割り当ての回避: 実際にヒープからメモリを割り当てる必要がなくなります。
  • GCの負荷軽減: runtime·zerobase は静的なポインタであるため、ガベージコレクタがこれを追跡する必要がありません。これにより、GCのマークフェーズやスイープフェーズにおける不要な処理が削減されます。
  • ポインタの同一性保証: Go言語の仕様ではゼロサイズのアロケーションが異なるポインタ値を返すことを要求していないため、常に同じポインタを返すことは完全に合法であり、効率的です。

この最適化は、特にゼロサイズのスライスや構造体を頻繁に作成するようなコードにおいて、顕著なパフォーマンス改善をもたらす可能性があります。例えば、make([]struct{}, 0) のような操作は、もはやメモリ割り当て器に負荷をかけなくなります。

また、UseSpanTypeTypeInfo_SingleObject といったフラグは、Goのメモリ管理における型情報とスパン(メモリブロックの管理単位)の関連付けに関するもので、このコミットの主要な変更点ではありませんが、メモリ割り当て後の型情報のセットアップに関連しています。ゼロサイズのアロケーションの場合、これらの型情報の設定もスキップされるか、あるいは runtime·zerobase に関連付けられることになります。

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

変更は src/pkg/runtime/malloc.goc ファイルに集中しています。

具体的には、runtime·new 関数と runtime·cnew 関数の両方で、メモリ割り当てのロジックが変更されています。

--- a/src/pkg/runtime/malloc.goc
+++ b/src/pkg/runtime/malloc.goc
@@ -697,14 +697,22 @@ runtime·new(Type *typ, uint8 *ret)
 
  	if(raceenabled)
  		m->racepc = runtime·getcallerpc(&typ);
- 	flag = typ->kind&KindNoPointers ? FlagNoPointers : 0;
- 	ret = runtime·mallocgc(typ->size, flag, 1, 1);
 
- 	if(UseSpanType && !flag) {
- 		if(false) {
- 			runtime·printf("new %S: %p\n", *typ->string, ret);
+ 	if(typ->size == 0) {
+ 		// All 0-length allocations use this pointer.
+ 		// The language does not require the allocations to
+ 		// have distinct values.
+ 		ret = (uint8*)&runtime·zerobase;
+ 	} else {
+ 		flag = typ->kind&KindNoPointers ? FlagNoPointers : 0;
+ 		ret = runtime·mallocgc(typ->size, flag, 1, 1);
+
+ 		if(UseSpanType && !flag) {
+ 			if(false) {
+ 				runtime·printf("new %S: %p\n", *typ->string, ret);
+ 			}
+ 			runtime·settype(ret, (uintptr)typ | TypeInfo_SingleObject);
  		}
- 		runtime·settype(ret, (uintptr)typ | TypeInfo_SingleObject);
  	}
 
  	FLUSH(&ret);
@@ -719,15 +727,24 @@ runtime·cnew(Type *typ)
 
  	if(raceenabled)
  		m->racepc = runtime·getcallerpc(&typ);
- 	flag = typ->kind&KindNoPointers ? FlagNoPointers : 0;
- 	ret = runtime·mallocgc(typ->size, flag, 1, 1);
 
- 	if(UseSpanType && !flag) {
- 		if(false) {
- 			runtime·printf("new %S: %p\n", *typ->string, ret);
+ 	if(typ->size == 0) {
+ 		// All 0-length allocations use this pointer.
+ 		// The language does not require the allocations to
+ 		// have distinct values.
+ 		ret = (uint8*)&runtime·zerobase;
+ 	} else {
+ 		flag = typ->kind&KindNoPointers ? FlagNoPointers : 0;
+ 		ret = runtime·mallocgc(typ->size, flag, 1, 1);
+
+ 		if(UseSpanType && !flag) {
+ 			if(false) {
+ 				runtime·printf("new %S: %p\n", *typ->string, ret);
+ 			}
+ 			runtime·settype(ret, (uintptr)typ | TypeInfo_SingleObject);
  		}
- 		runtime·settype(ret, (uintptr)typ | TypeInfo_SingleObject);
  	}
+
  	return ret;
  }

コアとなるコードの解説

runtime·new および runtime·cnew 関数の変更

これらの関数は、Goの new 組み込み関数に対応するランタイムレベルの実装です。runtime·new は単一のオブジェクトを割り当てるために使用され、runtime·cnew はおそらくコンカレントなコンテキストでの新しいオブジェクト割り当てに関連しています。

変更のポイントは、両関数に共通して追加された以下の条件分岐です。

 	if(typ->size == 0) {
 		// All 0-length allocations use this pointer.
 		// The language does not require the allocations to
 		// have distinct values.
 		ret = (uint8*)&runtime·zerobase;
 	} else {
 		// ... 既存のメモリ割り当てロジック ...
 	}
  1. if(typ->size == 0):

    • これは、割り当てを要求された型のサイズが0バイトであるかどうかをチェックする条件です。例えば、new(struct{}) の場合、typ->size は0になります。
  2. ret = (uint8*)&runtime·zerobase;:

    • もし型サイズが0であれば、runtime·mallocgc を呼び出す代わりに、runtime·zerobase のアドレスを ret に代入します。
    • runtime·zerobase は、Goランタイムが内部的に管理する、ゼロサイズのアロケーション専用のグローバルな「ベースアドレス」です。これは実際のメモリ領域を指すわけではなく、概念的なポインタとして機能します。
    • この行により、すべてのゼロサイズのアロケーションが同じポインタ値を返すようになります。
  3. else { ... 既存のメモリ割り当てロジック ... }:

    • 型サイズが0でない場合(つまり、通常のメモリ割り当てが必要な場合)は、以前と同様に flag を設定し、runtime·mallocgc を呼び出して実際のメモリを割り当てます。
    • その後の UseSpanTyperuntime·settype などの処理は、割り当てられたメモリに型情報を関連付けるためのもので、ゼロサイズのアロケーションの場合はこのブロックがスキップされるため、これらの処理も行われません。

この変更により、Goランタイムはゼロサイズのアロケーションを特別扱いし、メモリ割り当て器とガベージコレクタの負荷を大幅に軽減できるようになりました。これは、Goプログラムの全体的なパフォーマンスと効率に貢献する重要な最適化です。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (Go Runtime, Memory Managementに関するセクション)
  • Go言語のソースコード (特に src/runtime ディレクトリ)
  • Go言語のIssueトラッカー (Issue #3996の議論)
  • Go言語のガベージコレクションに関する技術記事やブログポスト
  • Go言語の newmake に関する解説記事