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

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

このコミットは、Go言語の初期ランタイムにおける配列操作に関する2つの重要な変更を含んでいます。一つは、配列の生成やスライス操作において、戻り値となるポインタが正しくメモリにフラッシュされるようにするバグ修正です。もう一つは、デバッグ時の配列情報の可視性を向上させるための新しいprintarray関数の導入です。

コミット

commit 61e0fcce8ae1642c661c2bc900d18c42af209c2e
Author: Ken Thompson <ken@golang.org>
Date:   Wed Dec 17 12:13:19 2008 -0800

    small bug
    new printarray
    
    R=r
    OCL=21429
    CL=21429

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

https://github.com/golang/go/commit/61e0fcce8ae1642c661c2bc900d18c42af209c2e

元コミット内容

small bug
new printarray

R=r
OCL=21429
CL=21429

変更の背景

このコミットは、Go言語の初期開発段階におけるランタイムの安定性とデバッグ能力の向上を目的としています。

  1. バグ修正(FLUSHマクロの適用対象): Go言語のランタイムは、C言語で記述されており、ポインタ操作が頻繁に行われます。特に、関数がポインタを返す場合、そのポインタが確実にメモリに書き込まれていることを保証することが重要です。初期のコードでは、一時的なローカル変数に対してFLUSHマクロが適用されていましたが、本来は関数の戻り値として使用されるポインタに対して適用されるべきでした。この誤りは、最適化や並行処理のコンテキストにおいて、古いポインタ値が使用されてしまう可能性のある潜在的なバグを引き起こす可能性がありました。この修正は、ポインタの整合性を保証し、ランタイムの堅牢性を高めるために行われました。

  2. デバッグ機能の強化(printarray: ソフトウェア開発において、デバッグは不可欠なプロセスです。特にランタイムのような低レベルのコードでは、データ構造の内部状態を正確に把握することが重要になります。以前は、配列のデバッグ出力には汎用的なポインタ表示関数(sys·printpointer)が使用されていました。しかし、配列は単なるメモリ上のアドレスだけでなく、要素数(nel)や容量(cap)といった重要なメタデータを持っています。これらの情報をデバッグ時に一目で確認できるようにすることで、開発者が配列の挙動をより迅速かつ正確に理解できるようになり、デバッグ効率が大幅に向上します。

前提知識の解説

  • Go言語ランタイム: Go言語のプログラムは、Goランタイム上で動作します。このランタイムは、ガベージコレクション、スケジューリング、メモリ管理、プリミティブなデータ構造の操作など、Goプログラムの実行に必要な低レベルの機能を提供します。初期のGoランタイムは、主にC言語で記述されていました。
  • Array構造体: Go言語の配列やスライスは、内部的にはランタイムのArray構造体によって表現されます。この構造体は、配列のデータへのポインタ、要素数(nel)、容量(cap)などの情報を含んでいます。
  • FLUSHマクロ: C言語のコンテキストでは、コンパイラの最適化によって変数の値がレジスタに保持され、メモリに書き込まれないことがあります。特に、ポインタのような重要な値がメモリに書き込まれていないと、他のスレッドや関数がその古い値を見てしまう可能性があります。FLUSHマクロ(または同様のメカニズム)は、特定の変数の値を強制的にメモリに書き出すことを保証するために使用されます。これは、並行処理や低レベルのシステムプログラミングにおいて、データの可視性と整合性を確保するために非常に重要です。
  • sys·プレフィックス: GoランタイムのC言語コードでは、システムコールやランタイム内部関数にsys·というプレフィックスが付けられることがよくあります。これは、それらがGo言語のユーザーコードから直接呼び出されることを意図していない、低レベルの内部関数であることを示唆しています。
  • printssys·printintsys·printpointer: これらはGoランタイム内部で使用されるデバッグ出力関数です。printsは文字列を、sys·printintは整数を、sys·printpointerはポインタのアドレスをそれぞれ出力します。

技術的詳細

このコミットは、src/runtime/array.csrc/runtime/runtime.hの2つのファイルに影響を与えています。

  1. FLUSHマクロの修正:

    • sys·newarray (新しい配列の作成), sys·arraysliced (既存の配列からスライスを作成), sys·arrayslices (バイト配列からスライスを作成), sys·arrays2d (2次元配列の作成) といった、配列やスライスを生成・操作する複数の関数において、FLUSH(&d);という記述がFLUSH(&ret);に変更されました。
    • これらの関数では、dは関数内で一時的に作成されるArray構造体へのポインタです。一方、retは関数の引数として渡され、最終的に呼び出し元に返されるArray構造体へのポインタです。
    • 元のコードでは、一時変数dがフラッシュされていましたが、関数が返す実際の値はretに代入されたポインタです。したがって、retが指すメモリ位置が確実に更新されるように、FLUSHマクロの適用対象をretに変更する必要がありました。これにより、呼び出し元が常に最新のポインタ値を受け取ることが保証されます。
  2. sys·printarray関数の追加と利用:

    • src/runtime/array.cの末尾に、新しいC関数sys·printarray(Array *a)が追加されました。
    • この関数は、引数としてArray構造体へのポインタを受け取り、その内部情報を整形してデバッグ出力します。具体的には、[要素数,容量]データポインタという形式で出力されます。
      • a->nel: 配列の現在の要素数。
      • a->cap: 配列の最大容量。
      • a->array: 配列の実際のデータが格納されているメモリ領域へのポインタ。
    • この新しい関数は、既存の配列操作関数(sys·newarray, sys·arraysliced, sys·arrayslices, sys·arrays2d)内のデバッグ出力部分で、sys·printpointer(ret);の代わりにsys·printarray(ret);として使用されるようになりました。これにより、デバッグログに配列の要素数と容量が追加され、より詳細な情報が得られるようになりました。
    • src/runtime/runtime.hには、この新しい関数のプロトタイプ宣言void sys·printarray(Array*);が追加され、他のランタイムファイルからこの関数が利用可能になりました。

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

src/runtime/array.c

--- a/src/runtime/array.c
+++ b/src/runtime/array.c
@@ -23,7 +23,7 @@ sys·newarray(uint32 nel, uint32 cap, uint32 width, Array* ret)
  	d->array = d->b;
 
  	ret = d;
-	FLUSH(&d);
+	FLUSH(&ret);
 
  	if(debug) {
  		prints("newarray: nel=");
@@ -33,7 +33,7 @@ sys·newarray(uint32 nel, uint32 cap, uint32 width, Array* ret)
  		prints("; width=");
  		sys·printint(width);
  		prints("; ret=");
-		sys·printpointer(ret);
+		sys·printarray(ret);
  		prints("\n");
  	}
  }
@@ -85,11 +85,11 @@ sys·arraysliced(Array* old, uint32 lb, uint32 hb, uint32 width, Array* ret)
  	d->array = old->array + lb*width;
 
  	ret = d;
-	FLUSH(&d);
+	FLUSH(&ret);
 
  	if(debug) {
  		prints("sys·arrayslices: old=");
-		sys·printpointer(old);
+		sys·printarray(old);
  		prints("; lb=");
  		sys·printint(lb);
  		prints("; hb=");
@@ -97,7 +97,7 @@ sys·arraysliced(Array* old, uint32 lb, uint32 hb, uint32 width, Array* ret)
  		prints("; width=");
  		sys·printint(width);
  		prints("; ret=");
-		sys·printpointer(ret);
+		sys·printarray(ret);
  		prints("\n");
  	}
  }
@@ -132,7 +132,7 @@ sys·arrayslices(byte* old, uint32 nel, uint32 lb, uint32 hb, uint32 width, Arra
  	d->array = old + lb*width;
 
  	ret = d;
-	FLUSH(&d);
+	FLUSH(&ret);
 
  	if(debug) {
  		prints("sys·arrayslices: old=");
@@ -146,7 +146,7 @@ sys·arrayslices(byte* old, uint32 nel, uint32 lb, uint32 hb, uint32 width, Arra
  		prints("; width=");
  		sys·printint(width);
  		prints("; ret=");
-		sys·printpointer(ret);
+		sys·printarray(ret);
  		prints("\n");
  	}
  }
@@ -164,15 +164,24 @@ sys·arrays2d(byte* old, uint32 nel, Array* ret)
  	d->array = old;
 
  	ret = d;
-	FLUSH(&d);
+	FLUSH(&ret);
 
  	if(debug) {
  		prints("sys·arrays2d: old=");
  		sys·printpointer(old);
-	 	prints("; nel=");
-	 	sys·printint(nel);
  		prints("; ret=");
-		sys·printpointer(ret);
+		sys·printarray(ret);
  		prints("\n");
  	}
  }
+\n+void\n+sys·printarray(Array *a)\n+{\n+\tprints("[");\n+\tsys·printint(a->nel);\n+\tprints(",");\n+\tsys·printint(a->cap);\n+\tprints("]");\n+\tsys·printpointer(a->array);\n+}\n```

### `src/runtime/runtime.h`

```diff
--- a/src/runtime/runtime.h
+++ b/src/runtime/runtime.h
@@ -348,6 +348,7 @@ void	sys·printpc(void*);
 void	sys·printpointer(void*);
 void	sys·printuint(uint64);
 void	sys·printhex(uint64);
+void	sys·printarray(Array*);
 void	sys·catstring(string, string, string);
 void	sys·cmpstring(string, string, int32);
 void	sys·slicestring(string, int32, int32, string);

コアとなるコードの解説

  • FLUSH(&ret);への変更:

    • sys·newarray, sys·arraysliced, sys·arrayslices, sys·arrays2dの各関数内で、ret = d;の直後にFLUSH(&d);があった箇所がFLUSH(&ret);に変更されています。
    • これは、関数が返すポインタretが、dが指す新しい配列データ構造を指すように設定された後、そのretポインタ自体がメモリに確実に書き込まれるようにするための修正です。これにより、呼び出し元がretを使用する際に、最新の正しいポインタ値が参照されることが保証されます。
  • sys·printarray(Array *a)関数の追加:

    • この新しい関数は、Array構造体のデバッグ情報を整形して出力します。
    • prints("[");sys·printint(a->nel);prints(",");sys·printint(a->cap);prints("]");sys·printpointer(a->array);という一連の呼び出しによって、[要素数,容量]データポインタという形式の文字列が生成されます。
    • 例えば、要素数10、容量20、データポインタが0x12345678の配列であれば、[10,20]0x12345678のような出力が得られます。
    • これにより、デバッグログから配列の現在の状態(使用されている要素数と確保されている容量)を即座に把握できるようになり、デバッグ作業の効率が向上します。
  • sys·printarrayの利用:

    • 既存の配列操作関数のデバッグ出力部分で、sys·printpointer(ret);sys·printarray(ret);に置き換えられています。
    • これにより、これらの関数がデバッグモードで実行された際に、より詳細な配列情報がログに出力されるようになります。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (現在のバージョン): https://go.dev/doc/
  • Gitのdiffコマンドに関するドキュメント: https://git-scm.com/docs/git-diff
  • C言語におけるvolatileキーワードやメモリバリアに関する一般的な情報 (FLUSHマクロの背景理解のため): (特定のURLは挙げませんが、C言語のメモリモデルや最適化に関する一般的なプログラミングリソースを参照)
  • Go言語の内部構造に関するブログ記事や解説 (初期のランタイムに関するものは少ないですが、現在のランタイムの理解に役立ちます): (特定のURLは挙げませんが、"Go runtime internals"などで検索)
  • Ken Thompson氏の業績に関する情報: (特定のURLは挙げませんが、"Ken Thompson Go"などで検索)