[インデックス 18166] ファイルの概要
このコミットは、Go言語の標準ライブラリである crypto/sha1
、crypto/sha256
、crypto/sha512
パッケージにおけるパフォーマンス改善を目的としています。具体的には、ハッシュ計算時に部分的な入力ブロックをスクラッチ領域にコピーする際の処理を、手動で記述されたループからGo言語の組み込み関数 copy
に置き換えることで、特に小さなデータブロックを頻繁にハッシュ化する際の効率を向上させています。
コミット
commit 29fe067ba7257bcfa7b1a15304592997b0d3c294
Author: Joel Sing <jsing@google.com>
Date: Mon Jan 6 01:34:56 2014 +1100
crypto/sha1, crypto/sha256, crypto/sha512: use copy for partial block
Use copy rather than a hand rolled loop when moving a partial input
block to the scratch area. This results in a reasonable performance
gain when partial blocks are written.
Benchmarks on Intel(R) Xeon(R) CPU X5650 @ 2.67GHz with Go amd64:
benchmark old MB/s new MB/s speedup
SHA1 BenchmarkHash8Bytes 18.37 22.80 1.24x
SHA256 BenchmarkHash8Bytes 11.86 13.78 1.16x
SHA512 BenchmarkHash8Bytes 4.51 5.24 1.16x
benchmark old ns/op new ns/op delta
SHA1 BenchmarkHash8Bytes 435 350 -19.54%
SHA256 BenchmarkHash8Bytes 674 580 -13.95%
SHA512 BenchmarkHash8Bytes 1772 1526 -13.88%
R=agl, dave, bradfitz
CC=golang-codereviews
https://golang.org/cl/35840044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/29fe067ba7257bcfa7b1a15304592997b0d3c294
元コミット内容
このコミットの目的は、crypto/sha1
、crypto/sha256
、crypto/sha512
パッケージにおいて、部分的な入力ブロックをスクラッチ領域に移動する際に、手書きのループではなく copy
関数を使用することです。これにより、部分的なブロックが書き込まれる際のパフォーマンスが向上します。
ベンチマーク結果(Intel(R) Xeon(R) CPU X5650 @ 2.67GHz, Go amd64)は以下の通りです。
ベンチマーク | 旧 MB/s | 新 MB/s | 速度向上 | 旧 ns/op | 新 ns/op | 差分 |
---|---|---|---|---|---|---|
SHA1 BenchmarkHash8Bytes | 18.37 | 22.80 | 1.24x | 435 | 350 | -19.54% |
SHA256 BenchmarkHash8Bytes | 11.86 | 13.78 | 1.16x | 674 | 580 | -13.95% |
SHA512 BenchmarkHash8Bytes | 4.51 | 5.24 | 1.16x | 1772 | 1526 | -13.88% |
変更の背景
Go言語の暗号化ハッシュ関数(SHA1, SHA256, SHA512)の実装において、入力データがハッシュアルゴリズムのブロックサイズに満たない場合(「部分ブロック」と呼ばれる)、そのデータを一時的なバッファ(スクラッチ領域)に格納し、次の入力データと結合して完全なブロックを形成してから処理を行う必要があります。
以前の実装では、この部分ブロックのコピーに手動で記述された for
ループが使用されていました。しかし、Go言語にはスライス間のデータコピーを効率的に行うための組み込み関数 copy
が存在します。copy
関数は、多くの場合、手動で記述されたループよりも最適化されており、特にアセンブリレベルでの最適化や、CPUのSIMD命令(Single Instruction, Multiple Data)を活用することで、高速なデータ転送を実現できます。
このコミットの背景には、Go言語の標準ライブラリのパフォーマンスを継続的に改善するという目標があります。特に、暗号化ハッシュ関数は多くのアプリケーションで利用される基本的な機能であり、その性能向上はシステム全体のパフォーマンスに寄与します。ベンチマーク結果が示すように、小さなデータ(8バイト)のハッシュ計算において顕著な改善が見られたことから、この変更が特にストリーミング処理や、小さなチャンクでデータが供給されるシナリオにおいて有効であることが示唆されます。
前提知識の解説
1. ハッシュ関数 (Cryptographic Hash Function)
ハッシュ関数は、任意の長さの入力データ(メッセージ)を受け取り、固定長の短い出力(ハッシュ値、メッセージダイジェスト、フィンガープリントなどと呼ばれる)を生成する一方向性の関数です。暗号学的ハッシュ関数は、以下の特性を持つことが求められます。
- 一方向性 (Preimage Resistance): ハッシュ値から元の入力データを効率的に復元することは困難である。
- 第二原像耐性 (Second Preimage Resistance): ある入力データと同じハッシュ値を持つ別の入力データを効率的に見つけることは困難である。
- 衝突耐性 (Collision Resistance): 同じハッシュ値を持つ異なる2つの入力データを効率的に見つけることは困難である。
SHA-1、SHA-256、SHA-512は、米国国家安全保障局(NSA)によって設計されたSecure Hash Algorithm (SHA) ファミリーに属する暗号学的ハッシュ関数です。
- SHA-1: 160ビットのハッシュ値を生成します。現在ではセキュリティ上の脆弱性が指摘されており、新規のアプリケーションでの使用は推奨されません。
- SHA-256: 256ビットのハッシュ値を生成します。SHA-2ファミリーの一部であり、広く利用されています。
- SHA-512: 512ビットのハッシュ値を生成します。SHA-2ファミリーの一部であり、SHA-256よりも長いハッシュ値を生成するため、より高いセキュリティレベルが求められる場合に利用されます。
これらのハッシュ関数は、内部的にデータを固定長のブロックに分割し、各ブロックに対して一連の数学的・論理的演算を適用することでハッシュ値を計算します。
2. Go言語のスライス (Slice)
Go言語のスライスは、配列のセグメントを参照するデータ構造です。スライスは、基となる配列へのポインタ、長さ(len
)、容量(cap
)の3つの要素で構成されます。スライスは動的なサイズ変更が可能であり、Go言語で最も頻繁に使用されるデータ構造の一つです。
3. copy
関数 (Go Built-in Function)
Go言語の組み込み関数 copy
は、ソーススライスからデスティネーションスライスへ要素をコピーするために使用されます。そのシグネチャは copy(dst, src []Type) int
であり、コピーされた要素の数を返します。copy
関数は、Goランタイムによって高度に最適化されており、多くの場合、手動で記述されたループよりも高速に動作します。これは、コンパイラが特定のアーキテクチャの特性(例: SIMD命令)を利用して、より効率的なコードを生成できるためです。
4. d.x[0:]
と d.x[:]
Go言語のスライス表記において、d.x[0:]
と d.x[:]
はどちらもスライス d.x
の全体を参照する新しいスライスを作成します。機能的には同じですが、d.x[:]
の方がより簡潔で慣用的な表現とされています。このコミットでは、この慣用的な表現への変更も行われています。
技術的詳細
このコミットの核心は、SHAハッシュ関数の Write
メソッドにおける最適化です。Write
メソッドは、入力データ p
を受け取り、それをハッシュ計算のために処理します。ハッシュ計算はブロック単位で行われるため、入力データがブロックサイズに満たない場合、その「部分ブロック」は内部バッファ d.x
に一時的に格納されます。
変更前のコードでは、この部分ブロックのコピーに以下のような手動ループが使用されていました。
// 変更前 (例: SHA1)
if d.nx > 0 { // d.nx はバッファに既に格納されているデータの量
n := len(p) // 入力データの長さ
if n > chunk-d.nx { // 入力データがバッファの残りの容量を超える場合
n = chunk - d.nx // コピーする量をバッファの残りの容量に制限
}
for i := 0; i < n; i++ {
d.x[d.nx+i] = p[i] // 1バイトずつ手動でコピー
}
d.nx += n
if d.nx == chunk {
block(d, d.x[0:]) // 完全なブロックが形成されたら処理
d.nx = 0
}
p = p[n:]
}
このループは、入力データ p
の先頭から n
バイトを、内部バッファ d.x
の d.nx
オフセットからコピーしています。
変更後のコードでは、この手動ループがGo言語の組み込み関数 copy
に置き換えられました。
// 変更後 (例: SHA1)
if d.nx > 0 {
n := copy(d.x[d.nx:], p) // d.x[d.nx:] はd.xのd.nx以降のスライス
d.nx += n
if d.nx == chunk {
block(d, d.x[:]) // d.x[0:] から d.x[:] へ変更
d.nx = 0
}
p = p[n:]
}
copy(d.x[d.nx:], p)
は、以下の処理を効率的に行います。
d.x[d.nx:]
: これは、内部バッファd.x
のd.nx
インデックスから始まるスライスを作成します。これがcopy
関数のデスティネーション(コピー先)となります。p
: これは、入力データを含むスライスであり、copy
関数のソース(コピー元)となります。copy
関数は、p
の内容をd.x[d.nx:]
にコピーします。コピーされるバイト数は、デスティネーションスライスまたはソーススライスのいずれか短い方の長さによって決定されます。これにより、手動ループで行っていたn
の計算と範囲チェックが不要になります。copy
関数は実際にコピーされたバイト数を返します。この戻り値がn
に代入され、d.nx
の更新に使用されます。
この変更により、コンパイラは copy
関数の呼び出しを、より最適化されたアセンブリ命令(例えば、REP MOVSB
のようなブロック転送命令や、SIMD命令)に変換できるようになります。これにより、特に小さなデータブロックが頻繁にコピーされるシナリオ(例: BenchmarkHash8Bytes
のようなベンチマーク)において、CPUの効率的な利用が可能となり、パフォーマンスが向上します。
また、block(d, d.x[0:])
が block(d, d.x[:])
に変更されています。これは機能的な変更ではなく、Go言語におけるスライス全体の参照の慣用的な表現への変更です。どちらも d.x
スライスの基底配列全体を参照する新しいスライスを生成しますが、d.x[:]
の方がより簡潔で推奨される書き方です。
コアとなるコードの変更箇所
変更は src/pkg/crypto/sha1/sha1.go
、src/pkg/crypto/sha256/sha256.go
、src/pkg/crypto/sha512/sha512.go
の3つのファイルに共通して適用されています。
各ファイルの Write
メソッド内の d.nx > 0
の条件ブロックが変更されています。
変更前:
// src/pkg/crypto/sha1/sha1.go (同様の変更がsha256.go, sha512.goにも適用)
if d.nx > 0 {
n := len(p)
if n > chunk-d.nx {
n = chunk - d.nx
}
for i := 0; i < n; i++ {
d.x[d.nx+i] = p[i]
}
d.nx += n
if d.nx == chunk {
block(d, d.x[0:]) // 変更点1
d.nx = 0
}
p = p[n:]
}
変更後:
// src/pkg/crypto/sha1/sha1.go (同様の変更がsha256.go, sha512.goにも適用)
if d.nx > 0 {
n := copy(d.x[d.nx:], p) // 変更点2: 手動ループをcopy関数に置き換え
d.nx += n
if d.nx == chunk {
block(d, d.x[:]) // 変更点1: d.x[0:] から d.x[:] へ変更
d.nx = 0
}
p = p[n:]
}
コアとなるコードの解説
d
はハッシュダイジェストの状態を保持する構造体(digest
)のインスタンスです。
d.x
は、ハッシュ計算のための内部バッファ(スクラッチ領域)であり、通常はバイトスライスです。
d.nx
は、d.x
バッファに現在格納されているデータのバイト数を示します。
chunk
は、ハッシュアルゴリズムのブロックサイズ(例: SHA1は64バイト、SHA256は64バイト、SHA512は128バイト)です。
Write
メソッドが呼び出され、入力データ p
が渡されると、まず d.nx > 0
の条件がチェックされます。これは、前回の Write
呼び出しで部分的なデータが d.x
に残っているかどうかを確認しています。
変更前:
手動ループでは、まず n
を計算して、入力データ p
から d.x
にコピーできる最大バイト数を決定していました。これは、p
の長さと d.x
の残りの容量 (chunk - d.nx
) の小さい方です。その後、for
ループを使って1バイトずつ p[i]
から d.x[d.nx+i]
へとデータをコピーしていました。このバイトごとのコピーは、オーバーヘッドが大きく、特に大量の小さなコピーが発生する場合に非効率的でした。
変更後:
n := copy(d.x[d.nx:], p)
の行が、このコピー処理を大幅に簡素化し、効率化しています。
d.x[d.nx:]
は、d.x
スライスのd.nx
インデックスから始まる部分スライスを作成します。これは、部分ブロックを格納するためのd.x
内の利用可能な領域を指します。copy
関数は、このターゲットスライス (d.x[d.nx:]
) とソーススライス (p
) を受け取り、可能な限り多くのバイトをコピーします。copy
関数は、コピー先の容量またはコピー元の長さのいずれか小さい方までしかコピーしません。これにより、手動ループで行っていたn
の計算と境界チェックが不要になります。copy
関数は実際にコピーされたバイト数をn
として返します。このn
を使ってd.nx
を更新し、d.x
に格納されているデータの総量を追跡します。
if d.nx == chunk
のブロックは、d.x
バッファが完全に満たされた場合に、block
関数を呼び出してハッシュ計算の次のステップを実行し、d.nx
をリセットするロジックです。ここで block(d, d.x[0:])
が block(d, d.x[:])
に変更されていますが、これは前述の通り、スライス全体の参照の慣用的な表現への変更であり、機能的な違いはありません。
この変更により、Goランタイムの最適化された copy
関数が利用されるため、特に小さなデータブロックのコピーにおいて、CPUの効率的な利用とパフォーマンスの向上が実現されました。ベンチマーク結果は、この変更が実際にSHAハッシュ関数の性能を向上させたことを明確に示しています。
関連リンク
- Go言語の
copy
関数に関する公式ドキュメント: https://pkg.go.dev/builtin#copy - Go言語の
crypto/sha1
パッケージ: https://pkg.go.dev/crypto/sha1 - Go言語の
crypto/sha256
パッケージ: https://pkg.go.dev/crypto/sha256 - Go言語の
crypto/sha512
パッケージ: https://pkg.go.dev/crypto/sha512
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード
- 暗号学的ハッシュ関数に関する一般的な情報源 (例: Wikipedia, NIST publications)
- Go言語のベンチマークに関する一般的な知識
- Go言語のスライスに関する一般的な知識
- Go言語の
copy
関数の内部実装と最適化に関する情報 (Goのソースコードや関連するブログ記事など) - Go言語のコードレビューシステム (Gerrit) のCL (Change-list) ページ: https://golang.org/cl/35840044
- このCLページには、コミットメッセージ、変更されたファイル、レビューコメントなどが含まれており、コミットの背景や議論を理解する上で非常に有用です。
- 特に、ベンチマーク結果がコミットメッセージに直接含まれているため、変更の意図と効果を明確に理解できます。
- Go言語の
crypto
パッケージの歴史と進化に関する情報 (Goのリリースノートやメーリングリストのアーカイブなど)- このコミットは2014年のものであり、Go言語の初期の最適化努力の一環として位置づけられます。
- SIMD命令とデータコピーの最適化に関する一般的なコンピュータアーキテクチャの知識。
copy
関数が手動ループよりも高速である理由の一つとして、コンパイラがSIMD命令を活用できる点が挙げられます。
# [インデックス 18166] ファイルの概要
このコミットは、Go言語の標準ライブラリである `crypto/sha1`、`crypto/sha256`、`crypto/sha512` パッケージにおけるパフォーマンス改善を目的としています。具体的には、ハッシュ計算時に部分的な入力ブロックをスクラッチ領域にコピーする際の処理を、手動で記述されたループからGo言語の組み込み関数 `copy` に置き換えることで、特に小さなデータブロックを頻繁にハッシュ化する際の効率を向上させています。
## コミット
commit 29fe067ba7257bcfa7b1a15304592997b0d3c294 Author: Joel Sing jsing@google.com Date: Mon Jan 6 01:34:56 2014 +1100
crypto/sha1, crypto/sha256, crypto/sha512: use copy for partial block
Use copy rather than a hand rolled loop when moving a partial input
block to the scratch area. This results in a reasonable performance
gain when partial blocks are written.
Benchmarks on Intel(R) Xeon(R) CPU X5650 @ 2.67GHz with Go amd64:
benchmark old MB/s new MB/s speedup
SHA1 BenchmarkHash8Bytes 18.37 22.80 1.24x
SHA256 BenchmarkHash8Bytes 11.86 13.78 1.16x
SHA512 BenchmarkHash8Bytes 4.51 5.24 1.16x
benchmark old ns/op new ns/op delta
SHA1 BenchmarkHash8Bytes 435 350 -19.54%
SHA256 BenchmarkHash8Bytes 674 580 -13.95%
SHA512 BenchmarkHash8Bytes 1772 1526 -13.88%
R=agl, dave, bradfitz
CC=golang-codereviews
https://golang.org/cl/35840044
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/29fe067ba7257bcfa7b1a15304592997b0d3c294](https://github.com/golang/go/commit/29fe067ba7257bcfa7b1a15304592997b0d3c294)
## 元コミット内容
このコミットの目的は、`crypto/sha1`、`crypto/sha256`、`crypto/sha512` パッケージにおいて、部分的な入力ブロックをスクラッチ領域に移動する際に、手書きのループではなく `copy` 関数を使用することです。これにより、部分的なブロックが書き込まれる際のパフォーマンスが向上します。
ベンチマーク結果(Intel(R) Xeon(R) CPU X5650 @ 2.67GHz, Go amd64)は以下の通りです。
| ベンチマーク | 旧 MB/s | 新 MB/s | 速度向上 | 旧 ns/op | 新 ns/op | 差分 |
| :------------------- | :------ | :------ | :------- | :------- | :------- | :------ |
| SHA1 BenchmarkHash8Bytes | 18.37 | 22.80 | 1.24x | 435 | 350 | -19.54% |
| SHA256 BenchmarkHash8Bytes | 11.86 | 13.78 | 1.16x | 674 | 580 | -13.95% |
| SHA512 BenchmarkHash8Bytes | 4.51 | 5.24 | 1.16x | 1772 | 1526 | -13.88% |
## 変更の背景
Go言語の暗号化ハッシュ関数(SHA1, SHA256, SHA512)の実装において、入力データがハッシュアルゴリズムのブロックサイズに満たない場合(「部分ブロック」と呼ばれる)、そのデータを一時的なバッファ(スクラッチ領域)に格納し、次の入力データと結合して完全なブロックを形成してから処理を行う必要があります。
以前の実装では、この部分ブロックのコピーに手動で記述された `for` ループが使用されていました。しかし、Go言語にはスライス間のデータコピーを効率的に行うための組み込み関数 `copy` が存在します。`copy` 関数は、多くの場合、手動で記述されたループよりも最適化されており、特にアセンブリレベルでの最適化や、CPUのSIMD命令(Single Instruction, Multiple Data)を活用することで、高速なデータ転送を実現できます。
このコミットの背景には、Go言語の標準ライブラリのパフォーマンスを継続的に改善するという目標があります。特に、暗号化ハッシュ関数は多くのアプリケーションで利用される基本的な機能であり、その性能向上はシステム全体のパフォーマンスに寄与します。ベンチマーク結果が示すように、小さなデータ(8バイト)のハッシュ計算において顕著な改善が見られたことから、この変更が特にストリーミング処理や、小さなチャンクでデータが供給されるシナリオにおいて有効であることが示唆されます。
## 前提知識の解説
### 1. ハッシュ関数 (Cryptographic Hash Function)
ハッシュ関数は、任意の長さの入力データ(メッセージ)を受け取り、固定長の短い出力(ハッシュ値、メッセージダイジェスト、フィンガープリントなどと呼ばれる)を生成する一方向性の関数です。暗号学的ハッシュ関数は、以下の特性を持つことが求められます。
* **一方向性 (Preimage Resistance)**: ハッシュ値から元の入力データを効率的に復元することは困難である。
* **第二原像耐性 (Second Preimage Resistance)**: ある入力データと同じハッシュ値を持つ別の入力データを効率的に見つけることは困難である。
* **衝突耐性 (Collision Resistance)**: 同じハッシュ値を持つ異なる2つの入力データを効率的に見つけることは困難である。
SHA-1、SHA-256、SHA-512は、米国国家安全保障局(NSA)によって設計されたSecure Hash Algorithm (SHA) ファミリーに属する暗号学的ハッシュ関数です。
* **SHA-1**: 160ビットのハッシュ値を生成します。現在ではセキュリティ上の脆弱性が指摘されており、新規のアプリケーションでの使用は推奨されません。
* **SHA-256**: 256ビットのハッシュ値を生成します。SHA-2ファミリーの一部であり、広く利用されています。
* **SHA-512**: 512ビットのハッシュ値を生成します。SHA-2ファミリーの一部であり、SHA-256よりも長いハッシュ値を生成するため、より高いセキュリティレベルが求められる場合に利用されます。
これらのハッシュ関数は、内部的にデータを固定長のブロックに分割し、各ブロックに対して一連の数学的・論理的演算を適用することでハッシュ値を計算します。
### 2. Go言語のスライス (Slice)
Go言語のスライスは、配列のセグメントを参照するデータ構造です。スライスは、基となる配列へのポインタ、長さ(`len`)、容量(`cap`)の3つの要素で構成されます。スライスは動的なサイズ変更が可能であり、Go言語で最も頻繁に使用されるデータ構造の一つです。
### 3. `copy` 関数 (Go Built-in Function)
Go言語の組み込み関数 `copy` は、ソーススライスからデスティネーションスライスへ要素をコピーするために使用されます。そのシグネチャは `copy(dst, src []Type) int` であり、コピーされた要素の数を返します。`copy` 関数は、Goランタイムによって高度に最適化されており、多くの場合、手動で記述されたループよりも高速に動作します。これは、コンパイラが特定のアーキテクチャの特性(例: SIMD命令)を利用して、より効率的なコードを生成できるためです。
### 4. `d.x[0:]` と `d.x[:]`
Go言語のスライス表記において、`d.x[0:]` と `d.x[:]` はどちらもスライス `d.x` の全体を参照する新しいスライスを作成します。機能的には同じですが、`d.x[:]` の方がより簡潔で慣用的な表現とされています。このコミットでは、この慣用的な表現への変更も行われています。
## 技術的詳細
このコミットの核心は、SHAハッシュ関数の `Write` メソッドにおける最適化です。`Write` メソッドは、入力データ `p` を受け取り、それをハッシュ計算のために処理します。ハッシュ計算はブロック単位で行われるため、入力データがブロックサイズに満たない場合、その「部分ブロック」は内部バッファ `d.x` に一時的に格納されます。
変更前のコードでは、この部分ブロックのコピーに以下のような手動ループが使用されていました。
```go
// 変更前 (例: SHA1)
if d.nx > 0 { // d.nx はバッファに既に格納されているデータの量
n := len(p) // 入力データの長さ
if n > chunk-d.nx { // 入力データがバッファの残りの容量を超える場合
n = chunk - d.nx // コピーする量をバッファの残りの容量に制限
}
for i := 0; i < n; i++ {
d.x[d.nx+i] = p[i] // 1バイトずつ手動でコピー
}
d.nx += n
if d.nx == chunk {
block(d, d.x[0:]) // 完全なブロックが形成されたら処理
d.nx = 0
}
p = p[n:]
}
このループは、入力データ p
の先頭から n
バイトを、内部バッファ d.x
の d.nx
オフセットからコピーしています。
変更後のコードでは、この手動ループがGo言語の組み込み関数 copy
に置き換えられました。
// 変更後 (例: SHA1)
if d.nx > 0 {
n := copy(d.x[d.nx:], p) // d.x[d.nx:] はd.xのd.nx以降のスライス
d.nx += n
if d.nx == chunk {
block(d, d.x[:]) // d.x[0:] から d.x[:] へ変更
d.nx = 0
}
p = p[n:]
}
copy(d.x[d.nx:], p)
は、以下の処理を効率的に行います。
d.x[d.nx:]
: これは、内部バッファd.x
のd.nx
インデックスから始まるスライスを作成します。これがcopy
関数のデスティネーション(コピー先)となります。p
: これは、入力データを含むスライスであり、copy
関数のソース(コピー元)となります。copy
関数は、p
の内容をd.x[d.nx:]
にコピーします。コピーされるバイト数は、デスティネーションスライスまたはソーススライスのいずれか短い方の長さによって決定されます。これにより、手動ループで行っていたn
の計算と範囲チェックが不要になります。copy
関数は実際にコピーされたバイト数を返します。この戻り値がn
に代入され、d.nx
の更新に使用されます。
この変更により、コンパイラは copy
関数の呼び出しを、より最適化されたアセンブリ命令(例えば、REP MOVSB
のようなブロック転送命令や、SIMD命令)に変換できるようになります。これにより、特に小さなデータブロックが頻繁にコピーされるシナリオ(例: BenchmarkHash8Bytes
のようなベンチマーク)において、CPUの効率的な利用が可能となり、パフォーマンスが向上します。
また、block(d, d.x[0:])
が block(d, d.x[:])
に変更されています。これは機能的な変更ではなく、Go言語におけるスライス全体の参照の慣用的な表現への変更です。どちらも d.x
スライスの基底配列全体を参照する新しいスライスを生成しますが、d.x[:]
の方がより簡潔で推奨される書き方です。
コアとなるコードの変更箇所
変更は src/pkg/crypto/sha1/sha1.go
、src/pkg/crypto/sha256/sha256.go
、src/pkg/crypto/sha512/sha512.go
の3つのファイルに共通して適用されています。
各ファイルの Write
メソッド内の d.nx > 0
の条件ブロックが変更されています。
変更前:
// src/pkg/crypto/sha1/sha1.go (同様の変更がsha256.go, sha512.goにも適用)
if d.nx > 0 {
n := len(p)
if n > chunk-d.nx {
n = chunk - d.nx
}
for i := 0; i < n; i++ {
d.x[d.nx+i] = p[i]
}
d.nx += n
if d.nx == chunk {
block(d, d.x[0:]) // 変更点1
d.nx = 0
}
p = p[n:]
}
変更後:
// src/pkg/crypto/sha1/sha1.go (同様の変更がsha256.go, sha512.goにも適用)
if d.nx > 0 {
n := copy(d.x[d.nx:], p) // 変更点2: 手動ループをcopy関数に置き換え
d.nx += n
if d.nx == chunk {
block(d, d.x[:]) // 変更点1: d.x[0:] から d.x[:] へ変更
d.nx = 0
}
p = p[n:]
}
コアとなるコードの解説
d
はハッシュダイジェストの状態を保持する構造体(digest
)のインスタンスです。
d.x
は、ハッシュ計算のための内部バッファ(スクラッチ領域)であり、通常はバイトスライスです。
d.nx
は、d.x
バッファに現在格納されているデータのバイト数を示します。
chunk
は、ハッシュアルゴリズムのブロックサイズ(例: SHA1は64バイト、SHA256は64バイト、SHA512は128バイト)です。
Write
メソッドが呼び出され、入力データ p
が渡されると、まず d.nx > 0
の条件がチェックされます。これは、前回の Write
呼び出しで部分的なデータが d.x
に残っているかどうかを確認しています。
変更前:
手動ループでは、まず n
を計算して、入力データ p
から d.x
にコピーできる最大バイト数を決定していました。これは、p
の長さと d.x
の残りの容量 (chunk - d.nx
) の小さい方です。その後、for
ループを使って1バイトずつ p[i]
から d.x[d.nx+i]
へとデータをコピーしていました。このバイトごとのコピーは、オーバーヘッドが大きく、特に大量の小さなコピーが発生する場合に非効率的でした。
変更後:
n := copy(d.x[d.nx:], p)
の行が、このコピー処理を大幅に簡素化し、効率化しています。
d.x[d.nx:]
は、d.x
スライスのd.nx
インデックスから始まる部分スライスを作成します。これは、部分ブロックを格納するためのd.x
内の利用可能な領域を指します。copy
関数は、このターゲットスライス (d.x[d.nx:]
) とソーススライス (p
) を受け取り、可能な限り多くのバイトをコピーします。copy
関数は、コピー先の容量またはコピー元の長さのいずれか小さい方までしかコピーしません。これにより、手動ループで行っていたn
の計算と境界チェックが不要になります。copy
関数は実際にコピーされたバイト数をn
として返します。このn
を使ってd.nx
を更新し、d.x
に格納されているデータの総量を追跡します。
if d.nx == chunk
のブロックは、d.x
バッファが完全に満たされた場合に、block
関数を呼び出してハッシュ計算の次のステップを実行し、d.nx
をリセットするロジックです。ここで block(d, d.x[0:])
が block(d, d.x[:])
に変更されていますが、これは前述の通り、スライス全体の参照の慣用的な表現への変更であり、機能的な違いはありません。
この変更により、Goランタイムの最適化された copy
関数が利用されるため、特に小さなデータブロックのコピーにおいて、CPUの効率的な利用とパフォーマンスの向上が実現されました。ベンチマーク結果は、この変更が実際にSHAハッシュ関数の性能を向上させたことを明確に示しています。
関連リンク
- Go言語の
copy
関数に関する公式ドキュメント: https://pkg.go.dev/builtin#copy - Go言語の
crypto/sha1
パッケージ: https://pkg.go.dev/crypto/sha1 - Go言語の
crypto/sha256
パッケージ: https://pkg.go.dev/crypto/sha256 - Go言語の
crypto/sha512
パッケージ: https://pkg.go.dev/crypto/sha512
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード
- 暗号学的ハッシュ関数に関する一般的な情報源 (例: Wikipedia, NIST publications)
- Go言語のベンチマークに関する一般的な知識
- Go言語のスライスに関する一般的な知識
- Go言語の
copy
関数の内部実装と最適化に関する情報 (Goのソースコードや関連するブログ記事など) - Go言語のコードレビューシステム (Gerrit) のCL (Change-list) ページ: https://golang.org/cl/35840044
- このCLページには、コミットメッセージ、変更されたファイル、レビューコメントなどが含まれており、コミットの背景や議論を理解する上で非常に有用です。
- 特に、ベンチマーク結果がコミットメッセージに直接含まれているため、変更の意図と効果を明確に理解できます。
- Go言語の
crypto
パッケージの歴史と進化に関する情報 (Goのリリースノートやメーリングリストのアーカイブなど)- このコミットは2014年のものであり、Go言語の初期の最適化努力の一環として位置づけられます。
- SIMD命令とデータコピーの最適化に関する一般的なコンピュータアーキテクチャの知識。
copy
関数が手動ループよりも高速である理由の一つとして、コンパイラがSIMD命令を活用できる点が挙げられます。