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

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

このコミットは、Go言語の初期開発段階におけるsrc/lib/container/vector.goファイルに対する変更を記録しています。このファイルは、Go言語における動的配列(ベクター)の実装を提供していました。主な変更点は、ベクターの初期化方法の改善、要素のクリアによるガベージコレクションの効率化、および冗長なコメントの削除です。

コミット

commit 0c4c842eb07f65cf36c718e72a11c094b79296ae
Author: Robert Griesemer <gri@golang.org>
Date:   Fri Nov 14 11:22:39 2008 -0800

    - added Init() function so that vectors can be used w/o New():
            var v Vector.Vector;
            v.Init();
            ...
    - clear vector elements in Reset
    - removed some comments that seem redundant
    
    R=r
    DELTA=16  (9 added, 5 deleted, 2 changed)
    OCL=19247
    CL=19247

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

https://github.com/golang/go/commit/0c4c842eb07f65cf36c718e72a11c094b79296ae

元コミット内容

このコミットは、src/lib/container/vector.goファイルに対して以下の変更を加えています。

  1. Init()関数の追加: Vector型をNew()関数を使わずに宣言した場合でも、明示的に初期化できるようにInit()関数が導入されました。これにより、var v Vector.Vector; v.Init(); のような使い方が可能になります。
  2. Reset()関数におけるベクター要素のクリア: Reset()時に、ベクターが保持していた要素をnilに設定することで、ガベージコレクション(GC)がそれらの要素が占めていたメモリを適切に解放できるように改善されました。
  3. 冗長なコメントの削除: コードの可読性を向上させるため、Goのランタイムが自動的に範囲チェックを行うことに関するコメントなど、不要と判断されたコメントが削除されました。

変更の背景

このコミットは、Go言語がまだ初期段階にあった2008年11月に行われたものです。当時のGo言語は、その設計と標準ライブラリが活発に進化している最中でした。

  1. 初期化の柔軟性向上: New()関数はオブジェクトのインスタンスを生成し、初期化を行う一般的なパターンですが、Goでは構造体をゼロ値で宣言することも可能です。Init()関数を追加することで、New()を使わずに宣言されたベクターも適切に初期化できる柔軟性を提供し、一貫した初期化ロジックを共有できるようになりました。これは、Goの設計思想である「ゼロ値は有用であるべき」という原則と、より複雑な初期化が必要な場合のパターンを模索していた時期の反映と考えられます。
  2. ガベージコレクションの効率化: Goはガベージコレクタを持つ言語であり、不要になったメモリを自動的に解放します。しかし、スライス(ベクターの基盤)が以前に参照していた大きなオブジェクトが、スライスの長さが短くなっても基盤となる配列に残ってしまうことがあります。Reset()Remove()のような操作で要素をnilに明示的に設定することで、これらのオブジェクトへの参照を断ち切り、ガベージコレクタがそれらを「到達不能」と判断し、より早くメモリを解放できるようになります。これは、メモリリークの可能性を減らし、アプリケーションのメモリ使用量を最適化するために重要です。
  3. コードの簡潔化とGoらしい記述: Go言語は簡潔さと明瞭さを重視します。At(), Set(), Insert()関数から「範囲チェックはランタイムが行うため不要」といったコメントを削除することは、Goの言語仕様とランタイムの挙動を理解していれば自明な情報を排除し、コードをよりクリーンにするための変更です。これは、Goのイディオム(慣用的な書き方)が確立されていく過程の一部と言えます。

前提知識の解説

Go言語のスライス (Slice)

Go言語のスライスは、配列をラップした動的なビューです。スライスは以下の3つの要素から構成されます。

  • ポインタ: スライスが参照する基盤となる配列の先頭要素へのポインタ。
  • 長さ (Length): スライスに含まれる要素の数。len()関数で取得できます。
  • 容量 (Capacity): スライスが基盤となる配列のどこまで拡張できるかを示す最大長。cap()関数で取得できます。

スライスは、基盤となる配列の一部または全体を参照します。スライスを再スライス(例: s = s[low:high])しても、基盤となる配列は変更されません。新しいスライスは、同じ基盤配列の異なる部分を参照するだけです。

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

Go言語は自動メモリ管理(ガベージコレクション)を採用しています。ガベージコレクタは、プログラムがもはや到達できない(参照されていない)メモリ領域を自動的に識別し、解放します。

スライスの場合、スライスの長さが短くなっても、基盤となる配列はそのまま残ります。もし基盤配列の要素が大きなオブジェクトへの参照を保持しており、その要素がスライスの現在の長さの範囲外になったとしても、基盤配列自体が参照され続けている限り、その要素が参照していたオブジェクトはガベージコレクタによって解放されません。これを「メモリリーク」と呼ぶことがあります。

この問題を解決するためには、不要になった要素への参照を明示的にnilに設定することが有効です。nilに設定することで、ガベージコレクタは当該オブジェクトへの参照がなくなったと判断し、メモリを解放できるようになります。

New()関数とInit()メソッドのパターン

Go言語では、構造体のインスタンスを生成する方法がいくつかあります。

  • ゼロ値: var s MyStruct のように宣言すると、構造体は各フィールドのゼロ値で初期化されます。
  • 複合リテラル: s := MyStruct{Field1: value1} のように宣言と同時に初期化できます。
  • new()組み込み関数: p := new(MyStruct) は、MyStruct型のゼロ値のインスタンスを割り当て、そのポインタを返します。

慣用的に、New()という名前の関数は、特定の型(この場合はVector)の新しいインスタンスを生成し、初期化して返すファクトリ関数として使用されます。 一方、Init()という名前のメソッドは、既に存在するインスタンス(ポインタまたは値レシーバ)に対して、追加の初期化ロジックを実行するために使用されます。このコミットでは、New()が内部でInit()を呼び出すことで、初期化ロジックの重複を避け、一貫性を保つパターンが採用されています。

技術的詳細

Init()関数の導入とNew()からの呼び出し

変更前は、New()関数内で直接v.elem = new([]Element, 8) [0 : 0];という初期化が行われていました。これは、容量8のElement型スライスを生成し、長さを0に設定する操作です。 変更後は、この初期化ロジックがInit()という独立したメソッドに切り出され、New()関数はそのInit()メソッドを呼び出すようになりました。

// 変更前 (New関数内)
// v.elem = new([]Element, 8) [0 : 0];

// 変更後 (Init関数)
func (v *Vector) Init() {
	v.elem = new([]Element, 8) [0 : 0];  // capacity must be > 0!
}

// 変更後 (New関数)
export func New() *Vector {
	v := new(Vector);
	v.Init(); // Init()を呼び出す
	return v;
}

この変更により、Vector型の変数をvar v Vector.Vector;のように宣言した場合でも、v.Init();を呼び出すことで、New()関数を使用した場合と同じ初期状態に設定できるようになりました。これは、Goの構造体がゼロ値で初期化される特性を考慮し、明示的な初期化が必要な場合に柔軟性を提供します。

Reset()およびRemove()におけるnil設定によるGCサポート

Reset()関数はベクターを空にするために使用されます。変更前は単にスライスの長さを0にリセットするv.elem = v.elem[0:0];だけでした。しかし、これによりスライスが参照していた基盤配列の要素はそのまま残り、もしそれらが大きなオブジェクトへの参照を保持していた場合、ガベージコレクタはそのオブジェクトを解放できませんでした。

変更後、Reset()関数はスライスの要素を逆順にnilに設定するループを追加しました。

// 変更前 (Reset関数)
// v.elem = v.elem[0:0];

// 変更後 (Reset関数)
func (v *Vector) Reset() {
	// support GC, nil out entries
	for j := len(v.elem) - 1; j >= 0; j-- {
		v.elem[j] = nil; // 要素をnilに設定
	}
	v.elem = v.elem[0:0];
}

同様に、Remove()関数でも、削除された要素が占めていた位置をnilに設定する変更が行われました。

// 変更前 (Remove関数)
// var e Element;
// v.elem[n - 1] = e;  // don't set to nil - may not be legal in the future

// 変更後 (Remove関数)
v.elem[n - 1] = nil;  // support GC, nil out entry

これらの変更は、Goのガベージコレクタが不要なメモリをより効率的に解放できるようにするための重要な最適化です。特に、ベクターがポインタや大きな構造体への参照を多数保持している場合に、メモリ使用量の削減に貢献します。

冗長なコメントの削除

At(), Set(), Insert()といったメソッドから、// range check unnecessary - done by runtimeというコメントが削除されました。Go言語では、スライスや配列へのアクセス時にインデックスが範囲外である場合、ランタイムが自動的にパニック(実行時エラー)を発生させます。したがって、これらのコメントはGoの基本的な挙動を説明するものであり、コードの意図を明確にする上では冗長と判断されました。これは、Goのコードベースが成熟し、言語のイディオムが確立されていく過程で、より簡潔でGoらしい記述が推奨されるようになったことを示唆しています。

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

src/lib/container/vector.go

--- a/src/lib/container/vector.go
+++ b/src/lib/container/vector.go
@@ -24,9 +24,14 @@ export type Vector struct {
 }
 
 
+func (v *Vector) Init() {
+	v.elem = new([]Element, 8) [0 : 0];  // capacity must be > 0!
+}
+
+
 export func New() *Vector {
 	v := new(Vector);
-	v.elem = new([]Element, 8) [0 : 0];  // capacity must be > 0!
+	v.Init();
 	return v;
 }
 
@@ -37,13 +42,11 @@ func (v *Vector) Len() int {
 
 
 func (v *Vector) At(i int) Element {
-	// range check unnecessary - done by runtime
 	return v.elem[i];
 }
 
 
 func (v *Vector) Set(i int, e Element) {
-	// range check unnecessary - done by runtime
 	v.elem[i] = e;
 }
 
@@ -51,24 +54,25 @@ func (v *Vector) Set(i int, e Element) {
 func (v *Vector) Remove(i int) Element {
 	ret := v.elem[i];
 	n := v.Len();
-	// range check unnecessary - done by runtime
 	for j := i + 1; j < n; j++ {
 		v.elem[j - 1] = v.elem[j];
 	}
-	var e Element;
-	v.elem[n - 1] = e;  // don't set to nil - may not be legal in the future
+	v.elem[n - 1] = nil;  // support GC, nil out entry
 	v.elem = v.elem[0 : n - 1];
 	return ret;
 }
 
 
 func (v *Vector) Reset() {
+	// support GC, nil out entries
+	for j := len(v.elem) - 1; j >= 0; j-- {
+		v.elem[j] = nil;
+	}
 	v.elem = v.elem[0:0];
 }
 
 func (v *Vector) Insert(i int, e Element) {
 	n := v.Len();
-	// range check unnecessary - done by runtime
 
 	// grow array by doubling its capacity
 	if n == cap(v.elem) {

コアとなるコードの解説

Init()関数の追加

+func (v *Vector) Init() {
+	v.elem = new([]Element, 8) [0 : 0];  // capacity must be > 0!
+}

Vector構造体のポインタレシーバを持つInit()メソッドが追加されました。このメソッドは、Vectorの内部スライスelemを初期化します。具体的には、容量が8のElement型スライスを新しく作成し、その長さを0に設定します。これにより、ベクターは初期状態で空ですが、要素を追加するための十分な初期容量を持つことになります。コメントにあるcapacity must be > 0!は、スライスの初期容量が0だと、要素を追加するたびに新しい基盤配列の割り当てが必要になり、パフォーマンスが低下する可能性があるため、ある程度の初期容量を確保していることを示唆しています。

New()関数からのInit()呼び出し

 export func New() *Vector {
 	v := new(Vector);
-	v.elem = new([]Element, 8) [0 : 0];  // capacity must be > 0!
+	v.Init();
 	return v;
 }

New()関数は、Vector型の新しいインスタンスを生成し、そのポインタを返します。変更前はNew()関数内で直接初期化ロジックが記述されていましたが、変更後は新しく追加されたInit()メソッドを呼び出すように変更されました。これにより、初期化ロジックが一箇所に集約され、コードの重複が排除され、保守性が向上しました。

At(), Set(), Insert()からのコメント削除

-	// range check unnecessary - done by runtime

これらのメソッドから、Goのランタイムが自動的に範囲チェックを行うことを示すコメントが削除されました。これは、Go言語の設計において、スライスや配列のインデックスアクセスは常にランタイムによって境界チェックされるため、開発者が明示的にコメントで言及する必要がないという判断に基づいています。コードがより簡潔になり、Goのイディオムに沿ったものになりました。

Remove()におけるnil設定

-	var e Element;
-	v.elem[n - 1] = e;  // don't set to nil - may not be legal in the future
+	v.elem[n - 1] = nil;  // support GC, nil out entry

Remove()関数は、指定されたインデックスの要素を削除し、後続の要素を前方にシフトします。削除された要素が元々占めていた最後の位置(n - 1)は、もはや有効なデータを含みません。変更前は、Element型のゼロ値で上書きされていましたが、変更後は明示的にnilに設定されるようになりました。これは、Elementがポインタ型である場合に、ガベージコレクタが不要になったオブジェクトを適切に解放できるようにするためです。以前のコメントdon't set to nil - may not be legal in the futureは、Goの型システムやGCの挙動に関する初期の不確実性や制約があったことを示唆していますが、このコミットの時点ではnil設定が適切かつ合法であると判断されたことを意味します。

Reset()におけるnil設定

 func (v *Vector) Reset() {
+	// support GC, nil out entries
+	for j := len(v.elem) - 1; j >= 0; j-- {
+		v.elem[j] = nil;
+	}
 	v.elem = v.elem[0:0];
 }

Reset()関数はベクターを空の状態に戻します。変更前は単にスライスの長さを0にリセットするだけでしたが、変更後は、スライスの全要素を逆順にnilに設定するループが追加されました。この変更もRemove()と同様に、ベクターが保持していたオブジェクトへの参照を明示的に解除し、ガベージコレクタがそれらのオブジェクトを解放できるようにするためのものです。これにより、ベクターが以前に大量のオブジェクトを保持していた場合に、メモリリークを防ぎ、メモリ効率を向上させることができます。

関連リンク

参考にした情報源リンク

  • Go言語のコミット履歴 (GitHub): https://github.com/golang/go/commits/master
  • Go言語の初期の設計に関する議論やドキュメント (Goの公式リポジトリやメーリングリストのアーカイブなど)
  • Go言語のSliceの内部構造に関する解説記事 (例: "Go Slices: usage and internals" by The Go Authors)
  • Go言語のガベージコレクションに関する技術記事やブログポスト