[インデックス 12413] ファイルの概要
このコミットは、Go言語の標準ライブラリstrconvパッケージからbytesパッケージへの依存関係を削除することを目的としています。具体的には、文字列のクォート(引用符付け)およびアンクォート(引用符外し)を行うquote.goファイルにおいて、bytes.Bufferの使用を[]byteスライスとappend関数による直接的なバイト操作に置き換えています。これにより、strconvパッケージの独立性が高まり、潜在的なパフォーマンス改善が期待されます。
コミット
commit eab42619467c0bb5c52cb17868df1ac77691d9cc
Author: Rob Pike <r@golang.org>
Date: Tue Mar 6 15:25:42 2012 +1100
strconv: remove dependency on bytes
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/5759044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/eab42619467c0bb5c52cb17868df1ac77691d9cc
元コミット内容
strconv: remove dependency on bytes
変更の背景
この変更の主な背景は、Go言語の標準ライブラリにおけるパッケージ間の依存関係を最小限に抑え、より効率的なコードを実現することにあります。
strconvパッケージは、文字列と数値の変換、文字列のクォート/アンクォートなど、基本的なデータ型変換機能を提供します。以前は、文字列構築のためにbytes.Bufferを使用していましたが、これは内部的にバイトスライスを管理し、必要に応じて拡張する便利な機能を提供します。しかし、bytes.Bufferは汎用的なバッファリングメカニズムであり、strconvパッケージの特定のユースケースにおいては、オーバーヘッドが発生する可能性がありました。
このコミットでは、bytes.Bufferの代わりに、直接[]byteスライスを操作し、Goの組み込み関数appendを使用することで、より低レベルで効率的な文字列構築を実現しています。これにより、bytesパッケージへの依存が解消され、strconvパッケージがより軽量で独立した存在となります。また、appendはコンパイラによって最適化されることが多く、特定のシナリオではbytes.Bufferよりも優れたパフォーマンスを発揮する可能性があります。特に、最終的な文字列のサイズがある程度予測できる場合、初期容量を適切に設定することで、再割り当ての回数を減らし、パフォーマンスを向上させることができます。
go/build/deps_test.goの変更は、strconvパッケージの依存関係リストからbytesパッケージを削除したことを反映しています。これは、この変更が単なる実装の詳細ではなく、パッケージ間の依存関係の構造そのものに影響を与えることを示しています。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念と標準ライブラリの知識が必要です。
-
bytes.Buffer:bytesパッケージが提供する可変長のバイトバッファです。WriteByte,WriteString,WriteRuneなどのメソッドを通じてバイト列を効率的に構築できます。内部的には[]byteスライスを保持し、必要に応じて容量を自動的に拡張します。文字列の結合や構築によく用いられます。 -
[]byteスライスとappend関数: Go言語におけるスライスは、配列の一部を参照する動的なデータ構造です。[]byteはバイトのスライスを表します。append関数は、スライスに要素を追加するために使用されます。スライスの容量が不足する場合、appendは新しいより大きな基底配列を割り当て、既存の要素をコピーしてから新しい要素を追加します。この操作は、特に頻繁な再割り当てが発生する場合、パフォーマンスに影響を与える可能性がありますが、初期容量を適切に設定することでこれを軽減できます。 -
string型と[]byte型の変換: Go言語では、string型は不変のバイト列であり、UTF-8エンコードされたテキストを表します。stringと[]byteは相互に変換可能ですが、変換時にはデータのコピーが発生します。string(byteSlice):[]byteをstringに変換します。[]byte(str):stringを[]byteに変換します。
-
unicode/utf8パッケージ: UTF-8エンコーディングされたテキストを扱うためのユーティリティを提供します。utf8.RuneError: 無効なUTF-8シーケンスをデコードした際に返されるUnicodeの置換文字(U+FFFD)を表すrune定数です。utf8.DecodeRuneInString(s string) (r rune, size int): 文字列sの先頭から1つのUTF-8エンコードされたルーンをデコードし、そのルーンとバイト数を返します。utf8.EncodeRune(p []byte, r rune) (n int): ルーンrをUTF-8エンコードし、結果をバイトスライスpに書き込み、書き込まれたバイト数を返します。
-
unicodeパッケージ: Unicode文字のプロパティ(例: 印刷可能かどうか)を扱うための関数を提供します。unicode.IsPrint(r rune) bool: ルーンrが印刷可能文字であるかどうかを判定します。unicode.MaxASCII: ASCII文字の最大値(127)を表す定数です。
-
rune型: Go言語におけるrune型は、Unicodeコードポイントを表す組み込みのエイリアス型(int32)です。UTF-8エンコードされた文字列をイテレートする際に、各文字(ルーン)を表現するために使用されます。
技術的詳細
このコミットの技術的詳細の核心は、bytes.Bufferから[]byteスライスとappendへの移行が、Go言語における文字列構築のパフォーマンスとメモリ管理に与える影響にあります。
bytes.Bufferの挙動:
bytes.Bufferは、内部的に[]byteスライスを保持し、Write系のメソッドが呼び出されるたびに、必要に応じてそのスライスの容量を拡張します。容量が不足した場合、新しいより大きなスライスが割り当てられ、既存のデータがコピーされます。この自動的な容量管理は便利ですが、頻繁な書き込みや、最終的なサイズが予測できない場合には、複数の再割り当てとデータコピーが発生し、オーバーヘッドとなる可能性があります。
[]byteとappendによる直接操作:
このコミットでは、bytes.Bufferの代わりに、make([]byte, 0, 3*len(s)/2)のように初期容量を指定して[]byteスライスを直接作成しています。
make([]byte, 0, capacity): 長さ0で指定されたcapacityを持つバイトスライスを作成します。これにより、append操作がcapacityに達するまでは、新しい基底配列の割り当てとデータコピーを避けることができます。3*len(s)/2: これは、元の文字列sの長さの1.5倍を初期容量として見積もっています。文字列のクォート処理では、エスケープシーケンスの追加などにより、元の文字列よりも長くなる可能性があるため、ある程度の余裕を持たせた見積もりです。これにより、多くのケースで再割り当てを最小限に抑えることを意図しています。
パフォーマンスとメモリ効率:
- 再割り当ての削減:
bytes.Bufferの自動拡張は便利ですが、appendを直接使用し、適切な初期容量を見積もることで、不要な再割り当てとデータコピーを減らすことができます。これは、特に大量の文字列操作を行う場合に、パフォーマンス向上に寄与します。 - ガベージコレクションの負荷軽減: 再割り当てが減ることで、一時的な大きなスライスが生成される頻度が減り、ガベージコレクタの負荷が軽減される可能性があります。
- インライン化の可能性:
appendはGoコンパイラによって高度に最適化され、多くの場合インライン化されます。これにより、関数呼び出しのオーバーヘッドが削減され、より高速なコードが生成される可能性があります。bytes.Bufferのメソッド呼び出しは、通常、より多くの間接的な処理を伴います。
具体的な変更点:
buf.WriteByte(b)はbuf = append(buf, b)に。buf.WriteString(s)はbuf = append(buf, s...)に。buf.WriteRune(r)は、utf8.EncodeRuneでルーンをバイトスライスにエンコードし、その結果をappendする形に。これは、WriteRuneが内部的に行っていた処理を明示的に行っています。
これらの変更は、strconvパッケージが文字列操作の低レベルな詳細をより直接的に制御し、パフォーマンスを最適化するためのものです。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更は、src/pkg/strconv/quote.goファイルに集中しています。
diff --git a/src/pkg/go/build/deps_test.go b/src/pkg/go/build/deps_test.go
index b9f4b127ab..9038924058 100644
--- a/src/pkg/go/build/deps_test.go
+++ b/src/pkg/go/build/deps_test.go
@@ -52,7 +52,7 @@ var pkgDeps = map[string][]string{\
"math/rand": {"L0", "math"},\
"path": {"L0", "unicode/utf8", "strings"},\
"sort": {"math"},\
-" strconv": {"L0", "bytes", "unicode", "unicode/utf8", "math", "strings"},\
+" strconv": {"L0", "unicode", "unicode/utf8", "math", "strings"},\
"strings": {"L0", "unicode", "unicode/utf8"},\
"unicode": {},\
"unicode/utf16": {},\
diff --git a/src/pkg/strconv/quote.go b/src/pkg/strconv/quote.go
index 61dbcae70f..57cdae1738 100644
--- a/src/pkg/strconv/quote.go
+++ b/src/pkg/strconv/quote.go
@@ -5,7 +5,6 @@
package strconv
import (
- "bytes"
"strings"
"unicode"
"unicode/utf8"
@@ -14,8 +13,9 @@ import (
const lowerhex = "0123456789abcdef"
func quoteWith(s string, quote byte, ASCIIonly bool) string {
- var buf bytes.Buffer
- buf.WriteByte(quote)
+ var runeTmp [utf8.UTFMax]byte
+ buf := make([]byte, 0, 3*len(s)/2) // Try to avoid more allocations.
+ buf = append(buf, quote)
for width := 0; len(s) > 0; s = s[width:] {
r := rune(s[0])
width = 1
@@ -23,64 +23,65 @@ func quoteWith(s string, quote byte, ASCIIonly bool) string {
r, width = utf8.DecodeRuneInString(s)
}
if width == 1 && r == utf8.RuneError {
- buf.WriteString(`\x`)
- buf.WriteByte(lowerhex[s[0]>>4])
- buf.WriteByte(lowerhex[s[0]&0xF])
+ buf = append(buf, `\x`...)
+ buf = append(buf, lowerhex[s[0]>>4])
+ buf = append(buf, lowerhex[s[0]&0xF])
continue
}
if r == rune(quote) || r == '\\' { // always backslashed
- buf.WriteByte('\\')
- buf.WriteByte(byte(r))
+ buf = append(buf, '\\')
+ buf = append(buf, byte(r))
continue
}
if ASCIIonly {
if r <= unicode.MaxASCII && unicode.IsPrint(r) {
- buf.WriteRune(r)
+ buf = append(buf, byte(r))
continue
}
} else if unicode.IsPrint(r) {
- buf.WriteRune(r)
+ n := utf8.EncodeRune(runeTmp[:], r)
+ buf = append(buf, runeTmp[:n]...)
continue
}
switch r {
case '\a':
- buf.WriteString(`\a`)
+ buf = append(buf, `\a`...)
case '\b':
- buf.WriteString(`\b`)
+ buf = append(buf, `\b`...)
case '\f':
- buf.WriteString(`\f`)
+ buf = append(buf, `\f`...)
case '\n':
- buf.WriteString(`\n`)
+ buf = append(buf, `\n`...)
case '\r':
- buf.WriteString(`\r`)
+ buf = append(buf, `\r`...)
case '\t':
- buf.WriteString(`\t`)
+ buf = append(buf, `\t`...)
case '\v':
- buf.WriteString(`\v`)
+ buf = append(buf, `\v`...)
default:
switch {
case r < ' ':
- buf.WriteString(`\x`)
- buf.WriteByte(lowerhex[s[0]>>4])
- buf.WriteByte(lowerhex[s[0]&0xF])
+ buf = append(buf, `\x`...)
+ buf = append(buf, lowerhex[s[0]>>4])
+ buf = append(buf, lowerhex[s[0]&0xF])
case r > unicode.MaxRune:
r = 0xFFFD
fallthrough
case r < 0x10000:
- buf.WriteString(`\u`)
+ buf = append(buf, `\u`...)
for s := 12; s >= 0; s -= 4 {
- buf.WriteByte(lowerhex[r>>uint(s)&0xF])
+ buf = append(buf, lowerhex[r>>uint(s)&0xF])
}
default:
- buf.WriteString(`\U`)
+ buf = append(buf, `\U`...)
for s := 28; s >= 0; s -= 4 {
- buf.WriteByte(lowerhex[r>>uint(s)&0xF])
+ buf = append(buf, lowerhex[r>>uint(s)&0xF])
}
}
}
}\
- buf.WriteByte(quote)
- return buf.String()
+ buf = append(buf, quote)
+ return string(buf)
}
@@ -329,14 +330,15 @@ func Unquote(s string) (t string, err error) {
}
}
- var buf bytes.Buffer
+ var runeTmp [utf8.UTFMax]byte
+ buf := make([]byte, 0, 3*len(s)/2) // Try to avoid more allocations.
for len(s) > 0 {
c, multibyte, ss, err := UnquoteChar(s, quote)
if err != nil {
return "", err
}
s = ss
if c < utf8.RuneSelf || !multibyte {
- buf.WriteByte(byte(c))
+ buf = append(buf, byte(c))
} else {
- buf.WriteString(string(c))
+ n := utf8.EncodeRune(runeTmp[:], c)
+ buf = append(buf, runeTmp[:n]...)
}
if quote == '\'' && len(s) != 0 {
// single-quoted must be single character
return "", ErrSyntax
}
}
- return buf.String(), nil
+ return string(buf), nil
}
コアとなるコードの解説
src/pkg/strconv/quote.goファイルは、Go言語のstrconvパッケージ内で文字列をクォート(引用符付け)およびアンクォート(引用符外し)する機能を提供します。このコミットでは、主にquoteWith関数とUnquote関数が変更されています。
quoteWith関数の変更点:
-
bytesパッケージのインポート削除:- "bytes"bytes.Bufferを使用しなくなったため、bytesパッケージのインポートが不要になりました。 -
bytes.Bufferから[]byteスライスへの移行:- var buf bytes.Buffer - buf.WriteByte(quote) + var runeTmp [utf8.UTFMax]byte + buf := make([]byte, 0, 3*len(s)/2) // Try to avoid more allocations. + buf = append(buf, quote)var buf bytes.Bufferが削除され、代わりにbuf := make([]byte, 0, 3*len(s)/2)が導入されました。これは、長さ0で、元の文字列sの長さの1.5倍の容量を持つバイトスライスbufを初期化しています。これにより、多くのケースでappend操作による再割り当てを最小限に抑えることを目指しています。buf.WriteByte(quote)はbuf = append(buf, quote)に置き換えられました。
-
バイト書き込み操作の変更:
bytes.BufferのWriteByteやWriteStringメソッドの呼び出しが、すべてappend関数を使ったバイトスライスへの追加に置き換えられています。buf.WriteByte(b)->buf = append(buf, b)buf.WriteString(s)->buf = append(buf, s...)(文字列をバイトスライスとして展開して追加)buf.WriteRune(r)->n := utf8.EncodeRune(runeTmp[:], r); buf = append(buf, runeTmp[:n]...)runeTmpは、utf8.EncodeRuneがルーンをUTF-8バイト列にエンコードする際に使用する一時的なバイトスライスです。utf8.UTFMaxはUTF-8エンコードされたルーンが取りうる最大バイト数(4バイト)です。utf8.EncodeRuneでルーンをバイト列に変換し、その結果をbufに追加しています。
-
最終的な文字列変換:
- return buf.String() + return string(buf)bytes.BufferのString()メソッドの代わりに、構築された[]byteスライスを直接string()にキャストして文字列に変換しています。
Unquote関数の変更点:
Unquote関数も同様に、bytes.Bufferの使用を[]byteスライスとappendに置き換えています。
-
bytes.Bufferから[]byteスライスへの移行:- var buf bytes.Buffer + var runeTmp [utf8.UTFMax]byte + buf := make([]byte, 0, 3*len(s)/2) // Try to avoid more allocations.quoteWith関数と同様に、初期容量を指定した[]byteスライスbufが導入されています。 -
バイト書き込み操作の変更:
buf.WriteByte(byte(c))->buf = append(buf, byte(c))buf.WriteString(string(c))->n := utf8.EncodeRune(runeTmp[:], c); buf = append(buf, runeTmp[:n]...)- デコードされたルーン
cがマルチバイト文字の場合、utf8.EncodeRuneを使用してバイトスライスに変換し、bufに追加しています。
- デコードされたルーン
-
最終的な文字列変換:
- return buf.String(), nil + return string(buf), nil構築された
[]byteスライスを直接string()にキャストして文字列に変換しています。
これらの変更により、strconvパッケージはbytes.Bufferという抽象化レイヤーを介さずに、より直接的かつ効率的にバイトスライスを操作するようになりました。これは、Go言語の標準ライブラリが、パフォーマンスとリソース効率を追求する上で、低レベルな操作を重視する設計思想の一例と言えます。
関連リンク
- Go CL 5759044: https://golang.org/cl/5759044
参考にした情報源リンク
- Go言語の公式ドキュメント (
bytesパッケージ,stringsパッケージ,unicodeパッケージ,unicode/utf8パッケージ) - Go言語の
append関数の挙動に関する一般的な情報 - Go言語における
stringと[]byteの変換に関する情報 - Go言語の
bytes.Bufferの内部実装に関する情報 (Goのソースコード)