Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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 ステートメントで各型を個別に処理していました。これはコードの冗長性を生み、将来的なメンテナンスや機能追加の際に手間がかかる可能性がありました。

この変更の背景には、以下の目的があったと考えられます。

  1. コードの統一性と簡潔性: Read 関数が既に採用していた「高速パスのサイズ計算」ロジックを Write 関数にも適用することで、両関数の実装パターンを統一し、コード全体の簡潔性を高める。
  2. 保守性の向上: 重複するロジックを共通化することで、将来的に新しい型を追加したり、既存の型の処理を変更したりする際の修正箇所を減らし、保守性を向上させる。
  3. リファクタリング: パフォーマンスに影響を与えることなく、コードの構造を改善し、より読みやすく、理解しやすいものにする。

前提知識の解説

encoding/binary パッケージ

encoding/binary パッケージは、Goのプリミティブ型(整数、浮動小数点数など)や構造体を、指定されたバイト順序(エンディアン)でバイト列に変換(エンコード)したり、バイト列からGoの型に変換(デコード)したりするための機能を提供します。ネットワーク通信やファイルI/Oで、異なるシステム間でバイナリデータをやり取りする際に特に重要です。

エンディアン (ByteOrder)

エンディアンとは、複数バイトで構成されるデータをメモリ上にどのように配置するかというバイト順序の規則です。

  • ビッグエンディアン (Big Endian): データの最上位バイト(最も大きな桁)が、最も小さいアドレスに格納されます。人間が数字を読む順序と同じです。
  • リトルエンディアン (Little Endian): データの最下位バイト(最も小さな桁)が、最も小さいアドレスに格納されます。Intel系のCPUなどで広く採用されています。

encoding/binary パッケージでは、binary.BigEndianbinary.LittleEndian というインターフェースが提供されており、これらを使ってバイト順序を指定します。

io.Readerio.Writer インターフェース

Go言語の io パッケージは、I/O操作のための基本的なインターフェースを定義しています。

  • io.Reader: データを読み込むためのインターフェースで、Read([]byte) (n int, err error) メソッドを持ちます。
  • io.Writer: データを書き込むためのインターフェースで、Write([]byte) (n int, err error) メソッドを持ちます。

binary.Readio.Reader からデータを読み込み、binary.Writeio.Writer にデータを書き込みます。

リフレクション (reflect パッケージ)

Go言語の reflect パッケージは、実行時にプログラムの構造を検査・操作するための機能を提供します。これにより、変数の型、値、メソッドなどを動的に調べたり、変更したりすることができます。encoding/binary パッケージでは、ReadWrite 関数が interface{} 型の data 引数を受け取るため、内部でリフレクションを使用して引数の実際の型を判断し、適切な処理を行います。

高速パス (Fast Path)

「高速パス」とは、特定の一般的なケース(この場合は、プリミティブ型やそれらのスライスなど、サイズが固定で予測可能なデータ型)に対して、リフレクションのようなオーバーヘッドの大きい処理をスキップし、より直接的かつ効率的なコードパスを実行する最適化手法です。これにより、頻繁に呼び出される処理のパフォーマンスを向上させることができます。

技術的詳細

このコミットの核となる変更は、Write 関数が Read 関数と同様に、intDataSize 関数(以前は intReadSize)を使ってデータサイズを事前に計算し、そのサイズに基づいてバイトスライスを準備する「高速パス」のロジックを採用した点です。

Read 関数の変更点

  • intReadSize(data)intDataSize(data) に変更されました。これは、関数名がその役割をより正確に反映するようにリネームされたものです。intDataSize は、ReadWrite の両方の高速パスで利用されるデータサイズを計算するようになりました。
  • []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 { ... } という条件で高速パスを試みるようになりました。

  1. intDataSize(data) を呼び出し、data の型が高速パスで処理できる型であれば、そのサイズ n を取得します。
  2. n がゼロでなければ(つまり高速パスが適用可能であれば)、b [8]byte という一時的なバイト配列、または必要に応じて make([]byte, n) で作成したバイトスライス bs を準備します。
  3. その後、data の実際の型に応じて、bs にデータをエンコードします。例えば、*int16 の場合は order.PutUint16(bs, uint16(*v)) を使って bs に2バイトを書き込みます。
  4. 最後に、準備された bsw.Write(bs) を使って io.Writer に書き込みます。

この変更により、Write 関数は Read 関数と対称的な構造になり、コードの重複が大幅に削減されました。

intReadSize から intDataSize への変更

  • 関数名が intReadSize から intDataSize に変更されました。これは、この関数が ReadWrite の両方の高速パスでデータサイズを計算するために使用されることを明確にするためです。
  • 関数のコメントも「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 ファイルにおける主要な変更箇所は以下の通りです。

  1. 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) {
    
  2. 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:]))
    
  3. 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    ```
    
    
  4. 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 関数は、ReadWrite の両方で高速パスを適用できる型とそのサイズを決定する中心的な役割を担うようになりました。

  • ポインタ型(例: *int8)だけでなく、非ポインタのプリミティブ型(例: int8)も高速パスの対象に含めることで、Write 関数が値渡しされたプリミティブ型も効率的に処理できるようになりました。
  • この関数がゼロを返す場合、それはその型が高速パスでは処理できないことを意味し、ReadWrite 関数はリフレクションを使ったより汎用的な(しかし遅い)パスにフォールバックします。

全体として、このコミットは encoding/binary パッケージの内部実装をより洗練させ、ReadWrite 関数のコードベースを統一し、保守性を高めるための重要なリファクタリングです。ベンチマークに影響がないとされていることから、この変更は主にコード品質の向上を目的としたものであることがわかります。

関連リンク

参考にした情報源リンク