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

[インデックス 10568] ファイルの概要

このコミットは、Go言語の標準ライブラリにおけるハッシュインターフェース hash.HashSum メソッドのシグネチャを変更し、メモリ割り当てを削減することを目的としています。具体的には、Sum() メソッドが Sum(in []byte) []byte となり、ハッシュ値を既存のバイトスライスに追加する形式に変更されました。これにより、ハッシュ計算結果を格納するための新しいバイトスライスの不要な割り当てを避けることが可能になります。

コミット

commit bac7bc55a6a8776f45144452a7236e34cdb09de6
Author: Adam Langley <agl@golang.org>
Date:   Thu Dec 1 12:35:37 2011 -0500

    Add a []byte argument to hash.Hash to allow an allocation to be saved.
    
    This is the result of running `gofix -r hashsum` over the tree, changing
    the hash function implementations by hand and then fixing a couple of
    instances where gofix didn't catch something.
    
    The changed implementations are as simple as possible while still
    working: I'm not trying to optimise in this CL.
    
    R=rsc, cw, rogpeppe
    CC=golang-dev
    https://golang.org/cl/5448065

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/bac7bc55a6a8776f45144452a7236e34cdb09de6

元コミット内容

hash.Hash インターフェースの Sum() メソッドに []byte 引数を追加し、メモリ割り当てを節約できるようにする。

これは、ツリー全体に対して gofix -r hashsum を実行し、ハッシュ関数の実装を手動で変更し、gofix が捕捉できなかったいくつかのインスタンスを修正した結果である。

変更された実装は、動作し続ける限り可能な限りシンプルである。この変更リストでは最適化を試みていない。

変更の背景

Go言語のハッシュ計算において、hash.Hash インターフェースの Sum() メソッドが呼び出されるたびに、ハッシュ結果を格納するための新しいバイトスライスが割り当てられていました。特に、ハッシュ計算が頻繁に行われるようなシナリオ(例: ネットワークプロトコル、暗号処理、データ構造のチェックサム計算など)では、この頻繁なメモリ割り当てがパフォーマンスのボトルネックとなる可能性がありました。

このコミットの主な目的は、この不要なメモリ割り当てを削減し、Goプログラムの効率を向上させることです。Sum メソッドに []byte 引数を追加することで、呼び出し元が既存のバイトスライスを渡せるようになり、そのスライスにハッシュ結果を追記する形にすることで、新しいスライスの割り当てを回避できるようになります。これは、特に大きなデータセットやストリーム処理において、メモリ使用量とガベージコレクションの負荷を軽減する効果が期待されます。

また、この変更は gofix ツール (-r hashsum オプション) を用いて既存のコードベースに適用されましたが、gofix が自動的に修正できないケースについては手動での修正も行われています。これは、Go言語の進化において、後方互換性を保ちつつ、より効率的なAPI設計へと移行するプロセスの一環と言えます。

前提知識の解説

1. Go言語の hash.Hash インターフェース

Go言語の hash パッケージは、暗号学的ハッシュ関数(MD5, SHA-1, SHA-256など)や非暗号学的ハッシュ関数(CRC32, Adler-32など)を統一的に扱うためのインターフェースを提供します。その中心となるのが hash.Hash インターフェースです。

hash.Hash インターフェースは、以下の主要なメソッドを定義しています(変更前の状態):

  • Write(p []byte) (n int, err error): ハッシュ計算の対象となるデータを追加します。io.Writer インターフェースを実装しています。
  • Sum() []byte: 現在のハッシュ値を計算し、その結果を新しいバイトスライスとして返します。
  • Reset(): ハッシュの状態を初期値に戻します。
  • Size() int: ハッシュ値のバイト長を返します。
  • BlockSize() int: ハッシュ関数のブロックサイズを返します。

このコミット以前は、Sum() メソッドが常に新しい []byte スライスを割り当ててハッシュ結果を返していました。

2. メモリ割り当てとガベージコレクション (GC)

Go言語はガベージコレクタ (GC) を備えており、開発者が手動でメモリを解放する必要はありません。しかし、プログラムが頻繁に小さなオブジェクトを割り当てたり解放したりすると、GCが頻繁に実行され、プログラムの実行が一時停止する「GCストップ・ザ・ワールド」が発生し、パフォーマンスに影響を与える可能性があります。

特に、ループ内で Sum() のようなメソッドが繰り返し呼び出され、そのたびに新しいバイトスライスが割り当てられると、大量の短期的なオブジェクトが生成され、GCの負荷が増大します。メモリ割り当てを削減することは、GCの頻度と時間を減らし、アプリケーションのスループットとレイテンシを改善する上で非常に重要です。

3. gofix ツール

gofix は、Go言語の古いAPIや慣用句を新しいものに自動的に更新するためのコマンドラインツールです。Go言語の進化に伴い、APIの変更や改善が行われることがありますが、gofix は既存のコードベースを新しいAPIに適合させる作業を支援します。

このコミットでは、gofix -r hashsum が使用されました。これは、hash.Hash インターフェースの Sum() メソッドの変更に関連するコードパターンを自動的に検出し、修正を試みるための特定のルール (hashsum) を適用することを意味します。しかし、コミットメッセージにあるように、gofix がすべてを自動的に修正できるわけではなく、手動での調整が必要な場合もあります。

4. append 関数とバイトスライス操作

Go言語の組み込み関数 append は、スライスに要素を追加するために使用されます。append は、元のスライスの容量が不足している場合、より大きな新しいスライスを割り当ててデータをコピーし、その新しいスライスを返します。容量が十分な場合は、既存のバッキング配列に直接要素を追加し、新しいスライスヘッダを返します。

Sum(in []byte) []byte のようなシグネチャは、この append 関数の挙動をハッシュ計算結果の返却に活用することを意図しています。呼び出し元は、ハッシュ結果を追記したい既存のバイトスライスを in 引数として渡すことができます。これにより、ハッシュ実装側で新しいスライスを make する必要がなくなり、メモリ割り当てを削減できます。

技術的詳細

このコミットの核心は、Go言語の hash.Hash インターフェースの Sum メソッドのシグネチャ変更です。

変更前:

type Hash interface {
    // ...
    Sum() []byte
    // ...
}

Sum() メソッドは、ハッシュ計算の結果を格納する新しい []byte スライスを常に返していました。これは、ハッシュ値を頻繁に取得するようなシナリオでは、不要なメモリ割り当てとそれに伴うガベージコレクションのオーバーヘッドを引き起こす可能性がありました。

変更後:

type Hash interface {
    // ...
    // Sum appends the current hash in the same manner as append(), without
    // changing the underlying hash state.
    Sum(in []byte) []byte
    // ...
}

新しい Sum(in []byte) []byte シグネチャでは、in という []byte 型の引数が追加されました。この in スライスは、ハッシュ計算の結果を追記するためのバッファとして機能します。Sum メソッドは、in スライスの末尾に現在のハッシュ値を追記し、その結果のスライスを返します。

この変更により、以下のような利点が得られます。

  1. メモリ割り当ての削減: 呼び出し元がハッシュ結果を格納するための十分な容量を持つ既存のバイトスライスを in 引数として渡すことで、Sum メソッド内で新しいスライスを make する必要がなくなります。これにより、特にハッシュ計算がループ内で頻繁に行われる場合に、大量の短期的なオブジェクトの生成を抑制し、ガベージコレクションの負荷を軽減できます。
  2. 柔軟性の向上: 呼び出し元は、ハッシュ結果を既存のデータ構造の一部として直接利用できるようになります。例えば、ネットワークパケットの構築中にハッシュ値を計算し、そのパケットのバイトスライスに直接追記するといった操作がより効率的に行えます。
  3. 慣用的なGoコード: このパターンは、Go言語の append 関数の挙動と一致しており、Goの標準ライブラリでよく見られる効率的なスライス操作の慣用句に沿っています。

この変更に伴い、Go標準ライブラリ内のMD4, MD5, RIPEMD160, SHA-1, SHA-256, SHA-512, Adler-32, CRC32, CRC64, FNVなどの様々なハッシュ関数の実装が、新しい Sum(in []byte) []byte シグネチャに適合するように更新されました。これらの実装では、ハッシュ結果を in スライスに append するロジックが導入されています。

また、crypto/hmac, crypto/tls, crypto/x509, crypto/rsa, crypto/ecdsa, archive/tar, exp/ssh, websocket など、hash.Hash インターフェースを利用していた多数のパッケージ内の呼び出し箇所も、h.Sum() から h.Sum(nil) へと変更されました。h.Sum(nil) は、新しいスライスが必要な場合に nil スライスを渡すことで、Sum メソッドが内部で新しいスライスを割り当てて返すようにする、新しい慣用句です。これにより、既存のコードの動作を維持しつつ、必要に応じてメモリ割り当てを最適化する余地が生まれます。

コアとなるコードの変更箇所

このコミットにおけるコアとなるコードの変更は、主に以下の2つのパターンに集約されます。

  1. hash.Hash インターフェースの Sum メソッドのシグネチャ変更: src/pkg/hash/hash.go ファイルにおいて、Hash インターフェースの定義が変更されました。

    --- a/src/pkg/hash/hash.go
    +++ b/src/pkg/hash/hash.go
    @@ -13,9 +13,9 @@ type Hash interface {
     	// It never returns an error.
     	io.Writer
     
    -	// Sum returns the current hash, without changing the
    -	// underlying hash state.
    -	Sum() []byte
    +	// Sum appends the current hash in the same manner as append(), without
    +	// changing the underlying hash state.
    +	Sum(in []byte) []byte
     
     	// Reset resets the hash to one with zero bytes written.
     	Reset()
    
  2. 各ハッシュ関数の Sum メソッドの実装変更: src/pkg/crypto/md4/md4.go, src/pkg/crypto/md5/md5.go, src/pkg/crypto/sha1/sha1.go, src/pkg/crypto/sha256/sha256.go, src/pkg/crypto/sha512/sha512.go, src/pkg/crypto/ripemd160/ripemd160.go, src/pkg/hash/adler32/adler32.go, src/pkg/hash/crc32/crc32.go, src/pkg/hash/crc64/crc64.go, src/pkg/hash/fnv/fnv.go など、具体的なハッシュアルゴリズムの実装において、Sum() メソッドが Sum(in []byte) []byte に変更され、ハッシュ結果を in スライスに追記するロジックが導入されました。

    例: src/pkg/crypto/md5/md5.go

    --- a/src/pkg/crypto/md5/md5.go
    +++ b/src/pkg/crypto/md5/md5.go
    @@ -77,7 +77,7 @@ func (d *digest) Write(p []byte) (nn int, err error) {
     	return
     }
     
    -func (d0 *digest) Sum() []byte {
    +func (d0 *digest) Sum(in []byte) []byte {
     	// Make a copy of d0 so that caller can keep writing and summing.
     	d := new(digest)
     	*d = *d0
    @@ -103,14 +103,11 @@ func (d0 *digest) Sum() []byte {
     		panic("d.nx != 0")
     	}
     
    -	p := make([]byte, 16)
    -	j := 0
     	for _, s := range d.s {
    -		p[j+0] = byte(s >> 0)
    -		p[j+1] = byte(s >> 8)
    -		p[j+2] = byte(s >> 16)
    -		p[j+3] = byte(s >> 24)
    -		j += 4
    +		in = append(in, byte(s>>0))
    +		in = append(in, byte(s>>8))
    +		in = append(in, byte(s>>16))
    +		in = append(in, byte(s>>24))
     	}
    -	return p
    +	return in
     }
    
  3. hash.Hash インターフェースを利用する箇所の呼び出し変更: src/cmd/cgo/main.go, src/pkg/archive/tar/reader_test.go, src/pkg/crypto/ecdsa/ecdsa_test.go, src/pkg/crypto/hmac/hmac.go, src/pkg/crypto/ocsp/ocsp.go, src/pkg/crypto/openpgp/..., src/pkg/crypto/rsa/..., src/pkg/crypto/tls/..., src/pkg/exp/ssh/..., src/pkg/io/multi_test.go, src/pkg/patch/git.go, src/pkg/websocket/..., test/fixedbugs/bug257.go など、Sum() メソッドを呼び出していた多数のファイルで、h.Sum()h.Sum(nil) に変更されました。

    例: src/cmd/cgo/main.go

    --- a/src/cmd/cgo/main.go
    +++ b/src/cmd/cgo/main.go
    @@ -189,7 +189,7 @@ func main() {
     		io.Copy(h, f)
     		f.Close()
     	}
    -	cPrefix = fmt.Sprintf("_%x", h.Sum()[0:6])
    +	cPrefix = fmt.Sprintf("_%x", h.Sum(nil)[0:6])
     
     	fs := make([]*File, len(goFiles))
     	for i, input := range goFiles {
    

コアとなるコードの解説

hash.Hash インターフェースの変更 (src/pkg/hash/hash.go)

この変更は、Go言語のハッシュインターフェースのセマンティクスを根本的に変更するものです。 変更前は Sum() が新しいスライスを返すことで、呼び出し元は常にハッシュ結果のコピーを受け取っていました。これはシンプルですが、頻繁な呼び出しではメモリ割り当てのオーバーヘッドが大きくなります。

変更後の Sum(in []byte) []byte は、Goの append 関数のパターンを踏襲しています。

  • in 引数は、ハッシュ結果を追記するための既存のバイトスライスです。
  • Sum メソッドは、in スライスの末尾にハッシュ結果を追記し、その結果のスライスを返します。

この設計により、呼び出し元は以下の2つの方法で Sum を利用できます。

  1. 新しいスライスが必要な場合: h.Sum(nil) のように nil を渡します。この場合、Sum メソッドの実装は内部で新しいスライスを割り当ててハッシュ結果を格納し、それを返します。これは変更前の Sum() と同等の動作を提供します。

  2. 既存のスライスに追記したい場合: buf := make([]byte, 0, h.Size()) のように事前に容量を確保したスライスを作成し、h.Sum(buf) のように渡します。これにより、Sum メソッドは buf のバッキング配列に直接ハッシュ結果を書き込み、新しいスライスの割り当てを回避できます。これは、特にハッシュ結果を他のデータと連結する場合や、ハッシュ計算を繰り返し行う場合に非常に効率的です。

各ハッシュ関数の Sum メソッドの実装変更

各ハッシュアルゴリズムの実装(例: MD5, SHA-1など)では、Sum メソッドの内部ロジックが変更されました。 変更前は、make([]byte, Size()) のように新しいスライスを作成し、そこにハッシュ値を書き込んでいました。

変更後は、in 引数にハッシュ値を append する形に変わりました。 例えば、MD5の Sum メソッドでは、ハッシュ結果の4つの uint32 値 (d.s) をバイトスライスに変換し、それを in スライスに追記しています。

// 変更前 (概念)
func (d0 *digest) Sum() []byte {
    p := make([]byte, 16) // 新しいスライスを割り当て
    // d.s の値を p に書き込む
    return p
}

// 変更後 (概念)
func (d0 *digest) Sum(in []byte) []byte {
    // d.s の値を in に append する
    in = append(in, byte(s>>0), byte(s>>8), byte(s>>16), byte(s>>24)) // 各 uint32 を4バイトに変換して追記
    return in
}

この変更により、ハッシュ計算結果のバイト表現を生成する際に、中間的なスライス割り当てが不要になる場合があります。

hash.Hash インターフェースを利用する箇所の呼び出し変更

既存のコードベースで h.Sum() を呼び出していた箇所は、すべて h.Sum(nil) に変更されました。 これは、前述の通り、新しいスライスが必要な場合に nil を渡すという新しい慣用句に従ったものです。これにより、既存のコードの動作は維持されつつ、将来的にパフォーマンスが重要となる箇所では、呼び出し元が適切なバッファを渡すことで最適化が可能になります。

このコミットは、Go言語の標準ライブラリがメモリ効率とパフォーマンスを継続的に改善していることを示す良い例です。APIの変更は広範囲に及びましたが、gofix ツールと手動の修正を組み合わせることで、スムーズな移行が図られました。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコード (特に hash パッケージと crypto パッケージ)
  • Go言語のメモリ管理とガベージコレクションに関する一般的な情報源
  • gofix ツールに関するGoブログ記事やドキュメント
  • Go言語の append 関数の挙動に関する解説記事