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

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

このコミットは、Go言語の標準ライブラリcryptoパッケージ内のハッシュアルゴリズム(MD5, SHA1, SHA256, SHA512)のベンチマークテストを改善するものです。既存のベンチマークに加えて、ハッシュ値の計算(チェックサムの生成)を含む新しいベンチマークを追加し、より実用的なパフォーマンス測定を可能にしています。

コミット

commit 03c52a5d65e06ab881af75a365004bea7ed7359d
Author: Eric Roshan-Eisner <eric.d.eisner@gmail.com>
Date:   Thu Nov 1 16:21:18 2012 -0400

    crypto: use better hash benchmarks
    
    Labels the existing benchmark as stream, and add benchmarks that
    compute the checksum.
    
    R=golang-dev, agl
    CC=golang-dev
    https://golang.org/cl/6814060

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

https://github.com/golang/go/commit/03c52a5d65e06ab881af75a365004bea7ed7359d

元コミット内容

このコミットの元の内容は以下の通りです。

  • コミットメッセージ: crypto: use better hash benchmarks
  • 詳細: 既存のベンチマークを「ストリーム」としてラベル付けし、チェックサムを計算するベンチマークを追加する。
  • レビュー担当者: golang-dev, agl
  • CC: golang-dev
  • 関連するGo CL (Change List) URL: https://golang.org/cl/6814060

変更の背景

Go言語のcryptoパッケージは、暗号学的ハッシュ関数を提供しており、その性能は多くのアプリケーションにとって重要です。既存のベンチマークは、主にデータストリームをハッシュ関数に書き込む(Writeメソッドを呼び出す)処理の速度を測定していました。しかし、実際の使用シナリオでは、データの書き込みだけでなく、最終的なハッシュ値(チェックサム)を計算して取得する(Sumメソッドを呼び出す)処理も含まれます。

このコミットの背景には、ベンチマークが実際の使用パターンをより正確に反映するように改善し、ハッシュ関数の総合的なパフォーマンスを評価できるようにするという意図があります。特に、Sumメソッドのオーバーヘッドがベンチマーク結果に適切に反映されるようにすることで、開発者がより現実的な性能評価を行えるようになります。

前提知識の解説

Go言語のベンチマーク

Go言語には、標準ライブラリtestingパッケージにベンチマーク機能が組み込まれています。

  • go test -bench=.: このコマンドを実行することで、_test.goファイル内に定義されたベンチマーク関数を実行できます。
  • ベンチマーク関数: BenchmarkXxx(*testing.B)というシグネチャを持つ関数です。
  • testing.B: ベンチマーク実行のためのコンテキストを提供します。
    • b.N: ベンチマーク関数が実行されるイテレーション回数。Goのテストフレームワークが自動的に調整し、統計的に有意な結果が得られるようにします。
    • b.SetBytes(n int64): 各イテレーションで処理されるバイト数を指定します。これにより、ベンチマーク結果が「操作あたりの時間」ではなく「バイトあたりの時間」や「秒あたりのバイト数」として報告されるようになります。
    • b.ResetTimer(): タイマーをリセットします。セットアップコードの時間を測定から除外するために使用されます。
    • b.StopTimer(): タイマーを停止します。
    • b.StartTimer(): タイマーを再開します。

暗号学的ハッシュ関数

暗号学的ハッシュ関数は、任意のサイズのデータを入力として受け取り、固定長のハッシュ値(またはメッセージダイジェスト)を出力する一方向関数です。主な特性として以下が挙げられます。

  • 一方向性: ハッシュ値から元のデータを復元することは計算上困難です。
  • 衝突耐性: 異なる入力から同じハッシュ値が生成されること(衝突)は計算上困難です。
  • 雪崩効果: 入力にわずかな変更があっても、ハッシュ値は大きく変化します。

MD5, SHA-1, SHA-256, SHA-512は、それぞれ異なるハッシュアルゴリズムであり、出力されるハッシュ値の長さやセキュリティレベルが異なります。これらはデータの完全性検証、デジタル署名、パスワードの保存などに広く利用されます。

hash.Hashインターフェース

Go言語のcryptoパッケージ内のハッシュ関数は、hash.Hashインターフェースを実装しています。このインターフェースは以下の主要なメソッドを定義しています。

  • Write(p []byte) (n int, err error): データをハッシュ関数に書き込みます。ストリーム処理が可能です。
  • Sum(b []byte) []byte: 現在のハッシュ値を計算し、bに追加して返します。このメソッドを呼び出すと、ハッシュ関数の内部状態はリセットされません。
  • Reset(): ハッシュ関数の内部状態を初期状態にリセットします。これにより、同じハッシュインスタンスを再利用して新しいハッシュ計算を開始できます。
  • Size() int: ハッシュ値のバイト長を返します。
  • BlockSize() int: ハッシュ関数のブロックサイズを返します。

技術的詳細

このコミットの主要な変更点は、ハッシュ関数のベンチマーク方法を改善したことです。

  1. buf変数の初期化方法の変更:

    • 変更前: makeBuf()関数を使用してbuf(8KBのバイトスライス)を初期化していました。この関数は各バイトにbyte(i)を割り当てることで、データが完全にゼロでないことを保証していました。
    • 変更後: var buf = make([]byte, 8192)と直接宣言するようになりました。これにより、bufはゼロ値で初期化されます。ベンチマークの目的上、データの具体的な内容がハッシュ計算のパフォーマンスに大きな影響を与えることは稀であるため、この変更は簡素化と見なせます。
  2. benchmarkSizeヘルパー関数の導入:

    • 変更前: 各ベンチマーク関数(BenchmarkHash1K, BenchmarkHash8Kなど)がそれぞれb.SetBytesbench.Writeのループを直接記述していました。
    • 変更後: func benchmarkSize(b *testing.B, size int)というヘルパー関数が導入されました。この関数は、指定されたsizeのデータをハッシュするベンチマークロジックをカプセル化します。これにより、コードの重複が削減され、可読性と保守性が向上しました。
  3. チェックサム計算の追加:

    • 最も重要な変更点です。変更前のベンチマークはbench.Write(buf[:size])のみを実行し、データの書き込み性能を測定していました。
    • 変更後: benchmarkSize関数内で、各イテレーションの開始時にbench.Reset()を呼び出してハッシュ関数の状態をリセットし、bench.Write(buf[:size])でデータを書き込んだ後、bench.Sum(sum[:0])を呼び出してハッシュ値を計算し、その結果をsumスライスに格納しています。
    • sum := make([]byte, bench.Size())でハッシュ値を格納するためのスライスを事前に確保しています。bench.Sum(sum[:0])は、sumスライスの先頭からハッシュ値を追加するというGoのイディオムです。
  4. 新しいベンチマーク関数の追加:

    • BenchmarkHash8Bytes(b *testing.B)が追加されました。これは、非常に小さいデータサイズ(8バイト)でのハッシュ計算性能を測定します。これにより、ハッシュ関数の初期化や最終化のオーバーヘッドがより顕著に現れる可能性があります。

これらの変更により、ベンチマークは単なるデータストリーミングの速度だけでなく、ハッシュ関数のライフサイクル全体(リセット、書き込み、最終的なハッシュ値の計算)を含む、より包括的な性能測定を行うようになりました。これは、実際のアプリケーションでのハッシュ関数の利用パターンに近いため、「より良いベンチマーク」と言えます。

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

このコミットは、src/pkg/crypto/md5/md5_test.gosrc/pkg/crypto/sha1/sha1_test.gosrc/pkg/crypto/sha256/sha256_test.gosrc/pkg/crypto/sha512/sha512_test.goの4つのファイルに同様の変更を加えています。

以下に、src/pkg/crypto/md5/md5_test.goの変更点を例として示します。他のファイルも同様のパターンで変更されています。

--- a/src/pkg/crypto/md5/md5_test.go
+++ b/src/pkg/crypto/md5/md5_test.go
@@ -80,26 +80,26 @@ func ExampleNew() {
 }
 
 var bench = md5.New()
-var buf = makeBuf()
+var buf = make([]byte, 8192)
 
-func makeBuf() []byte {
-	b := make([]byte, 8<<10)
-	for i := range b {
-		b[i] = byte(i)
+func benchmarkSize(b *testing.B, size int) {
+	b.SetBytes(int64(size))
+	sum := make([]byte, bench.Size())
+	for i := 0; i < b.N; i++ {
+		bench.Reset()
+		bench.Write(buf[:size])
+		bench.Sum(sum[:0])
 	}
-	return b
 }
 
+func BenchmarkHash8Bytes(b *testing.B) {
+	benchmarkSize(b, 8)
+}
+
 func BenchmarkHash1K(b *testing.B) {
-	b.SetBytes(1024)
-	for i := 0; i < b.N; i++ {
-		bench.Write(buf[:1024])
-	}
+	benchmarkSize(b, 1024)
 }
 
 func BenchmarkHash8K(b *testing.B) {
-	b.SetBytes(int64(len(buf)))
-	for i := 0; i < b.N; i++ {
-		bench.Write(buf)
-	}
+	benchmarkSize(b, 8192)
 }

コアとなるコードの解説

変更前

変更前のベンチマークコードは、makeBuf()関数で8KBのバイトスライスbufを作成し、BenchmarkHash1KBenchmarkHash8Kの2つのベンチマーク関数を持っていました。これらの関数は、b.SetBytesで処理バイト数を設定し、ループ内でbench.Writeを呼び出してデータをハッシュ関数に書き込む時間のみを測定していました。Sumメソッドの呼び出しは含まれていませんでした。

var bench = md5.New()
var buf = makeBuf() // 8KBのバッファを初期化

func makeBuf() []byte {
	b := make([]byte, 8<<10) // 8KB
	for i := range b {
		b[i] = byte(i) // 各バイトを初期化
	}
	return b
}

func BenchmarkHash1K(b *testing.B) {
	b.SetBytes(1024) // 1KB
	for i := 0; i < b.N; i++ {
		bench.Write(buf[:1024]) // データの書き込みのみ
	}
}

func BenchmarkHash8K(b *testing.B) {
	b.SetBytes(int64(len(buf))) // 8KB
	for i := 0; i < b.N; i++ {
		bench.Write(buf) // データの書き込みのみ
	}
}

変更後

変更後のコードでは、以下の点が改善されています。

  1. bufの簡素化: var buf = make([]byte, 8192)と直接宣言することで、makeBuf関数が不要になりました。
  2. benchmarkSizeヘルパー関数:
    • この関数は、ベンチマークの共通ロジックを抽象化します。
    • b.SetBytes(int64(size))で、ベンチマークが処理するバイト数を設定します。
    • sum := make([]byte, bench.Size())で、ハッシュ結果を格納するためのスライスを事前に確保します。これにより、Sumメソッドが新しいスライスを割り当てるオーバーヘッドを避けることができます。
    • ループ内で、以下の3つの重要なステップが実行されます。
      • bench.Reset(): 各イテレーションの開始時にハッシュ関数の内部状態をリセットします。これにより、各イテレーションが独立したハッシュ計算として扱われます。
      • bench.Write(buf[:size]): 指定されたサイズのデータをハッシュ関数に書き込みます。
      • bench.Sum(sum[:0]): ハッシュ値を計算し、sumスライスに格納します。この操作は、ハッシュ計算の最終段階であり、実際のアプリケーションで必ず行われるため、ベンチマークに含めることが重要です。
  3. BenchmarkHash8Bytesの追加:
    • benchmarkSize(b, 8)を呼び出すことで、8バイトという非常に小さいデータサイズでのハッシュ計算性能を測定します。これは、ハッシュ関数の初期化や最終化のコストが相対的に大きくなるシナリオを評価するのに役立ちます。
  4. 既存ベンチマークの簡素化:
    • BenchmarkHash1KBenchmarkHash8Kは、それぞれbenchmarkSizeヘルパー関数を呼び出すように変更され、コードがより簡潔になりました。
var bench = md5.New()
var buf = make([]byte, 8192) // 8KBのバッファをゼロ値で初期化

// benchmarkSize は指定されたサイズのデータをハッシュするベンチマークを実行するヘルパー関数
func benchmarkSize(b *testing.B, size int) {
	b.SetBytes(int64(size)) // 各イテレーションで処理するバイト数を設定
	sum := make([]byte, bench.Size()) // ハッシュ結果を格納するスライスを事前に確保
	for i := 0; i < b.N; i++ {
		bench.Reset() // 各イテレーションでハッシュ関数をリセット
		bench.Write(buf[:size]) // データをハッシュ関数に書き込む
		bench.Sum(sum[:0]) // ハッシュ値を計算し、結果をsumスライスに格納
	}
}

func BenchmarkHash8Bytes(b *testing.B) {
	benchmarkSize(b, 8) // 8バイトのハッシュベンチマーク
}

func BenchmarkHash1K(b *testing.B) {
	benchmarkSize(b, 1024) // 1KBのハッシュベンチマーク
}

func BenchmarkHash8K(b *testing.B) {
	benchmarkSize(b, 8192) // 8KBのハッシュベンチマーク
}

この変更により、Goのcryptoパッケージのハッシュベンチマークは、より現実的な使用シナリオを反映し、ハッシュ関数の総合的なパフォーマンスをより正確に測定できるようになりました。

関連リンク

参考にした情報源リンク