[インデックス 17210] ファイルの概要
このコミットは、Go言語の標準ライブラリ encoding/binary
パッケージ内の binary.go
ファイルに対する変更です。encoding/binary
パッケージは、Goのデータ構造とバイトシーケンスの間で変換を行うための機能を提供します。具体的には、数値や構造体をバイト列にエンコード(書き込み)したり、バイト列からデコード(読み込み)したりする際に、エンディアン(バイト順序)を指定して処理を行うことができます。
このコミットの主な目的は、Write
関数が Read
関数と同様の「高速パス(fast path)」の計算ロジックを利用するように改善することです。これにより、コードの重複を排除し、保守性を向上させることが意図されています。コミットメッセージには「ベンチマークへの影響なし」と明記されており、パフォーマンスに悪影響を与えることなくコードの品質を改善するリファクタリングであることが示唆されています。
コミット
commit c18af467fdec00d4369bc9b5a140ff1d043aab2e
Author: Rob Pike <r@golang.org>
Date: Wed Aug 14 07:03:56 2013 +1000
encoding/binary: make Write work like Read
Use the fast path calculation to shorten the code.
No effect on benchmarks.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/12696046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c18af467fdec00d4369bc9b5a140ff1d043aab2e
元コミット内容
このコミットは、encoding/binary
パッケージの Write
関数を Read
関数と同じように動作させることを目的としています。具体的には、「高速パスの計算」を利用してコードを短縮し、簡潔にすることを目指しています。この変更はベンチマークに影響を与えないとされています。
変更の背景
encoding/binary
パッケージの Read
関数と Write
関数は、それぞれバイト列の読み込みと書き込みを担当します。これらの関数は、Goのプリミティブ型(int8
, uint16
など)やそれらのスライス、あるいは構造体を効率的に処理するために、特定の型に対して最適化された「高速パス」を持っていました。
コミット前の Write
関数は、Read
関数とは異なる方法で高速パスを実装しており、多くの case
文を持つ大きな switch
ステートメントで各型を個別に処理していました。これはコードの冗長性を生み、将来的なメンテナンスや機能追加の際に手間がかかる可能性がありました。
この変更の背景には、以下の目的があったと考えられます。
- コードの統一性と簡潔性:
Read
関数が既に採用していた「高速パスのサイズ計算」ロジックをWrite
関数にも適用することで、両関数の実装パターンを統一し、コード全体の簡潔性を高める。 - 保守性の向上: 重複するロジックを共通化することで、将来的に新しい型を追加したり、既存の型の処理を変更したりする際の修正箇所を減らし、保守性を向上させる。
- リファクタリング: パフォーマンスに影響を与えることなく、コードの構造を改善し、より読みやすく、理解しやすいものにする。
前提知識の解説
encoding/binary
パッケージ
encoding/binary
パッケージは、Goのプリミティブ型(整数、浮動小数点数など)や構造体を、指定されたバイト順序(エンディアン)でバイト列に変換(エンコード)したり、バイト列からGoの型に変換(デコード)したりするための機能を提供します。ネットワーク通信やファイルI/Oで、異なるシステム間でバイナリデータをやり取りする際に特に重要です。
エンディアン (ByteOrder
)
エンディアンとは、複数バイトで構成されるデータをメモリ上にどのように配置するかというバイト順序の規則です。
- ビッグエンディアン (Big Endian): データの最上位バイト(最も大きな桁)が、最も小さいアドレスに格納されます。人間が数字を読む順序と同じです。
- リトルエンディアン (Little Endian): データの最下位バイト(最も小さな桁)が、最も小さいアドレスに格納されます。Intel系のCPUなどで広く採用されています。
encoding/binary
パッケージでは、binary.BigEndian
と binary.LittleEndian
というインターフェースが提供されており、これらを使ってバイト順序を指定します。
io.Reader
と io.Writer
インターフェース
Go言語の io
パッケージは、I/O操作のための基本的なインターフェースを定義しています。
io.Reader
: データを読み込むためのインターフェースで、Read([]byte) (n int, err error)
メソッドを持ちます。io.Writer
: データを書き込むためのインターフェースで、Write([]byte) (n int, err error)
メソッドを持ちます。
binary.Read
は io.Reader
からデータを読み込み、binary.Write
は io.Writer
にデータを書き込みます。
リフレクション (reflect
パッケージ)
Go言語の reflect
パッケージは、実行時にプログラムの構造を検査・操作するための機能を提供します。これにより、変数の型、値、メソッドなどを動的に調べたり、変更したりすることができます。encoding/binary
パッケージでは、Read
や Write
関数が interface{}
型の data
引数を受け取るため、内部でリフレクションを使用して引数の実際の型を判断し、適切な処理を行います。
高速パス (Fast Path)
「高速パス」とは、特定の一般的なケース(この場合は、プリミティブ型やそれらのスライスなど、サイズが固定で予測可能なデータ型)に対して、リフレクションのようなオーバーヘッドの大きい処理をスキップし、より直接的かつ効率的なコードパスを実行する最適化手法です。これにより、頻繁に呼び出される処理のパフォーマンスを向上させることができます。
技術的詳細
このコミットの核となる変更は、Write
関数が Read
関数と同様に、intDataSize
関数(以前は intReadSize
)を使ってデータサイズを事前に計算し、そのサイズに基づいてバイトスライスを準備する「高速パス」のロジックを採用した点です。
Read
関数の変更点
intReadSize(data)
がintDataSize(data)
に変更されました。これは、関数名がその役割をより正確に反映するようにリネームされたものです。intDataSize
は、Read
とWrite
の両方の高速パスで利用されるデータサイズを計算するようになりました。[]uint8
のケースで、以前はループでバイトをコピーしていましたが、copy(data, bs)
を使用するように変更されました。copy
関数はGoの組み込み関数であり、スライス間のコピーを効率的に行うため、より簡潔でパフォーマンスの良い実装になります。
Write
関数の変更点
コミットの主要な変更点です。
以前の Write
関数は、data
引数の型に応じて、*int8
, int8
, []int8
, *uint8
, uint8
, []uint8
など、各型に対して個別の case
文を持つ大きな switch
ステートメントで処理を行っていました。このアプローチは冗長であり、Read
関数が既に持っていた効率的な高速パスのロジックとは異なっていました。
変更後、Write
関数は Read
関数と同様に、まず if n := intDataSize(data); n != 0 { ... }
という条件で高速パスを試みるようになりました。
intDataSize(data)
を呼び出し、data
の型が高速パスで処理できる型であれば、そのサイズn
を取得します。n
がゼロでなければ(つまり高速パスが適用可能であれば)、b [8]byte
という一時的なバイト配列、または必要に応じてmake([]byte, n)
で作成したバイトスライスbs
を準備します。- その後、
data
の実際の型に応じて、bs
にデータをエンコードします。例えば、*int16
の場合はorder.PutUint16(bs, uint16(*v))
を使ってbs
に2バイトを書き込みます。 - 最後に、準備された
bs
をw.Write(bs)
を使ってio.Writer
に書き込みます。
この変更により、Write
関数は Read
関数と対称的な構造になり、コードの重複が大幅に削減されました。
intReadSize
から intDataSize
への変更
- 関数名が
intReadSize
からintDataSize
に変更されました。これは、この関数がRead
とWrite
の両方の高速パスでデータサイズを計算するために使用されることを明確にするためです。 - 関数のコメントも「It returns zero if the type cannot be implemented by the fast path in Read or Write.」と更新され、その汎用性が強調されています。
switch
ステートメントのcase
に、ポインタ型だけでなく、非ポインタのプリミティブ型(例:int8
,int16
など)も追加されました。これにより、Write
関数がint8(v)
のような直接的な値を受け取った場合でも高速パスが適用されるようになりました。
コアとなるコードの変更箇所
src/pkg/encoding/binary/binary.go
ファイルにおける主要な変更箇所は以下の通りです。
-
Read
関数内のintReadSize
のリネーム:--- a/src/pkg/encoding/binary/binary.go +++ b/src/pkg/encoding/binary/binary.go @@ -135,7 +135,7 @@ func (bigEndian) GoString() string { return "binary.BigEndian" } // may be used for padding. func Read(r io.Reader, order ByteOrder, data interface{}) error { // Fast path for basic types and slices. - if n := intReadSize(data); n != 0 { + if n := intDataSize(data); n != 0 { var b [8]byte var bs []byte if n > len(b) {
-
Read
関数内の[]uint8
のコピー方法の変更:--- a/src/pkg/encoding/binary/binary.go +++ b/src/pkg/encoding/binary/binary.go @@ -164,13 +164,11 @@ func Read(r io.Reader, order ByteOrder, data interface{}) error { case *uint64: *data = order.Uint64(bs) case []int8: - for i, x := range bs { // Easier to loop over the input for 8-bit cases. + for i, x := range bs { // Easier to loop over the input for 8-bit values. data[i] = int8(x) } case []uint8: - for i, x := range bs { // Easier to loop over the input for 8-bit cases. - data[i] = x - } + copy(data, bs) case []int16: for i := range data { data[i] = int16(order.Uint16(bs[2*i:]))
-
Write
関数の大幅なリファクタリング: 以前の冗長なswitch
ステートメントが削除され、Read
関数と同様の高速パスロジックが導入されました。--- a/src/pkg/encoding/binary/binary.go +++ b/src/pkg/encoding/binary/binary.go @@ -229,97 +227,95 @@ func Read(r io.Reader, order ByteOrder, data interface{}) error { // When writing structs, zero values are written for fields // with blank (_) field names. func Write(w io.Writer, order ByteOrder, data interface{}) error { - // Fast path for basic types. - var b [8]byte - var bs []byte - switch v := data.(type) { - case *int8: - bs = b[:1] - b[0] = byte(*v) - case int8: - bs = b[:1] - b[0] = byte(v) - case []int8: - bs = make([]byte, len(v)) - for i, x := range v { - bs[i] = byte(x) - } - case *uint8: - bs = b[:1] - b[0] = *v - case uint8: - bs = b[:1] - b[0] = byte(v) - case []uint8: - bs = v - case *int16: - bs = b[:2] - order.PutUint16(bs, uint16(*v)) - case int16: - bs = b[:2] - order.PutUint16(bs, uint16(v)) - case []int16: - bs = make([]byte, 2*len(v)) - for i, x := range v { - order.PutUint16(bs[2*i:], uint16(x)) - } - case *uint16: - bs = b[:2] - order.PutUint16(bs, *v) - case uint16: - bs = b[:2] - order.PutUint16(bs, v) - case []uint16: - bs = make([]byte, 2*len(v)) - for i, x := range v { - order.PutUint16(bs[2*i:], x) - } - case *int32: - bs = b[:4] - order.PutUint32(bs, uint32(*v)) - case int32: - bs = b[:4] - order.PutUint32(bs, uint32(v)) - case []int32: - bs = make([]byte, 4*len(v)) - for i, x := range v { - order.PutUint32(bs[4*i:], uint32(x)) - }\n-\tcase *uint32:\n-\t\tbs = b[:4]\n-\t\torder.PutUint32(bs, *v)\n-\tcase uint32:\n-\t\tbs = b[:4]\n-\t\torder.PutUint32(bs, v)\n-\tcase []uint32:\n-\t\tbs = make([]byte, 4*len(v))\n-\t\tfor i, x := range v {\n-\t\t\torder.PutUint32(bs[4*i:], x)\n-\t\t}\n-\tcase *int64:\n-\t\tbs = b[:8]\n-\t\torder.PutUint64(bs, uint64(*v))\n-\tcase int64:\n-\t\tbs = b[:8]\n-\t\torder.PutUint64(bs, uint64(v))\n-\tcase []int64:\n-\t\tbs = make([]byte, 8*len(v))\n-\t\tfor i, x := range v {\n-\t\t\torder.PutUint64(bs[8*i:], uint64(x))\n-\t\t}\n-\tcase *uint64:\n-\t\tbs = b[:8]\n-\t\torder.PutUint64(bs, *v)\n-\tcase uint64:\n-\t\tbs = b[:8]\n-\t\torder.PutUint64(bs, v)\n-\tcase []uint64:\n-\t\tbs = make([]byte, 8*len(v))\n-\t\tfor i, x := range v {\n-\t\t\torder.PutUint64(bs[8*i:], x)\n-\t\t}\n-\t}\n-\tif bs != nil {\n+\t// Fast path for basic types and slices.\n+\tif n := intDataSize(data); n != 0 {\n+\t\tvar b [8]byte\n+\t\tvar bs []byte\n+\t\tif n > len(b) {\n+\t\t\tbs = make([]byte, n)\n+\t\t} else {\n+\t\t\tbs = b[:n]\n+\t\t}\n+\t\tswitch v := data.(type) {\n+\t\tcase *int8:\n+\t\t\tbs = b[:1]\n+\t\t\tb[0] = byte(*v)\n+\t\tcase int8:\n+\t\t\tbs = b[:1]\n+\t\t\tb[0] = byte(v)\n+\t\tcase []int8:\n+\t\t\tfor i, x := range v {\n+\t\t\t\tbs[i] = byte(x)\n+\t\t\t}\n+\t\tcase *uint8:\n+\t\t\tbs = b[:1]\n+\t\t\tb[0] = *v\n+\t\tcase uint8:\n+\t\t\tbs = b[:1]\n+\t\t\tb[0] = byte(v)\n+\t\tcase []uint8:\n+\t\t\tbs = v\n+\t\tcase *int16:\n+\t\t\tbs = b[:2]\n+\t\t\torder.PutUint16(bs, uint16(*v))\n+\t\tcase int16:\n+\t\t\tbs = b[:2]\n+\t\t\torder.PutUint16(bs, uint16(v))\n+\t\tcase []int16:\n+\t\t\tfor i, x := range v {\n+\t\t\t\torder.PutUint16(bs[2*i:], uint16(x))\n+\t\t\t}\n+\t\tcase *uint16:\n+\t\t\tbs = b[:2]\n+\t\t\torder.PutUint16(bs, *v)\n+\t\tcase uint16:\n+\t\t\tbs = b[:2]\n+\t\t\torder.PutUint16(bs, v)\n+\t\tcase []uint16:\n+\t\t\tfor i, x := range v {\n+\t\t\t\torder.PutUint16(bs[2*i:], x)\n+\t\t\t}\n+\t\tcase *int32:\n+\t\t\tbs = b[:4]\n+\t\t\torder.PutUint32(bs, uint32(*v))\n+\t\tcase int32:\n+\t\t\tbs = b[:4]\n+\t\t\torder.PutUint32(bs, uint32(v))\n+\t\tcase []int32:\n+\t\t\tfor i, x := range v {\n+\t\t\t\torder.PutUint32(bs[4*i:], uint32(x))\n+\t\t\t}\n+\t\tcase *uint32:\n+\t\t\tbs = b[:4]\n+\t\t\torder.PutUint32(bs, *v)\n+\t\tcase uint32:\n+\t\t\tbs = b[:4]\n+\t\t\torder.PutUint32(bs, v)\n+\t\tcase []uint32:\n+\t\t\tfor i, x := range v {\n+\t\t\t\torder.PutUint32(bs[4*i:], x)\n+\t\t\t}\n+\t\tcase *int64:\n+\t\t\tbs = b[:8]\n+\t\t\torder.PutUint64(bs, uint64(*v))\n+\t\tcase int64:\n+\t\t\tbs = b[:8]\n+\t\t\torder.PutUint64(bs, uint64(v))\n+\t\tcase []int64:\n+\t\t\tfor i, x := range v {\n+\t\t\t\torder.PutUint64(bs[8*i:], uint64(x))\n+\t\t\t}\n+\t\tcase *uint64:\n+\t\t\tbs = b[:8]\n+\t\t\torder.PutUint64(bs, *v)\n+\t\tcase uint64:\n+\t\t\tbs = b[:8]\n+\t\t\torder.PutUint64(bs, v)\n+\t\tcase []uint64:\n+\t\t\tfor i, x := range v {\n+\t\t\t\torder.PutUint64(bs[8*i:], x)\n+\t\t\t}\n+\t\t}\n \t\t_, err := w.Write(bs)\n \t\treturn err\n \t}\n ```
-
intReadSize
関数のリネームと機能拡張:--- a/src/pkg/encoding/binary/binary.go +++ b/src/pkg/encoding/binary/binary.go @@ -609,29 +605,29 @@ func (e *encoder) skip(v reflect.Value) { e.buf = e.buf[n:] } -// intReadSize returns the size of the data required to represent the data when encoded.\n-// It returns zero if the type cannot be implemented by the fast path in Read.\n-func intReadSize(data interface{}) int {\n+// intDataSize returns the size of the data required to represent the data when encoded.\n+// It returns zero if the type cannot be implemented by the fast path in Read or Write.\n+func intDataSize(data interface{}) int {\n switch data := data.(type) {\n - case *int8, *uint8:\n + case int8, *int8, *uint8:\n return 1 case []int8: return len(data) case []uint8: return len(data) - case *int16, *uint16:\n + case int16, *int16, *uint16:\n return 2 case []int16: return 2 * len(data) case []uint16: return 2 * len(data) - case *int32, *uint32:\n + case int32, *int32, *uint32:\n return 4 case []int32: return 4 * len(data) case []uint32: return 4 * len(data) - case *int64, *uint64:\n + case int64, *int64, *uint64:\n return 8 case []int64: return 8 * len(data)
コアとなるコードの解説
Read
関数と intDataSize
の連携
Read
関数は、intDataSize(data)
を呼び出すことで、読み込むデータのサイズを事前に決定します。このサイズがゼロでない場合(つまり、高速パスが適用可能な場合)、Read
関数はリフレクションを使用せずに、直接バイトスライス bs
を操作してデータをデコードします。
[]uint8
のケースで copy(data, bs)
を使用するようになったのは、Goの copy
関数がバイトスライスのコピーにおいて非常に効率的であるためです。以前のループ処理よりも、より最適化された方法でデータをコピーできます。
Write
関数の高速パス導入
このコミットの最も重要な変更は、Write
関数に Read
関数と同様の高速パスロジックが導入されたことです。
変更前は、Write
関数は引数 data
の型を一つずつ switch
で判定し、それぞれの型に応じたバイト列への変換ロジックを個別に記述していました。例えば、int16
の場合は order.PutUint16(bs, uint16(v))
を呼び出し、[]int16
の場合はループ内で order.PutUint16
を呼び出す、といった具合です。この方法は、型が増えるたびに switch
文が肥大化し、コードの重複も多くなっていました。
変更後、Write
関数はまず if n := intDataSize(data); n != 0 { ... }
という条件で、data
が高速パスで処理できる型であるかを判定します。
- もし高速パスが適用可能であれば、
intDataSize
が返すサイズn
に基づいて、適切なサイズのバイトスライスbs
を準備します。 - その後、内部の
switch
ステートメントでdata
の具体的な型を再度判定し、bs
にデータをエンコードします。この内部switch
は、以前のWrite
関数のswitch
と似ていますが、bs
の準備が共通化されたため、各case
のロジックがより簡潔になりました。
この構造により、Write
関数は Read
関数と非常に似たパターンで動作するようになり、コードの対称性が向上しました。これにより、コードの理解が容易になり、将来的な機能拡張やバグ修正がしやすくなります。
intDataSize
の役割の拡張
intDataSize
関数は、Read
と Write
の両方で高速パスを適用できる型とそのサイズを決定する中心的な役割を担うようになりました。
- ポインタ型(例:
*int8
)だけでなく、非ポインタのプリミティブ型(例:int8
)も高速パスの対象に含めることで、Write
関数が値渡しされたプリミティブ型も効率的に処理できるようになりました。 - この関数がゼロを返す場合、それはその型が高速パスでは処理できないことを意味し、
Read
やWrite
関数はリフレクションを使ったより汎用的な(しかし遅い)パスにフォールバックします。
全体として、このコミットは encoding/binary
パッケージの内部実装をより洗練させ、Read
と Write
関数のコードベースを統一し、保守性を高めるための重要なリファクタリングです。ベンチマークに影響がないとされていることから、この変更は主にコード品質の向上を目的としたものであることがわかります。
関連リンク
- Go CL (Code Review) リンク: https://golang.org/cl/12696046
参考にした情報源リンク
- Go言語
encoding/binary
パッケージ公式ドキュメント: https://pkg.go.dev/encoding/binary - Go言語
io
パッケージ公式ドキュメント: https://pkg.go.dev/io - Go言語
reflect
パッケージ公式ドキュメント: https://pkg.go.dev/reflect - Go言語の
copy
関数に関する情報: https://go.dev/ref/spec#Copy_and_append - エンディアンに関する一般的な情報 (例: Wikipedia): https://ja.wikipedia.org/wiki/%E3%82%A8%E3%83%B3%E3%83%87%E3%82%A3%E3%82%A2%E3%83%B3