[インデックス 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のソースコード)