[インデックス 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
のように元のスライスを別の変数に代入すると、osrc
は src
と同じ基底配列を参照し続けます。もし src
が大きなデータの一部を切り出したスライスであった場合、osrc
がその大きな基底配列への参照を保持し続けるため、src
の処理が完了しても、osrc
がスコープを抜けるまで基底配列全体がガベージコレクションの対象とならない可能性があります。これは、不要なメモリが解放されずに保持され続ける「メモリリーク」の一種と見なすことができます。
この最適化は、このような不要なメモリ保持を防ぎ、ガベージコレクションの効率を向上させることを目的としています。特に、大量のBase32/Base64デコード処理が行われるようなシナリオでは、この変更が全体的なメモリフットプリントとパフォーマンスに寄与する可能性があります。
前提知識の解説
Go言語のスライスとメモリ管理
Go言語のスライスは、配列をラップした軽量なデータ構造です。スライスは以下の3つの要素から構成されます。
- ポインタ: スライスが参照する基底配列の先頭要素へのポインタ。
- 長さ (length): スライスに含まれる要素の数。
- 容量 (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/base64
の decode
メソッド内で、入力スライス src
の元の長さを計算するために、以下のような行がありました。
osrc := src
ここで osrc
は src
のコピー(スライスヘッダのコピー)であり、src
と同じ基底配列を参照します。デコード処理中に src
スライスは src = src[1:]
のように先頭から要素を消費していく形で短くなっていきますが、osrc
は元の src
が参照していた基底配列全体への参照を保持し続けます。
もし src
が非常に大きなバイト配列の一部を切り出したスライスであった場合、osrc
がその大きな基底配列への参照を保持している限り、その基底配列全体はガベージコレクションの対象になりません。これは、デコード処理中に src
が短くなっても、元の大きな配列がメモリ上に残り続けることを意味します。
このコミットでは、この問題を解決するために、osrc := src
を olen := len(src)
に変更しています。
olen := len(src)
olen
は src
の初期の長さを示す単なる整数値であり、元の 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.go
と base64.go
) の decode
メソッドにおいて、以下の変更が行われています。
-
osrc := src
の削除とolen := len(src)
の追加:- 変更前:
osrc := src
は、入力スライスsrc
のスライスヘッダをosrc
にコピーしていました。これにより、osrc
はsrc
と同じ基底配列を参照し続け、src
が短くなっても元の基底配列全体がガベージコレクションの対象とならない可能性がありました。 - 変更後:
olen := len(src)
は、src
の初期の長さ(要素数)を整数値としてolen
に格納します。これにより、olen
は元のスライスやその基底配列への参照を一切保持しません。デコード処理中にsrc
が短くなっても、元の基底配列への参照がolen
によって保持されることはなく、他の参照がなければ早期にGCの対象となり得ます。
- 変更前:
-
CorruptInputError
の引数変更:- エラー発生時に
CorruptInputError
を返す際の引数(エラーが発生したオフセットを示す値)が、len(osrc)
を使用していた箇所からolen
を使用するように変更されています。 - 例:
CorruptInputError(len(osrc) - len(src) - j)
がCorruptInputError(olen - len(src) - j)
に変更されています。 - これにより、エラーオフセットの計算も、元のスライスへの参照なしに、初期の長さ
olen
を基準に行われるようになります。
- エラー発生時に
この変更は、Goのスライスが参照する基底配列のライフタイム管理をより効率的に行い、不要なメモリ保持を防ぐことで、メモリフットプリントを削減し、ガベージコレクションの効率を向上させるという、Go言語のメモリ最適化における典型的なアプローチの一つです。
関連リンク
- Go言語の公式ドキュメント: https://go.dev/doc/
- Go言語のスライスに関するブログ記事 (Go公式ブログ): https://go.dev/blog/slices
- Base32エンコーディング (Wikipedia): https://ja.wikipedia.org/wiki/Base32
- Base64エンコーディング (Wikipedia): https://ja.wikipedia.org/wiki/Base64
参考にした情報源リンク
- Go言語のコミット履歴 (GitHub): https://github.com/golang/go/commits/master
- Go言語のコードレビューシステム (Gerrit): https://go-review.googlesource.com/ (コミットメッセージに記載されている
https://golang.org/cl/7568045
は、このGerritの変更リストへのリンクです。) - Go言語のスライスとメモリ管理に関する一般的な情報源(Go言語の書籍やオンラインチュートリアルなど)