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

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

このコミットは、Go言語の標準ライブラリである encoding/base32 および encoding/base64 パッケージにおける、スタック空間の最適化を目的とした変更です。具体的には、デコード処理中に元の入力スライスへの参照を保持する方法を変更することで、メモリ使用量を削減し、ガベージコレクションの効率を向上させています。

コミット

commit 96e0c0c7648bc4dbdf433d14c43f6f31d4f13472
Author: Nigel Tao <nigeltao@golang.org>
Date:   Tue Mar 12 11:24:24 2013 +1100

    encoding/base32, encoding/base64: a small stack-space optimization.
    
    R=dsymonds, dave
    CC=golang-dev
    https://golang.org/cl/7568045

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

https://github.com/golang/go/commit/96e0c0c7648bc4dbdf433d14c43f6f31d4f13472

元コミット内容

encoding/base32, encoding/base64: a small stack-space optimization.

このコミットは、encoding/base32 および encoding/base64 パッケージにおいて、わずかなスタック空間の最適化を行うものです。

変更の背景

Go言語では、関数呼び出し時に引数やローカル変数がスタックに割り当てられます。スライスは、その実体が配列へのポインタ、長さ、容量から構成される構造体です。関数にスライスが渡される際、スライスヘッダ(ポインタ、長さ、容量)はスタックにコピーされますが、スライスが参照する基底配列はヒープに存在することが多いです。

このコミットが行われた2013年当時、Goのコンパイラやランタイムは現在ほど成熟しておらず、特にメモリ管理やガベージコレクション(GC)の最適化は継続的に行われていました。この変更の背景には、encoding/base32 および encoding/base64 のデコード関数において、入力スライス src の元の長さを計算するために osrc := src のように元のスライス全体をコピーして保持していたことが挙げられます。

Goのスライスは参照型のように振る舞いますが、実際にはスライスヘッダがコピーされる値型です。しかし、osrc := src のように元のスライスを別の変数に代入すると、osrcsrc と同じ基底配列を参照し続けます。もし src が大きなデータの一部を切り出したスライスであった場合、osrc がその大きな基底配列への参照を保持し続けるため、src の処理が完了しても、osrc がスコープを抜けるまで基底配列全体がガベージコレクションの対象とならない可能性があります。これは、不要なメモリが解放されずに保持され続ける「メモリリーク」の一種と見なすことができます。

この最適化は、このような不要なメモリ保持を防ぎ、ガベージコレクションの効率を向上させることを目的としています。特に、大量のBase32/Base64デコード処理が行われるようなシナリオでは、この変更が全体的なメモリフットプリントとパフォーマンスに寄与する可能性があります。

前提知識の解説

Go言語のスライスとメモリ管理

Go言語のスライスは、配列をラップした軽量なデータ構造です。スライスは以下の3つの要素から構成されます。

  1. ポインタ: スライスが参照する基底配列の先頭要素へのポインタ。
  2. 長さ (length): スライスに含まれる要素の数。
  3. 容量 (capacity): スライスの先頭要素から基底配列の末尾までの要素の数。

スライスを別の変数に代入したり、関数に引数として渡したりすると、このスライスヘッダ(ポインタ、長さ、容量)がコピーされます。このため、複数のスライスが同じ基底配列の一部または全体を参照することが可能です。

arr := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := arr[2:5] // s1 は arr の 2, 3, 4 を参照
s2 := s1      // s2 も arr の 2, 3, 4 を参照

この特性は非常に強力ですが、注意しないと意図しないメモリ保持につながることがあります。特に、大きな配列から小さなスライスを切り出し、その小さなスライスだけが必要になった場合でも、元の大きな配列への参照がどこかに残っていると、その大きな配列全体がGCの対象とならず、メモリが解放されないことがあります。

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

Go言語は自動メモリ管理(ガベージコレクション)を採用しています。GCは、プログラムが動的に割り当てたメモリのうち、もはや到達不可能(参照されていない)になったメモリ領域を自動的に解放する仕組みです。GCの目的は、プログラマが手動でメモリを管理する手間を省き、メモリリークを防ぐことですが、GC自体にもコストがかかります。不要なメモリが長く保持されると、GCの頻度が増えたり、GCにかかる時間が長くなったりして、アプリケーションのパフォーマンスに影響を与える可能性があります。

Base32/Base64エンコーディング

  • Base32エンコーディング: 32種類の文字(A-Zと2-7)を使用してバイナリデータをテキスト形式に変換するエンコーディング方式です。主に、人間が読みやすく、手動で入力しやすい形式でデータを表現する際に使用されます。5ビット単位でデータを処理し、8文字のBase32出力が5バイトのバイナリデータに対応します。
  • Base64エンコーディング: 64種類の文字(A-Z, a-z, 0-9, +, /)とパディング文字(=)を使用してバイナリデータをテキスト形式に変換するエンコーディング方式です。主に、バイナリデータをテキストベースのプロトコル(例: HTTP、MIME)で安全に転送するために使用されます。6ビット単位でデータを処理し、4文字のBase64出力が3バイトのバイナリデータに対応します。

これらのエンコーディング/デコーディング処理は、大量のデータを扱う場合、メモリ効率が重要になります。

技術的詳細

このコミットの技術的詳細は、Goのスライスが持つ参照特性とガベージコレクションの挙動を理解しているかどうかにかかっています。

変更前は、encoding/base32 および encoding/base64decode メソッド内で、入力スライス src の元の長さを計算するために、以下のような行がありました。

osrc := src

ここで osrcsrc のコピー(スライスヘッダのコピー)であり、src と同じ基底配列を参照します。デコード処理中に src スライスは src = src[1:] のように先頭から要素を消費していく形で短くなっていきますが、osrc は元の src が参照していた基底配列全体への参照を保持し続けます。

もし src が非常に大きなバイト配列の一部を切り出したスライスであった場合、osrc がその大きな基底配列への参照を保持している限り、その基底配列全体はガベージコレクションの対象になりません。これは、デコード処理中に src が短くなっても、元の大きな配列がメモリ上に残り続けることを意味します。

このコミットでは、この問題を解決するために、osrc := srcolen := len(src) に変更しています。

olen := len(src)

olensrc の初期の長さを示す単なる整数値であり、元の src スライスやその基底配列への参照を保持しません。これにより、decode 関数内で src スライスが短くなり、元の基底配列への参照が不要になった時点で、他の参照がなければその基底配列がガベージコレクションの対象となり、メモリが解放される可能性が高まります。

エラー発生時の CorruptInputError の引数も、len(osrc) - len(src) - j のような式から olen - len(src) - j のような式に変更されています。これにより、エラーメッセージのオフセット計算も、元のスライスへの参照なしに、初期の長さ olen を基準に行われるようになります。

この変更は、特にストリーム処理のように、入力データが一度にすべてメモリにロードされるのではなく、チャンクごとに処理されるようなシナリオで、メモリフットプリントを削減する効果が期待できます。

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

src/pkg/encoding/base32/base32.go

--- a/src/pkg/encoding/base32/base32.go
+++ b/src/pkg/encoding/base32/base32.go
@@ -230,7 +230,7 @@ func (e CorruptInputError) Error() string {
 // indicates if end-of-message padding was encountered and thus any
 // additional data is an error.
 func (enc *Encoding) decode(dst, src []byte) (n int, end bool, err error) {
-	osrc := src
+	olen := len(src)
 	for len(src) > 0 && !end {
 		// Decode quantum using the base32 alphabet
 		var dbuf [8]byte
@@ -238,7 +238,7 @@ func (enc *Encoding) decode(dst, src []byte) (n int, end bool, err error) {
 
 		for j := 0; j < 8; {
 			if len(src) == 0 {
-				return n, false, CorruptInputError(len(osrc) - len(src) - j)
+				return n, false, CorruptInputError(olen - len(src) - j)
 			}
 			in := src[0]
 			src = src[1:]
@@ -250,12 +250,12 @@ func (enc *Encoding) decode(dst, src []byte) (n int, end bool, err error) {
 			// We've reached the end and there's padding
 			if len(src)+j < 8-1 {
 				// not enough padding
-				return n, false, CorruptInputError(len(osrc))
+				return n, false, CorruptInputError(olen)
 			}
 			for k := 0; k < 8-1-j; k++ {
 				if len(src) > k && src[k] != '=' {
 					// incorrect padding
-					return n, false, CorruptInputError(len(osrc) - len(src) + k - 1)
+					return n, false, CorruptInputError(olen - len(src) + k - 1)
 				}
 			}
 			dlen, end = j, true
@@ -265,13 +265,13 @@ func (enc *Encoding) decode(dst, src []byte) (n int, end bool, err error) {
 			// Examples" for an illustration for how the the 1st, 3rd and 6th base32
 			// src bytes do not yield enough information to decode a dst byte.
 			if dlen == 1 || dlen == 3 || dlen == 6 {
-				return n, false, CorruptInputError(len(osrc) - len(src) - 1)
+				return n, false, CorruptInputError(olen - len(src) - 1)
 			}
 			break
 		}
 		dbuf[j] = enc.decodeMap[in]
 		if dbuf[j] == 0xFF {
-			return n, false, CorruptInputError(len(osrc) - len(src) - 1)
+			return n, false, CorruptInputError(olen - len(src) - 1)
 		}
 		j++
 	}

src/pkg/encoding/base64/base64.go

--- a/src/pkg/encoding/base64/base64.go
+++ b/src/pkg/encoding/base64/base64.go
@@ -210,7 +210,7 @@ func (e CorruptInputError) Error() string {
 // indicates if end-of-message padding was encountered and thus any
 // additional data is an error.
 func (enc *Encoding) decode(dst, src []byte) (n int, end bool, err error) {
-	osrc := src
+	olen := len(src)
 	for len(src) > 0 && !end {
 		// Decode quantum using the base64 alphabet
 		var dbuf [4]byte
@@ -218,7 +218,7 @@ func (enc *Encoding) decode(dst, src []byte) (n int, end bool, err error) {
 
 		for j := 0; j < 4; {
 			if len(src) == 0 {
-				return n, false, CorruptInputError(len(osrc) - len(src) - j)
+				return n, false, CorruptInputError(olen - len(src) - j)
 			}
 			in := src[0]
 			src = src[1:]
@@ -230,18 +230,18 @@ func (enc *Encoding) decode(dst, src []byte) (n int, end bool, err error) {
 			// We've reached the end and there's padding
 			if len(src)+j < 4-1 {
 				// not enough padding
-				return n, false, CorruptInputError(len(osrc))
+				return n, false, CorruptInputError(olen)
 			}
 			if len(src) > 0 && src[0] != '=' {
 				// incorrect padding
-				return n, false, CorruptInputError(len(osrc) - len(src) - 1)
+				return n, false, CorruptInputError(olen - len(src) - 1)
 			}
 			dlen, end = j, true
 			break
 		}
 		dbuf[j] = enc.decodeMap[in]
 		if dbuf[j] == 0xFF {
-			return n, false, CorruptInputError(len(osrc) - len(src) - 1)
+			return n, false, CorruptInputError(olen - len(src) - 1)
 		}
 		j++
 	}

コアとなるコードの解説

両方のファイル (base32.gobase64.go) の decode メソッドにおいて、以下の変更が行われています。

  1. osrc := src の削除と olen := len(src) の追加:

    • 変更前: osrc := src は、入力スライス src のスライスヘッダを osrc にコピーしていました。これにより、osrcsrc と同じ基底配列を参照し続け、src が短くなっても元の基底配列全体がガベージコレクションの対象とならない可能性がありました。
    • 変更後: olen := len(src) は、src の初期の長さ(要素数)を整数値として olen に格納します。これにより、olen は元のスライスやその基底配列への参照を一切保持しません。デコード処理中に src が短くなっても、元の基底配列への参照が olen によって保持されることはなく、他の参照がなければ早期にGCの対象となり得ます。
  2. CorruptInputError の引数変更:

    • エラー発生時に CorruptInputError を返す際の引数(エラーが発生したオフセットを示す値)が、len(osrc) を使用していた箇所から olen を使用するように変更されています。
    • 例: CorruptInputError(len(osrc) - len(src) - j)CorruptInputError(olen - len(src) - j) に変更されています。
    • これにより、エラーオフセットの計算も、元のスライスへの参照なしに、初期の長さ olen を基準に行われるようになります。

この変更は、Goのスライスが参照する基底配列のライフタイム管理をより効率的に行い、不要なメモリ保持を防ぐことで、メモリフットプリントを削減し、ガベージコレクションの効率を向上させるという、Go言語のメモリ最適化における典型的なアプローチの一つです。

関連リンク

参考にした情報源リンク

  • Go言語のコミット履歴 (GitHub): https://github.com/golang/go/commits/master
  • Go言語のコードレビューシステム (Gerrit): https://go-review.googlesource.com/ (コミットメッセージに記載されている https://golang.org/cl/7568045 は、このGerritの変更リストへのリンクです。)
  • Go言語のスライスとメモリ管理に関する一般的な情報源(Go言語の書籍やオンラインチュートリアルなど)