[インデックス 10633] ファイルの概要
このコミットは、Go言語の標準ライブラリにおけるcrypto
パッケージ内の様々なハッシュ関数およびTLS (Transport Layer Security) 関連のコードにおいて、メモリ割り当てを削減し、パフォーマンスを向上させることを目的としています。具体的には、ハッシュ計算の結果を格納するための新しいスライス(動的配列)の生成を極力避け、既存のバッファを再利用する、あるいはスタック上に直接値をコピーすることで、ガベージコレクションの負荷を軽減しています。
コミット
commit 554ac03637bd855179c93d76d05b9c847571d0e2
Author: Adam Langley <agl@golang.org>
Date: Tue Dec 6 18:25:14 2011 -0500
crypto: allocate less.
The code in hash functions themselves could write directly into the
output buffer for a savings of about 50ns. But it's a little ugly so I
wasted a copy.
R=bradfitz
CC=golang-dev
https://golang.org/cl/5440111
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/554ac03637bd855179c93d76d05b9c847571d0e2
元コミット内容
このコミットの元のメッセージは以下の通りです。
crypto: allocate less.
The code in hash functions themselves could write directly into the output buffer for a savings of about 50ns. But it's a little ugly so I wasted a copy.
日本語訳:
crypto: 割り当てを減らす。
ハッシュ関数自体のコードは、出力バッファに直接書き込むことで約50ナノ秒の節約が可能だった。しかし、それは少し見苦しいので、私はコピーを無駄にした。
このメッセージは、メモリ割り当てを減らすという明確な意図を示しています。特に「コピーを無駄にした」という表現は、以前のコードが不必要にメモリをコピーしていたことを示唆しており、このコミットでその「無駄」を排除しようとしていることが読み取れます。
変更の背景
Go言語はガベージコレクション(GC)によってメモリ管理を行いますが、頻繁なメモリ割り当てはGCの頻度を増加させ、アプリケーションのパフォーマンスに影響を与える可能性があります。特に、暗号化処理のような計算負荷が高く、かつ頻繁に実行される可能性のある操作では、わずかなメモリ割り当ての削減でも全体的なスループットに大きな改善をもたらすことがあります。
このコミットは、crypto
パッケージ内のハッシュ関数(MD5, SHA1, SHA256, SHA512, RIPEMD160, HMAC)やTLSのMAC (Message Authentication Code) 計算において、ハッシュ結果を格納するためのスライス(Go言語の動的配列)の割り当てを最適化することを目的としています。以前の実装では、ハッシュ結果を返す際に常に新しいスライスを割り当てていたため、これがパフォーマンスのボトルネックとなる可能性がありました。
コミットメッセージにある「約50ナノ秒の節約」という具体的な数値は、この最適化がマイクロベンチマークレベルで測定可能な効果をもたらすことを示しており、特に高頻度で呼び出される暗号関数においては、累積的なパフォーマンス向上が期待されます。
前提知識の解説
Go言語のスライスとメモリ割り当て
Go言語のスライスは、配列への参照、長さ、容量を持つデータ構造です。append
関数は、スライスの容量が足りない場合に新しい基底配列を割り当ててデータをコピーします。make([]byte, 0, capacity)
のように容量を指定してスライスを作成すると、その容量内では再割り当てなしでappend
操作が可能です。
new(T)
は型T
のゼロ値を指すポインタを返しますが、T{}
やvar t T
のように値型を直接宣言すると、スタック上に値が割り当てられることが多く、ヒープ割り当てを避けることができます。
hash.Hash
インターフェース
Go言語のcrypto
パッケージでは、様々なハッシュ関数がhash.Hash
インターフェースを実装しています。このインターフェースには以下の主要なメソッドがあります。
Write(p []byte) (n int, err error)
: ハッシュ計算の対象となるデータを書き込みます。Sum(b []byte) []byte
: 現在のハッシュ値を計算し、それをb
に追記した新しいスライスを返します。b
がnil
の場合、新しいスライスが割り当てられます。b
に既存のスライスを渡すと、そのスライスの容量が許す限り、新しい割り当てなしでハッシュ値が追記されます。Reset()
: ハッシュの状態を初期化します。Size() int
: ハッシュ値のバイト長を返します。BlockSize() int
: ハッシュ関数のブロックサイズを返します。
TLS (Transport Layer Security) のMAC (Message Authentication Code)
TLSプロトコルでは、通信の完全性と認証を保証するためにMACが使用されます。MACは、メッセージと秘密鍵から計算される短い固定長のデータであり、メッセージが改ざんされていないこと、および送信者が秘密鍵を知っていることを検証するために使用されます。MACの計算にはハッシュ関数が利用されます。
技術的詳細
このコミットの主要な最適化は、以下の3つのパターンに集約されます。
-
new(digest)
から値のコピーへの変更: ハッシュ関数のSum
メソッド内で、digest
構造体のコピーを作成する際に、以前はd := new(digest); *d = *d0
という形式で新しいポインタとそれに対応するヒープメモリを割り当てていました。これをd := *d0
という形式に変更することで、d0
の値を直接d
にコピーし、digest
構造体自体のヒープ割り当てを回避しています。これにより、ガベージコレクションの対象となるオブジェクトが減少し、GCのオーバーヘッドが削減されます。 -
Sum(nil)
からSum(in)
またはSum(digest[:0])
への変更:hash.Hash
インターフェースのSum
メソッドは、引数として[]byte
スライスを受け取ります。このスライスにハッシュ結果を追記し、その結果を含む新しいスライスを返します。- 以前の
Sum(nil)
の呼び出しは、常にハッシュ結果を格納するための新しいスライスを割り当てていました。 - 変更後、
Sum(in)
のように既存の入力スライスを渡すことで、そのスライスの基底配列に十分な容量があれば、新しい割り当てなしでハッシュ結果が追記されます。 - また、
Sum(digest[:0])
のように、固定サイズの配列(例:var digest [Size]byte
)を基底配列とするゼロ長のスライスを渡すことで、その固定サイズの配列のメモリを再利用してハッシュ結果を格納できるようになります。これにより、ハッシュ結果を一時的に格納するための動的なスライス割り当てが不要になります。
- 以前の
-
TLS MAC計算におけるバッファの再利用:
crypto/tls
パッケージでは、MAC計算のためにmacFunction
インターフェースが定義されています。以前のMAC
メソッドは、MAC結果を返す際に新しいスライスを割り当てていました。 変更後、macFunction
インターフェースのMAC
メソッドにdigestBuf []byte
という引数が追加されました。これにより、呼び出し元はMAC結果を格納するための既存のバッファを渡すことができ、MAC
メソッドはそのバッファを再利用して結果を書き込むことができます。具体的には、halfConn
構造体にinDigestBuf
とoutDigestBuf
というフィールドが追加され、これらがMAC計算の際に再利用されるバッファとして機能します。
これらの変更は、Go言語のメモリ管理モデルとスライスの特性を最大限に活用し、ヒープ割り当てを減らすことで、ガベージコレクションの頻度と時間を削減し、全体的なパフォーマンスを向上させる効果があります。特に、暗号処理のように頻繁に実行されるコードパスでは、この種のマイクロ最適化が大きな影響を与える可能性があります。
コアとなるコードの変更箇所
このコミットは、以下のファイルにわたってメモリ割り当ての最適化を行っています。
src/pkg/crypto/hmac/hmac.go
src/pkg/crypto/md5/md5.go
src/pkg/crypto/openpgp/s2k/s2k.go
src/pkg/crypto/ripemd160/ripemd160.go
src/pkg/crypto/rsa/rsa.go
src/pkg/crypto/sha1/sha1.go
src/pkg/crypto/sha256/sha256.go
src/pkg/crypto/sha512/sha512.go
src/pkg/crypto/tls/cipher_suites.go
src/pkg/crypto/tls/conn.go
src/pkg/crypto/tls/handshake_client.go
src/pkg/crypto/tls/handshake_server.go
具体的な変更のパターンは以下の通りです。
src/pkg/crypto/md5/md5.go
(および ripemd160
, sha1
, sha256
, sha512
も同様)
変更前:
func (d0 *digest) Sum(in []byte) []byte {
d := new(digest) // ヒープ割り当て
*d = *d0
// ...
for _, s := range d.s {
in = append(in, byte(s>>0)) // バイトごとにappend、複数回再割り当ての可能性
// ...
}
return in
}
変更後:
func (d0 *digest) Sum(in []byte) []byte {
d := *d0 // 値のコピー、ヒープ割り当てなし
// ...
var digest [Size]byte // 固定サイズの配列をスタックに割り当て
for i, s := range d.s {
digest[i*4] = byte(s)
// ...
}
return append(in, digest[:]...) // 固定配列のスライスを一度にappend
}
src/pkg/crypto/hmac/hmac.go
(および openpgp/s2k
, rsa
も同様)
変更前:
func (h *hmac) Sum(in []byte) []byte {
sum := h.inner.Sum(nil) // 新しいスライスを割り当て
// ...
return h.outer.Sum(in) // 新しいスライスを割り当て
}
変更後:
func (h *hmac) Sum(in []byte) []byte {
origLen := len(in)
in = h.inner.Sum(in) // 既存のinスライスに追記、再利用の可能性
// ...
copy(h.tmp[padSize:], in[origLen:])
// ...
return h.outer.Sum(in[:origLen]) // 既存のinスライスの一部を渡し、そこに追記
}
src/pkg/crypto/tls/cipher_suites.go
変更前:
type macFunction interface {
Size() int
MAC(seq, data []byte) []byte // 新しいスライスを返す
}
func (s ssl30MAC) MAC(seq, record []byte) []byte {
// ...
digest := s.h.Sum(nil) // 新しいスライスを割り当て
// ...
return s.h.Sum(nil) // 新しいスライスを割り当て
}
変更後:
type macFunction interface {
Size() int
MAC(digestBuf, seq, data []byte) []byte // 既存のバッファを受け取る
}
func (s ssl30MAC) MAC(digestBuf, seq, record []byte) []byte {
// ...
digestBuf = s.h.Sum(digestBuf[:0]) // 既存のdigestBufを再利用
// ...
return s.h.Sum(digestBuf[:0]) // 既存のdigestBufを再利用
}
src/pkg/crypto/tls/conn.go
halfConn
構造体にinDigestBuf
とoutDigestBuf
というフィールドが追加され、MAC計算時にこれらのバッファが再利用されるようになりました。
type halfConn struct {
// ...
nextCipher interface{} // next encryption state
nextMac macFunction // next MAC algorithm
// used to save allocating a new buffer for each MAC.
inDigestBuf, outDigestBuf []byte // 新しいフィールド
}
そして、decrypt
およびencrypt
メソッド内でこれらのバッファがhc.mac.MAC
に渡されるよう変更されています。
コアとなるコードの解説
ハッシュ関数のSum
メソッドの最適化
md5.go
やsha1.go
などのハッシュ関数のSum
メソッドにおける変更は、主に以下の2点に集約されます。
-
digest
構造体のコピー方法の変更:d := new(digest); *d = *d0
というコードは、digest
型の新しいインスタンスをヒープに割り当て、そのポインタをd
に代入し、その後d0
の値を新しく割り当てられたメモリにコピーしていました。これは、digest
構造体が比較的小さい場合でもヒープ割り当てとそれに伴うGCのオーバーヘッドを発生させます。d := *d0
という変更は、d0
の値を直接d
という新しい変数にコピーします。Go言語では、このような値のコピーは通常スタック上で行われるため、ヒープ割り当てを完全に回避できます。これにより、Sum
メソッドが呼び出されるたびに発生していた一時的なヒープオブジェクトの生成がなくなります。 -
ハッシュ結果の格納方法の変更: 以前は、ハッシュ結果の各バイトをループ内で
in = append(in, byte(s>>X))
のようにin
スライスに逐次追加していました。この方法では、in
スライスの容量が不足するたびに新しい基底配列が割り当てられ、既存のデータがコピーされるという再割り当てが複数回発生する可能性がありました。 変更後、var digest [Size]byte
という固定サイズの配列を宣言し、ハッシュ結果をこの配列に直接書き込みます。この配列は通常スタック上に割り当てられます。その後、return append(in, digest[:]...)
という形で、この固定配列全体を一度にin
スライスに追記します。append
関数は、in
スライスの既存の容量が十分であれば、新しい割り当てなしでdigest
の内容を追記できます。容量が不足する場合でも、一度の再割り当てで済むため、複数回の再割り当てとコピーを避けることができます。これにより、メモリ割り当ての回数とコピーの量が削減され、パフォーマンスが向上します。
HMACおよびS2K、RSAにおけるSum
の引数変更
hmac.go
、openpgp/s2k/s2k.go
、rsa/rsa.go
における変更は、hash.Hash
インターフェースのSum
メソッドの引数にnil
ではなく既存のスライスを渡すことで、メモリ割り当てを削減しています。
h.inner.Sum(nil)
は常に新しいスライスを返しますが、h.inner.Sum(in)
とすることで、in
スライスの既存の容量を再利用してハッシュ結果を格納できる可能性が生まれます。s2k.go
やrsa.go
では、h.Sum(digest[:0])
のように、事前に宣言されたdigest
スライス(または配列を基底とするスライス)の容量を再利用しています。digest[:0]
は、digest
スライスの基底配列を共有しつつ、長さが0のスライスを作成するイディオムです。これにより、Sum
メソッドがハッシュ結果をこの既存のバッファに直接書き込むことができ、新しいスライスの割り当てが不要になります。
TLS MAC計算におけるバッファの再利用
crypto/tls
パッケージの変更は、TLSのMAC計算において、MAC結果を格納するためのバッファを再利用するメカニズムを導入しています。
macFunction
インターフェースのMAC
メソッドにdigestBuf []byte
という引数を追加することで、MAC計算の呼び出し元が結果を書き込むためのバッファを提供できるようになりました。halfConn
構造体にinDigestBuf
とoutDigestBuf
というフィールドが追加され、これらがそれぞれ受信MACと送信MACの計算結果を格納するための再利用可能なバッファとして機能します。MAC
メソッド内では、s.h.Sum(digestBuf[:0])
のように、渡されたdigestBuf
の基底配列を再利用してハッシュ結果を格納します。これにより、MACが計算されるたびに新しいスライスが割り当てられることを防ぎ、メモリ割り当てを削減します。
これらの変更は、Go言語のメモリモデルとスライスの効率的な利用を深く理解していることを示しており、特にパフォーマンスが重視される暗号ライブラリにおいて、ガベージコレクションの負荷を軽減し、実行速度を向上させるための典型的な最適化手法です。
関連リンク
- Go言語の
hash
パッケージドキュメント: https://pkg.go.dev/hash - Go言語のスライスに関する公式ブログ記事: https://go.dev/blog/slices
- Go言語のメモリ管理とガベージコレクションに関する情報 (一般的な情報源):
- Goのメモリ管理とGCの仕組み: https://go.dev/doc/gc-guide
- Goのメモリプロファイリング: https://go.dev/blog/pprof
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード (特に
src/pkg/crypto
ディレクトリ) - Go言語のスライスとメモリ管理に関する一般的な知識
- Gitのコミットログと差分表示
- TLSプロトコルに関する一般的な知識 (MACの役割など)