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

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

このコミットは、Go言語の crypto/md5 パッケージにおけるMD5ハッシュ計算のパフォーマンス改善を目的としています。特に、入力データがメモリ上で適切にアラインされている場合に、uint32 型のロードを安全に利用することで書き込み処理を高速化し、さらにアラインされていない書き込みに対するテストとベンチマークを追加しています。これにより、MD5ハッシュ計算の効率が向上し、特に大きなデータブロックの処理において顕著な速度向上が見られます。

コミット

  • コミットハッシュ: 38458ce3fea94ed9c89eb2dc78be9bc79b2b0c66
  • 作者: Shenghou Ma minux.ma@gmail.com
  • コミット日時: 2012年11月18日 (日) 02:23:34 +0800

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

https://github.com/golang/go/commit/38458ce3fea94ed9c89eb2dc78be9bc79b2b0c66

元コミット内容

crypto/md5: speed up aligned writes and test/bench unaligned writes
Write() can safely use uint32 loads when input is aligned.
Also add test and benchmarks for unaligned writes.

Benchmark result obtained by Dave Cheney on ARMv5TE @ 1.2GHz:
benchmark                       old ns/op    new ns/op    delta
BenchmarkHash8Bytes                  4104         3417  -16.74%
BenchmarkHash1K                     22061        11208  -49.20%
BenchmarkHash8K                    146630        65148  -55.57%
BenchmarkHash8BytesUnaligned         4128         3436  -16.76%
BenchmarkHash1KUnaligned            22054        21473   -2.63%
BenchmarkHash8KUnaligned           146658       146909   +0.17%

benchmark                        old MB/s     new MB/s  speedup
BenchmarkHash8Bytes                  1.95         2.34    1.20x
BenchmarkHash1K                     46.42        91.36    1.97x
BenchmarkHash8K                     55.87       125.74    2.25x
BenchmarkHash8BytesUnaligned         1.94         2.33    1.20x
BenchmarkHash1KUnaligned            46.43        47.69    1.03x
BenchmarkHash8KUnaligned            55.76        55.76    1.00x

R=golang-dev, dave, bradfitz
CC=golang-dev
https://golang.org/cl/6782072

変更の背景

MD5ハッシュ計算は、データの整合性チェックやデジタル署名など、様々な場面で利用される重要な暗号学的ハッシュ関数です。Go言語の crypto/md5 パッケージは、これらの機能を提供しますが、パフォーマンスは常に重要な考慮事項です。特に、大量のデータを処理する場合、ハッシュ計算の効率がアプリケーション全体のパフォーマンスに大きく影響します。

このコミットの背景には、MD5ハッシュ計算の内部処理において、入力データのメモリ配置(アラインメント)がパフォーマンスに与える影響を最適化するという目的があります。多くのCPUアーキテクチャでは、特定のデータ型(例えば uint32uint64)をメモリから読み書きする際に、そのアドレスがデータ型のサイズ(またはその倍数)にアラインされていると、より高速に処理できます。アラインされていないアクセスは、パフォーマンスの低下や、場合によってはハードウェア例外を引き起こす可能性があります。

このコミット以前の crypto/md5 パッケージでは、入力データのアラインメントを考慮せずにバイト単位で処理を行っていた可能性があります。これにより、アラインメントの恩恵を受けられず、特に uint32 のようなワード単位での高速なロードが可能な状況でも、その最適化が適用されていませんでした。

また、アラインされていないデータに対する処理の堅牢性とパフォーマンスも重要です。現実世界のデータは必ずしも理想的なアラインメントで提供されるわけではないため、アラインされていない入力に対しても正しく、かつ合理的なパフォーマンスで動作することが求められます。

このコミットは、これらの課題に対処し、MD5ハッシュ計算の全体的なパフォーマンスを向上させるとともに、アラインメントに関する挙動をより明確にし、テストカバレッジを拡大することを目的としています。

前提知識の解説

MD5 (Message-Digest Algorithm 5)

MD5は、128ビット(16バイト)のハッシュ値を生成する暗号学的ハッシュ関数です。入力されたデータ(メッセージ)から、そのデータを代表する固定長の短い値を生成します。このハッシュ値は、データの改ざん検出やデジタル署名などに利用されます。MD5は現在、衝突耐性(異なる入力から同じハッシュ値が生成される可能性)の脆弱性が指摘されており、セキュリティが要求される場面ではSHA-256などのより強力なハッシュ関数が推奨されていますが、データの整合性チェックなど、一部の用途では依然として利用されています。

MD5の内部処理では、入力データを512ビット(64バイト)のブロックに分割し、各ブロックに対して一連のビット演算と加算を繰り返してハッシュ値を更新していきます。このブロック処理の効率が、MD5計算全体のパフォーマンスに直結します。

データアラインメント (Data Alignment)

データアラインメントとは、コンピュータのメモリ上でデータが配置される際のアドレスの制約のことです。多くのCPUアーキテクチャでは、特定のデータ型(例: 4バイトの intuint32)は、そのデータ型のサイズ(この場合は4バイト)の倍数となるアドレスに配置されていると、CPUが効率的にアクセスできます。

  • アラインされたアクセス: データのアドレスがそのデータ型のサイズ(またはその倍数)に合致している場合。CPUは一度のメモリアクセスでデータを読み書きできるため、高速です。
  • アラインされていないアクセス (Unaligned Access): データのアドレスがそのデータ型のサイズに合致していない場合。CPUはデータを読み書きするために複数回のメモリアクセスを行ったり、特別なハードウェア処理を必要としたりするため、パフォーマンスが低下します。一部のアーキテクチャでは、アラインされていないアクセスがハードウェア例外を引き起こすこともあります。

例えば、4バイトの uint32 型のデータがアドレス 0x1000 に配置されていればアラインされていますが、アドレス 0x1001 に配置されている場合はアラインされていません。

Go言語の unsafe パッケージ

Go言語の unsafe パッケージは、Goの型安全性をバイパスして、低レベルのメモリ操作を可能にするためのパッケージです。通常、Goは厳格な型システムを持ち、ポインタ演算や型変換を制限することで安全性を保証しますが、unsafe パッケージを使用すると、C言語のようにメモリを直接操作できます。

  • unsafe.Pointer: 任意の型のポインタを保持できる汎用ポインタ型です。*T 型から unsafe.Pointer へ、また unsafe.Pointer から *T 型へ、そして uintptr 型との間で相互に変換できます。
  • uintptr: 整数型であり、ポインタの値を保持できます。unsafe.Pointeruintptr の間で変換することで、ポインタの値を整数として扱い、ポインタ演算(アドレスの加算・減算)を行うことができます。
  • unsafe.Alignof(x): 式 x のアラインメント要件を返します。これは、x がメモリ上で配置される際に要求されるバイト境界を示します。例えば、unsafe.Alignof(uint32(0)) は通常4を返します。

unsafe パッケージは強力ですが、Goのメモリ安全性を損なう可能性があるため、慎重に使用する必要があります。主に、C言語との相互運用、システムプログラミング、またはパフォーマンスが極めて重要な場面での最適化に利用されます。

Go言語のベンチマーク (testing パッケージ)

Go言語の標準ライブラリ testing パッケージには、ベンチマークテストを記述するための機能が含まれています。ベンチマーク関数は BenchmarkXxx(*testing.B) というシグネチャを持ち、b.N 回の操作を実行するループを含みます。

  • b.SetBytes(n): ベンチマークが処理するバイト数を指定します。これにより、結果が「MB/s」(1秒あたりのメガバイト数)として表示されるようになります。
  • b.ResetTimer(): ベンチマークの計測を開始します。セットアップコードの時間を計測から除外するために使用されます。
  • b.N: ベンチマーク関数が実行される回数です。testing パッケージが自動的に調整し、統計的に有意な結果が得られるようにします。

このコミットでは、BenchmarkHash8BytesUnaligned のように、アラインされていない入力に対するベンチマークも追加されており、様々な条件下でのパフォーマンス特性を評価しています。

技術的詳細

このコミットの主要な技術的詳細は、MD5ハッシュ計算のコア部分である block 関数における入力データの処理方法の変更にあります。MD5の block 関数は、入力バイトスライス p を処理し、内部状態を更新します。MD5のアルゴリズムは32ビットワード(uint32)で動作するため、入力バイトスライスを効率的に32ビットワードとして読み込むことがパフォーマンス向上に繋がります。

変更前のコードでは、入力スライス p の先頭アドレスが uint32 のアラインメント要件を満たしているかどうかを明示的にチェックしていませんでした。代わりに、p の長さが _BlockSize (64バイト) と等しい場合にのみ、unsafe.Pointer を使用して p*[16]uint32 型に直接キャストしていました。これは、p_BlockSize の倍数で、かつそのアドレスが uint32 のアラインメント要件を満たしている場合にのみ安全な最適化でした。

このコミットでは、以下の重要な変更が加えられました。

  1. アラインメントチェックの追加: src/pkg/crypto/md5/gen.go および src/pkg/crypto/md5/md5block.goblock 関数において、入力スライス p の先頭アドレスが uint32 のアラインメント要件を満たしているかどうかを uintptr(unsafe.Pointer(&p[0]))&(unsafe.Alignof(uint32(0))-1) == 0 という式でチェックするようになりました。

    • unsafe.Pointer(&p[0]): バイトスライス p の最初の要素のアドレスを unsafe.Pointer に変換します。
    • uintptr(...): unsafe.Pointer を整数型 uintptr に変換し、アドレス値を数値として扱えるようにします。
    • unsafe.Alignof(uint32(0)): uint32 型のアラインメント要件(通常は4)を取得します。
    • unsafe.Alignof(uint32(0))-1: アラインメント要件から1を引いた値(例: 4-1=3)は、アラインメントチェックのためのビットマスクとして機能します。
    • &: ビットAND演算子です。アドレス値とビットマスクのビットANDを取ることで、アドレスがアラインメント要件の倍数であるかどうかをチェックします。結果が0であれば、アドレスはアラインされています。
  2. アラインされた場合の uint32 ロードの適用: 上記のチェックにより、p の先頭アドレスが uint32 のアラインメント要件を満たしていることが確認できた場合、X = (*[16]uint32)(unsafe.Pointer(&p[0])) という行が実行されます。これにより、入力バイトスライス p が直接 uint32 の配列として扱われ、CPUが効率的に32ビットワードをロードできるようになります。これは、バイト単位でデータを読み込むよりもはるかに高速です。

  3. アラインされていない場合のフォールバック: もしアラインメントチェックが失敗した場合(つまり、入力がアラインされていない場合)、コードは X = &xbufj := 0 のブロックにフォールバックします。このブロックでは、p のバイトデータを xbuf という一時的な [16]uint32 配列にバイト単位でコピーします。このコピー処理はアラインされたロードよりも遅いですが、アラインされていない入力に対しても正しく動作することを保証します。

  4. ベンチマークの追加: src/pkg/crypto/md5/md5_test.go に、アラインされていない入力に対するベンチマーク関数 (BenchmarkHash8BytesUnaligned, BenchmarkHash1KUnaligned, BenchmarkHash8KUnaligned) が追加されました。これにより、アラインメントがパフォーマンスに与える影響を定量的に評価できるようになり、最適化の効果を明確に示しています。

  5. テストケースの拡張: TestGolden 関数内で、アラインされていない書き込みをテストするためのロジックが追加されました。これにより、md5.Write がアラインされていない入力に対しても正しくハッシュ値を計算できることが保証されます。

これらの変更により、Goの crypto/md5 パッケージは、アラインされた入力に対しては最大限のパフォーマンスを発揮し、アラインされていない入力に対しても正しく、かつ合理的なパフォーマンスで動作するようになりました。ベンチマーク結果が示すように、特に大きなデータブロック(1KBや8KB)のアラインされたハッシュ計算において、大幅な速度向上が実現されています。

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

このコミットで変更された主要なファイルとコードスニペットは以下の通りです。

src/pkg/crypto/md5/gen.go および src/pkg/crypto/md5/md5block.go

これらのファイルは、MD5のコアとなる block 関数の実装を含んでいます。変更は両方のファイルで同一です。

--- a/src/pkg/crypto/md5/gen.go
+++ b/src/pkg/crypto/md5/gen.go
@@ -203,6 +203,8 @@ func block(dig *digest, p []byte) {
 			// less code and run 1.3x faster if we take advantage of that.\n \t\t\t// My apologies.\n \t\t\tX = (*[16]uint32)(unsafe.Pointer(&p[0]))
+\t\t} else if uintptr(unsafe.Pointer(&p[0]))&(unsafe.Alignof(uint32(0))-1) == 0 {
+\t\t\tX = (*[16]uint32)(unsafe.Pointer(&p[0]))
 \t\t} else {
 \t\t\tX = &xbuf
 \t\t\tj := 0

src/pkg/crypto/md5/md5_test.go

このファイルには、MD5パッケージのテストとベンチマークが含まれています。

--- a/src/pkg/crypto/md5/md5_test.go
+++ b/src/pkg/crypto/md5/md5_test.go
@@ -9,6 +9,7 @@ import (
 	"fmt"
 	"io"
 	"testing"
+	"unsafe"
 )
 
 type md5Test struct {
@@ -54,13 +55,19 @@ func TestGolden(t *testing.T) {
 	for i := 0; i < len(golden); i++ {
 		g := golden[i]
 		c := md5.New()
-		for j := 0; j < 3; j++ {
+		for j := 0; j < 3+4; j++ {
 			if j < 2 {
 				io.WriteString(c, g.in)
-			} else {
+			} else if j == 2 {
 				io.WriteString(c, g.in[0:len(g.in)/2])
 				c.Sum(nil)
 				io.WriteString(c, g.in[len(g.in)/2:])
+			} else if j > 2 {
+				// test unaligned write
+				buf = buf[1:]
+				copy(buf, g.in)
+				c.Write(buf[:len(g.in)])
 			}
 			s := fmt.Sprintf("%x", c.Sum(nil))
 			if s != g.out {
@@ -80,11 +87,18 @@ func ExampleNew() {
 }
 
 var bench = md5.New()
-var buf = make([]byte, 8192)
+var buf = make([]byte, 8192+1)
+var sum = make([]byte, bench.Size())
 
-func benchmarkSize(b *testing.B, size int) {
+func benchmarkSize(b *testing.B, size int, unaligned bool) {
 	b.SetBytes(int64(size))
-	sum := make([]byte, bench.Size())
+	buf := buf
+	if unaligned {
+		if uintptr(unsafe.Pointer(&buf[0]))&(unsafe.Alignof(uint32(0))-1) == 0 {
+			buf = buf[1:]
+		}
+	}
+	b.ResetTimer()
 	for i := 0; i < b.N; i++ {
 		bench.Reset()
 		bench.Write(buf[:size])
@@ -93,13 +107,25 @@ func benchmarkSize(b *testing.B, size int) {
 }
 
 func BenchmarkHash8Bytes(b *testing.B) {
-	benchmarkSize(b, 8)
+	benchmarkSize(b, 8, false)
 }
 
 func BenchmarkHash1K(b *testing.B) {
-	benchmarkSize(b, 1024)
+	benchmarkSize(b, 1024, false)
 }
 
 func BenchmarkHash8K(b *testing.B) {
-	benchmarkSize(b, 8192)
+	benchmarkSize(b, 8192, false)
+}
+
+func BenchmarkHash8BytesUnaligned(b *testing.B) {
+	benchmarkSize(b, 8, true)
+}
+
+func BenchmarkHash1KUnaligned(b *testing.B) {
+	benchmarkSize(b, 1024, true)
+}
+
+func BenchmarkHash8KUnaligned(b *testing.B) {
+	benchmarkSize(b, 8192, true)
 }

コアとなるコードの解説

src/pkg/crypto/md5/gen.go および src/pkg/crypto/md5/md5block.go の変更

これらのファイルにおける block 関数の変更は、MD5ハッシュ計算のパフォーマンス最適化の核心です。

			// less code and run 1.3x faster if we take advantage of that.
			// My apologies.
			X = (*[16]uint32)(unsafe.Pointer(&p[0]))
		} else if uintptr(unsafe.Pointer(&p[0]))&(unsafe.Alignof(uint32(0))-1) == 0 {
			X = (*[16]uint32)(unsafe.Pointer(&p[0]))
		} else {
			X = &xbuf
			j := 0
  • 既存の最適化: 変更前のコードには、既に p の長さが _BlockSize (64バイト) と等しい場合に unsafe.Pointer を使って p*[16]uint32 に直接キャストする最適化がありました。これは、入力が64バイトブロックで、かつアラインされている場合に高速な処理を可能にするものでした。コメントにある "less code and run 1.3x faster" は、この最適化の重要性を示しています。

  • 新しいアラインメントチェックと最適化: else if uintptr(unsafe.Pointer(&p[0]))&(unsafe.Alignof(uint32(0))-1) == 0 の行が追加されました。

    • これは、入力スライス p の先頭アドレスが uint32 のアラインメント要件(通常4バイト)を満たしているかどうかをチェックしています。
    • unsafe.Alignof(uint32(0))uint32 のアラインメント要件を返します。例えば、これが4であれば、unsafe.Alignof(uint32(0))-1 は3(バイナリで 0b11)になります。
    • uintptr(unsafe.Pointer(&p[0]))p の先頭アドレスを整数値として取得します。
    • このアドレス値と (unsafe.Alignof(uint32(0))-1) のビットAND演算を行い、結果が 0 であれば、アドレスは uint32 のアラインメント要件を満たしていることになります。例えば、アドレスが 0x1000 (アラインされている) であれば 0x1000 & 0x3 = 0。アドレスが 0x1001 (アラインされていない) であれば 0x1001 & 0x3 = 1 となり、0以外になります。
    • この条件が真の場合、つまり puint32 のアラインメント要件を満たしている場合、X = (*[16]uint32)(unsafe.Pointer(&p[0])) が実行されます。これにより、p のバイトデータが直接 uint32 の配列として扱われ、CPUが効率的に32ビットワードをロードできるようになります。これは、アラインされたデータに対する高速な処理を可能にします。
  • アラインされていない場合のフォールバック: else ブロックは、入力がアラインされていない場合(または、既存の len(p) == _BlockSize の条件も満たさない場合)に実行されます。

    • X = &xbuf は、xbuf という一時的な [16]uint32 配列をMD5の内部処理で使用することを意味します。
    • j := 0 から始まる後続のコード(diffには含まれていませんが、元のコードには存在します)は、入力スライス p のバイトデータを xbuf にバイト単位でコピーする処理を行います。このバイト単位のコピーは、アラインされた uint32 ロードよりも遅いですが、アラインされていない入力に対しても正しく動作することを保証します。

この変更により、block 関数は入力データのアラインメント状態を動的に判断し、最適なデータアクセス方法を選択することで、パフォーマンスと堅牢性を両立させています。

src/pkg/crypto/md5/md5_test.go の変更

テストファイルでは、主にベンチマークとテストケースの拡張が行われています。

  • unsafe パッケージのインポート: import "unsafe" が追加され、アラインメント関連の操作が可能になりました。

  • TestGolden 関数の変更:

    		for j := 0; j < 3+4; j++ {
    			if j < 2 {
    				io.WriteString(c, g.in)
    			} else if j == 2 {
    				io.WriteString(c, g.in[0:len(g.in)/2])
    				c.Sum(nil)
    				io.WriteString(c, g.in[len(g.in)/2:])
    			} else if j > 2 {
    				// test unaligned write
    				buf = buf[1:]
    				copy(buf, g.in)
    				c.Write(buf[:len(g.in)])
    			}
    

    ループの回数が 3 から 3+4 に増え、j > 2 の条件でアラインされていない書き込みをテストする新しいパスが追加されました。

    • buf = buf[1:]: buf スライスの先頭を1バイトずらすことで、buf の先頭アドレスがアラインされていない状態を作り出します。
    • copy(buf, g.in): 元の入力データをアラインされていない buf にコピーします。
    • c.Write(buf[:len(g.in)]): アラインされていないデータを含む bufmd5.Write に渡してテストします。これにより、md5.Write がアラインされていない入力に対しても正しく動作することが検証されます。
  • ベンチマーク関数の変更と追加:

    var buf = make([]byte, 8192+1) // +1 for unaligned tests
    var sum = make([]byte, bench.Size())
    
    func benchmarkSize(b *testing.B, size int, unaligned bool) {
    	b.SetBytes(int64(size))
    	buf := buf
    	if unaligned {
    		if uintptr(unsafe.Pointer(&buf[0]))&(unsafe.Alignof(uint32(0))-1) == 0 {
    			buf = buf[1:]
    		}
    	}
    	b.ResetTimer()
    	// ... (benchmarking loop)
    }
    
    func BenchmarkHash8Bytes(b *testing.B) { benchmarkSize(b, 8, false) }
    func BenchmarkHash1K(b *testing.B) { benchmarkSize(b, 1024, false) }
    func BenchmarkHash8K(b *testing.B) { benchmarkSize(b, 8192, false) }
    
    func BenchmarkHash8BytesUnaligned(b *testing.B) { benchmarkSize(b, 8, true) }
    func BenchmarkHash1KUnaligned(b *testing.B) { benchmarkSize(b, 1024, true) }
    func BenchmarkHash8KUnaligned(b *testing.B) { benchmarkSize(b, 8192, true) }
    
    • var buf = make([]byte, 8192+1): ベンチマーク用のバッファサイズが1バイト増やされ、アラインされていないテストケースに対応できるようになりました。
    • benchmarkSize 関数に unaligned というブール型の引数が追加されました。
    • if unaligned ブロック内で、buf の先頭アドレスが uint32 のアラインメント要件を満たしている場合に、buf = buf[1:] を実行して意図的にアラインされていない状態を作り出します。これにより、アラインされていない入力に対するベンチマークが可能になります。
    • 新しいベンチマーク関数 (BenchmarkHash8BytesUnaligned など) が追加され、アラインされた入力とアラインされていない入力の両方でパフォーマンスを測定できるようになりました。

これらのテストとベンチマークの追加により、MD5実装の堅牢性とパフォーマンス特性がより詳細に評価できるようになり、最適化の効果が明確に示されています。

関連リンク

参考にした情報源リンク