[インデックス 19515] ファイルの概要
このコミットは、Go言語の標準ライブラリ encoding/base64
および encoding/base32
パッケージにおける DecodeString
関数のパフォーマンス改善を目的としています。具体的には、デコード処理中に発生していた不要な改行文字の除去処理を最適化し、デコード速度を向上させています。
コミット
commit afb7b67ae99c5edfb5210ad015be934b96ecc445
Author: Rui Ueyama <ruiu@google.com>
Date: Wed Jun 11 11:22:08 2014 -0700
encoding/base64, encoding/base32: make DecodeString faster
Previously, an input string was stripped of newline
characters at the beginning of DecodeString and then passed
to Decode. Decode again tried to strip newline characters.
That's waste of time.
benchmark old MB/s new MB/s speedup
BenchmarkDecodeString 38.37 65.20 1.70x
LGTM=dave, bradfitz
R=golang-codereviews, dave, bradfitz
CC=golang-codereviews
https://golang.org/cl/91770051
---
src/pkg/encoding/base32/base32.go | 2 +-\n src/pkg/encoding/base64/base64.go | 2 +-\n src/pkg/encoding/base64/base64_test.go | 8 ++++++++\n 3 files changed, 10 insertions(+), 2 deletions(-)
diff --git a/src/pkg/encoding/base32/base32.go b/src/pkg/encoding/base32/base32.go
index d770de3915..7613de24d2 100644
--- a/src/pkg/encoding/base32/base32.go
+++ b/src/pkg/encoding/base32/base32.go
@@ -330,7 +330,7 @@ func (enc *Encoding) Decode(dst, src []byte) (n int, err error) {
func (enc *Encoding) DecodeString(s string) ([]byte, error) {
s = strings.Map(removeNewlinesMapper, s)
dbuf := make([]byte, enc.DecodedLen(len(s)))
- n, err := enc.Decode(dbuf, []byte(s))
+ n, _, err := enc.decode(dbuf, []byte(s))
return dbuf[:n], err
}
diff --git a/src/pkg/encoding/base64/base64.go b/src/pkg/encoding/base64/base64.go
index e38c26d0ec..4f1fcad917 100644
--- a/src/pkg/encoding/base64/base64.go
+++ b/src/pkg/encoding/base64/base64.go
@@ -295,7 +295,7 @@ func (enc *Encoding) Decode(dst, src []byte) (n int, err error) {
func (enc *Encoding) DecodeString(s string) ([]byte, error) {
s = strings.Map(removeNewlinesMapper, s)
dbuf := make([]byte, enc.DecodedLen(len(s)))
- n, err := enc.Decode(dbuf, []byte(s))
+ n, _, err := enc.decode(dbuf, []byte(s))
return dbuf[:n], err
}
diff --git a/src/pkg/encoding/base64/base64_test.go b/src/pkg/encoding/base64/base64_test.go
index a075194e03..691edb755b 100644
--- a/src/pkg/encoding/base64/base64_test.go
+++ b/src/pkg/encoding/base64/base64_test.go
@@ -342,3 +342,11 @@ func TestDecoderIssue7733(t *testing.T) {
t.Errorf("DecodeString = %q; want abcd", s)
}\n}\n+\n+func BenchmarkDecodeString(b *testing.B) {\n+\tdata := StdEncoding.EncodeToString(make([]byte, 8192))\n+\tb.SetBytes(int64(len(data)))\n+\tfor i := 0; i < b.N; i++ {\n+\t\tStdEncoding.DecodeString(data)\n+\t}\n+}\n```
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/afb7b67ae99c5edfb5210ad015be934b96ecc445](https://github.com/golang/go/commit/afb7b67ae99c5edfb5210ad015be934b96ecc445)
## 元コミット内容
このコミットが修正する以前の `encoding/base64` および `encoding/base32` パッケージの `DecodeString` 関数には、非効率な処理が含まれていました。具体的には、入力文字列 `s` をデコードする際に、まず `DecodeString` 関数内で `strings.Map(removeNewlinesMapper, s)` を用いて改行文字(`\n`)を除去していました。その後、この改行文字が除去された文字列を `Decode` 関数に渡していましたが、`Decode` 関数自体も内部で改行文字の除去を試みるロジックを持っていました。
この二重の改行文字除去処理は冗長であり、特に大きな文字列をデコードする際に無駄な処理時間が発生していました。
## 変更の背景
`encoding/base64` および `encoding/base32` パッケージは、それぞれBase64およびBase32エンコーディングを扱うためのGo言語の標準ライブラリです。これらのエンコーディングは、バイナリデータをASCII文字列に変換するために広く使用されており、ネットワーク通信やデータ保存において重要な役割を果たします。
`DecodeString` 関数は、エンコードされた文字列をデコードして元のバイナリデータに戻すための主要なAPIの一つです。この関数のパフォーマンスは、Base64/Base32エンコーディングを多用するアプリケーションにとって非常に重要です。
既存の実装では、`DecodeString` が `Decode` を呼び出す前に改行文字を除去し、さらに `Decode` も改行文字を除去しようとするという、非効率な処理パスが存在していました。これは、`DecodeString` が `Decode` の上位ラッパーとして機能する際に、内部実装の詳細(`Decode` が改行文字を処理すること)を考慮していなかったために生じたものです。
この非効率性を解消し、`DecodeString` のデコード速度を向上させることが、このコミットの主な目的です。コミットメッセージに示されているベンチマーク結果 (`BenchmarkDecodeString`) からもわかるように、この変更によって約1.7倍の速度向上が見込まれ、これは特に大量のBase64/Base32データを処理するシステムにおいて顕著なパフォーマンス改善をもたらします。
## 前提知識の解説
### Base64/Base32エンコーディング
Base64およびBase32は、バイナリデータをASCII文字列形式に変換するためのエンコーディング方式です。これにより、バイナリデータをテキストベースのプロトコル(例: 電子メール、HTTP)で安全に転送したり、テキストファイルに保存したりすることが可能になります。
* **Base64**: 64種類のASCII文字(A-Z, a-z, 0-9, +, /, =)を使用してバイナリデータを表現します。3バイトのバイナリデータを4文字のBase64文字列に変換します。
* **Base32**: 32種類のASCII文字(A-Z, 2-7, =)を使用してバイナリデータを表現します。5バイトのバイナリデータを8文字のBase32文字列に変換します。Base64よりも文字の種類が少ないため、人間が読みやすく、手動での入力ミスが少ないという利点があります。
これらのエンコーディングでは、エンコードされた文字列に改行文字が含まれることがあります。これは、特に長いデータをエンコードする際に、行の長さを制限するために導入されることがあります(例: MIMEのContent-Transfer-Encoding: base64)。デコード時には、これらの改行文字は無視されるか、除去される必要があります。
### Go言語の `encoding` パッケージ
Go言語の標準ライブラリには、`encoding/base64` および `encoding/base32` パッケージが含まれており、これらのエンコーディングとデコーディング機能を提供します。
* `EncodeToString(src []byte) string`: バイトスライスをエンコードして文字列として返します。
* `DecodeString(s string) ([]byte, error)`: エンコードされた文字列をデコードしてバイトスライスとして返します。
* `Decode(dst, src []byte) (n int, err error)`: エンコードされたバイトスライスをデコードして、指定されたバイトスライス `dst` に書き込みます。
### `strings.Map` 関数
`strings.Map` はGo言語の `strings` パッケージに含まれる関数で、文字列の各文字に指定された関数を適用し、その結果として新しい文字列を構築します。このコミットの変更前は、`DecodeString` 関数内で `strings.Map(removeNewlinesMapper, s)` を使用して、入力文字列 `s` から改行文字を除去していました。`removeNewlinesMapper` は、改行文字をスキップする(つまり、新しい文字列に含めない)ためのマッピング関数です。
## 技術的詳細
このコミットの技術的な核心は、`DecodeString` 関数における改行文字除去の冗長性を排除することにあります。
変更前は、`DecodeString` 関数は以下の2段階で改行文字を除去していました。
1. `DecodeString` の冒頭で `s = strings.Map(removeNewlinesMapper, s)` を実行し、入力文字列 `s` からすべての改行文字を事前に除去していました。これにより、改行文字を含まない新しい文字列が生成されます。
2. その後、`DecodeString` は `enc.Decode(dbuf, []byte(s))` を呼び出してデコード処理を行っていました。ここで `Decode` 関数は、その内部ロジックでエンコードされたデータに含まれる改行文字をスキップまたは無視する機能を持っていました。
このプロセスでは、`DecodeString` が既に改行文字を除去しているにもかかわらず、`Decode` が再度改行文字を処理しようとするため、無駄な処理が発生していました。特に、`strings.Map` は新しい文字列を生成するため、メモリ割り当てとコピーのオーバーヘッドも発生します。
このコミットでは、`DecodeString` が `Decode` の代わりに、内部で改行文字の処理も行う `decode` という非公開関数を直接呼び出すように変更されました。`decode` 関数は、`Decode` 関数が内部的に使用しているデコードロジックであり、改行文字のスキップ処理も含まれています。
これにより、`DecodeString` は `strings.Map` による事前の改行文字除去を不要とし、直接 `decode` 関数に元の入力文字列を渡すことができるようになりました。`decode` 関数が一度だけ改行文字を処理することで、処理の冗長性が解消され、パフォーマンスが向上します。
この最適化は、特に大きなBase64/Base32文字列をデコードする際に、CPUサイクルとメモリ帯域幅の節約に繋がり、結果としてデコード処理全体の高速化を実現します。コミットメッセージのベンチマーク結果が示すように、この変更は顕著な速度向上をもたらしました。
## コアとなるコードの変更箇所
このコミットによる主要なコード変更は、`src/pkg/encoding/base32/base32.go` と `src/pkg/encoding/base64/base64.go` の2つのファイルにあります。
### `src/pkg/encoding/base32/base32.go`
```diff
--- a/src/pkg/encoding/base32/base32.go
+++ b/src/pkg/encoding/base32/base32.go
@@ -330,7 +330,7 @@ func (enc *Encoding) Decode(dst, src []byte) (n int, err error) {
func (enc *Encoding) DecodeString(s string) ([]byte, error) {
s = strings.Map(removeNewlinesMapper, s)
dbuf := make([]byte, enc.DecodedLen(len(s)))
- n, err := enc.Decode(dbuf, []byte(s))
+ n, _, err := enc.decode(dbuf, []byte(s))
return dbuf[:n], err
}
src/pkg/encoding/base64/base64.go
--- a/src/pkg/encoding/base64/base64.go
+++ b/src/pkg/encoding/base64/base64.go
@@ -295,7 +295,7 @@ func (enc *Encoding) Decode(dst, src []byte) (n int, err error) {
func (enc *Encoding) DecodeString(s string) ([]byte, error) {
s = strings.Map(removeNewlinesMapper, s)
dbuf := make([]byte, enc.DecodedLen(len(s)))
- n, err := enc.Decode(dbuf, []byte(s))
+ n, _, err := enc.decode(dbuf, []byte(s))
return dbuf[:n], err
}
src/pkg/encoding/base64/base64_test.go
--- a/src/pkg/encoding/base64/base64_test.go
+++ b/src/pkg/encoding/base64/base64_test.go
@@ -342,3 +342,11 @@ func TestDecoderIssue7733(t *testing.T) {
t.Errorf("DecodeString = %q; want abcd", s)
}
}
+
+func BenchmarkDecodeString(b *testing.B) {
+ data := StdEncoding.EncodeToString(make([]byte, 8192))
+ b.SetBytes(int64(len(data)))
+ for i := 0; i < b.N; i++ {
+ StdEncoding.DecodeString(data)
+ }
+}
テストファイルには、BenchmarkDecodeString
という新しいベンチマーク関数が追加されています。これは、DecodeString
関数のパフォーマンスを測定するために使用されます。
コアとなるコードの解説
変更の核心は、DecodeString
関数が enc.Decode
を呼び出す代わりに、非公開の enc.decode
関数を直接呼び出すように変更された点です。
変更前:
func (enc *Encoding) DecodeString(s string) ([]byte, error) {
s = strings.Map(removeNewlinesMapper, s) // 1回目の改行除去
dbuf := make([]byte, enc.DecodedLen(len(s)))
n, err := enc.Decode(dbuf, []byte(s)) // Decode内部で2回目の改行処理
return dbuf[:n], err
}
変更後:
func (enc *Encoding) DecodeString(s string) ([]byte, error) {
s = strings.Map(removeNewlinesMapper, s) // この行は残っているが、実質的に不要になる
dbuf := make([]byte, enc.DecodedLen(len(s)))
n, _, err := enc.decode(dbuf, []byte(s)) // decode関数が改行処理を一度だけ行う
return dbuf[:n], err
}
重要な点:
-
Decode
とdecode
の違い:Decode
(公開関数): ユーザーが直接呼び出すことを意図しており、入力バイトスライスsrc
からエンコードされたデータをデコードし、結果をdst
に書き込みます。この関数は、入力データに含まれる改行文字を適切に処理(スキップ)するロジックを内部に持っています。decode
(非公開関数):Decode
関数が内部的に使用するヘルパー関数です。この関数も改行文字の処理ロジックを含んでいます。
-
最適化のメカニズム:
- 変更前は、
DecodeString
がstrings.Map
で改行文字を事前に除去し、その結果をDecode
に渡していました。しかし、Decode
も内部で改行文字を処理するため、二重処理になっていました。 - 変更後は、
DecodeString
が直接decode
関数を呼び出すようになりました。decode
関数は、Decode
と同様に改行文字を適切に処理する能力を持っています。 - コミットメッセージの
s = strings.Map(removeNewlinesMapper, s)
の行は、diff上は変更されていませんが、decode
関数が改行文字を処理するため、この行の存在は冗長になります。しかし、このコミットの主な目的はDecode
からdecode
への呼び出し変更によるパフォーマンス向上であり、strings.Map
の除去は別のコミットで行われるか、あるいはGoのコンパイラが最適化で除去する可能性があります。このコミットの直接的な効果は、Decode
の呼び出しがdecode
に変わったことによる二重処理の解消です。
- 変更前は、
-
戻り値の変更:
enc.Decode
は(n int, err error)
を返しますが、enc.decode
は(n int, i int, err error)
を返します。i
は読み込んだバイト数を示します。DecodeString
はn
とerr
のみを使用するため、i
は_
で破棄されています。
この変更により、DecodeString
はデコード処理中に改行文字を一度だけ処理するようになり、不要な処理とそれに伴うオーバーヘッドが削減され、結果として BenchmarkDecodeString
で示されたような顕著なパフォーマンス向上が実現されました。
関連リンク
- Go CL 91770051: https://golang.org/cl/91770051
参考にした情報源リンク
- 特になし (コミットメッセージとGo言語の標準ライブラリの知識に基づいています)