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

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

このコミットは、Go言語の crypto/aes パッケージにおいて、IntelおよびAMDプロセッサが提供するAES-NI (Advanced Encryption Standard New Instructions) 命令セットを活用することで、AES暗号化/復号および鍵拡張処理のパフォーマンスを大幅に向上させるものです。特に amd64 アーキテクチャ向けにアセンブリコードが追加され、ハードウェアアクセラレーションが利用可能かどうかに応じて、Go言語で実装された汎用バージョンとアセンブリ最適化バージョンが切り替えられるようになりました。

コミット

commit 948db4e0919c7ae5553f5ba727bdae1d19fbf8c0
Author: Shenghou Ma <minux.ma@gmail.com>
Date:   Thu Sep 27 01:54:10 2012 +0800

    crypto/aes: speed up using AES-NI on amd64
    
    This CL requires CL 5970055.
    
    benchmark           old ns/op    new ns/op    delta
    BenchmarkEncrypt          161           23  -85.71%
    BenchmarkDecrypt          158           24  -84.24%
    BenchmarkExpand           526           62  -88.21%
    
    benchmark            old MB/s     new MB/s  speedup
    BenchmarkEncrypt        99.32       696.19    7.01x
    BenchmarkDecrypt       100.93       641.56    6.36x
    
    R=golang-dev, bradfitz, dave, rsc
    CC=golang-dev
    https://golang.org/cl/6549055

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

https://github.com/golang/go/commit/948db4e0919c7ae5553f5ba727bdae1d19fbf8c0

元コミット内容

上記の「コミット」セクションに記載されている内容と同一です。

変更の背景

AES (Advanced Encryption Standard) は、広く利用されているブロック暗号アルゴリズムであり、多くのセキュリティプロトコルやアプリケーションの基盤となっています。しかし、ソフトウェアのみでAESを実装すると、特に大量のデータを処理する場合にCPUリソースを多く消費し、パフォーマンスのボトルネックとなることがあります。

このコミットの背景には、以下の要因があります。

  1. パフォーマンスの要求: 暗号化/復号処理は、ネットワーク通信、ディスクI/O、データベース操作など、様々な場面で頻繁に実行されます。これらの処理の高速化は、システム全体の応答性向上に直結します。
  2. AES-NIの普及: Intelは2008年、AMDは2010年頃から、AESの暗号化/復号処理をハードウェアレベルで高速化するための専用命令セットであるAES-NI (Advanced Encryption Standard New Instructions) をCPUに搭載し始めました。これにより、ソフトウェア実装に比べて桁違いのパフォーマンス向上が期待できるようになりました。
  3. Go言語の標準ライブラリの最適化: Go言語の crypto/aes パッケージは、標準でAES機能を提供していますが、初期の実装は汎用的なソフトウェアベースでした。AES-NIの普及に伴い、Go言語の標準ライブラリもこのハードウェアアクセラレーションの恩恵を受けられるように最適化する必要がありました。
  4. 既存のGoコードベースとの統合: AES-NIを導入するにあたり、既存のGoコードベースとのシームレスな統合が求められました。具体的には、AES-NIが利用可能な環境では自動的にアセンブリ最適化バージョンを使用し、利用できない環境では既存のGo言語バージョンにフォールバックする仕組みが必要です。

このコミットは、これらの背景を踏まえ、Go言語の crypto/aes パッケージがAES-NIをサポートし、パフォーマンスを劇的に向上させることを目的としています。コミットメッセージに示されているベンチマーク結果からも、その効果が明確に見て取れます。

前提知識の解説

1. AES (Advanced Encryption Standard)

AESは、米国標準技術研究所 (NIST) によって2001年に制定されたブロック暗号です。ブロック長は128ビットで固定されており、鍵長は128ビット、192ビット、256ビットのいずれかを選択できます。AESは、SPN (Substitution-Permutation Network) 構造に基づいており、複数のラウンドで構成されます。各ラウンドでは、バイト置換 (SubBytes)、行シフト (ShiftRows)、列混合 (MixColumns)、鍵加算 (AddRoundKey) の4つの変換が適用されます。

  • 鍵拡張 (Key Expansion): AESでは、元の秘密鍵から各ラウンドで使用するラウンド鍵を生成するプロセスが必要です。これは鍵拡張アルゴリズムと呼ばれ、暗号化/復号の前に一度実行されます。
  • 暗号化/復号ブロック処理: 128ビットの平文ブロックを128ビットの暗号文ブロックに変換(またはその逆)する処理です。鍵長に応じてラウンド数が異なります(128ビット鍵で10ラウンド、192ビット鍵で12ラウンド、256ビット鍵で14ラウンド)。

2. AES-NI (Advanced Encryption Standard New Instructions)

AES-NIは、IntelおよびAMDのCPUに搭載されている、AES暗号化/復号処理をハードウェアレベルで高速化するための専用命令セットです。これらの命令は、AESの各ラウンド操作(SubBytes, ShiftRows, MixColumns, AddRoundKey)や鍵拡張の一部を単一のCPUサイクルで実行できるように設計されています。

主なAES-NI命令:

  • AESENC: AES暗号化の1ラウンドを実行。
  • AESENCLAST: AES暗号化の最終ラウンドを実行。
  • AESDEC: AES復号の1ラウンドを実行。
  • AESDECLAST: AES復号の最終ラウンドを実行。
  • AESIMC: AES復号の鍵拡張で使用される逆MixColumns変換を実行。
  • AESKEYGENASSIST: AES鍵拡張の一部を高速化。

これらの命令を使用することで、ソフトウェアでこれらの操作を実装するよりもはるかに高速にAES処理を実行できます。

3. CPUID命令

CPUID命令は、x86アーキテクチャのプロセッサが自身の機能やサポートする命令セットに関する情報をソフトウェアに提供するための命令です。特定のレジスタに値を設定してCPUID命令を実行すると、プロセッサは別のレジスタにその機能に関する情報を返します。AES-NIのサポートを検出するためには、CPUID命令を使って特定のビットがセットされているかを確認します。

4. Go言語のアセンブリ (Plan 9 Assembly)

Go言語は、パフォーマンスが重要な部分でアセンブリ言語を使用することができます。Goのアセンブリは、一般的なIntel/AT&T構文とは異なるPlan 9アセンブリ構文を採用しています。Goのアセンブリファイルは .s 拡張子を持ち、Goのツールチェーンによってコンパイルされます。

  • TEXT ディレクティブ: 関数を定義します。例: TEXT ·hasAsm(SB),7,$0hasAsm という関数を定義します。
  • SB (Static Base): グローバルシンボルや外部シンボルを参照するための擬似レジスタ。
  • FP (Frame Pointer): 関数の引数やローカル変数を参照するための擬似レジスタ。
  • レジスタ: AX, BX, CX, DX など、x86-64の汎用レジスタを使用します。
  • SIMDレジスタ: X0, X1 など、SSE/AVX命令で使用される128ビットまたは256ビットのレジスタ(XMM/YMMレジスタ)を使用します。AES-NI命令はXMMレジスタを使用します。

Goのアセンブリは、Goのランタイムと密接に連携し、Goの関数呼び出し規約に従う必要があります。

5. ビルドタグ (+build)

Go言語では、ファイルの先頭に +build ディレクティブを記述することで、特定の環境(OS、アーキテクチャ、Goバージョンなど)でのみそのファイルをコンパイル対象とするように指定できます。

  • // +build amd64: このファイルは amd64 アーキテクチャでのみコンパイルされる。
  • // +build !amd64: このファイルは amd64 以外のアーキテクチャでのみコンパイルされる。

これにより、プラットフォーム固有の最適化や実装を、他のプラットフォームのビルドに影響を与えることなく提供できます。

技術的詳細

このコミットの主要な技術的詳細は、AES-NI命令をGo言語の crypto/aes パッケージに統合し、パフォーマンスを最大化するためのアプローチにあります。

1. AES-NIの検出 (hasAsm 関数)

src/pkg/crypto/aes/asm_amd64.shasAsm というアセンブリ関数が追加されました。この関数は、CPUがAES-NI命令セットをサポートしているかどうかを検出します。

  • CPUID 命令を使用し、EAXレジスタに 1 を設定して呼び出します。
  • 結果として返されるECXレジスタのビット25(AESNIビット)がセットされているかどうかを確認します。
  • セットされていれば true を、そうでなければ false を返します。

この hasAsm 関数の結果は、Goコード (cipher_asm.go) で useAsm というグローバル変数に格納され、その後の暗号化/復号/鍵拡張処理でAES-NIを使用するかどうかの判断に利用されます。

2. 条件付きコンパイルと実装の切り替え

Goのビルドタグ (+build) を利用して、AES-NI最適化バージョンと汎用Go言語バージョンを切り替える仕組みが導入されました。

  • src/pkg/crypto/aes/cipher_asm.go: // +build amd64 タグが付与されており、amd64 アーキテクチャでのみコンパイルされます。このファイルには、encryptBlock, decryptBlock, expandKey のGo関数が定義されており、内部で useAsm の値に基づいてアセンブリ実装 (encryptBlockAsm など) またはGo言語実装 (encryptBlockGo など) を呼び出します。
  • src/pkg/crypto/aes/cipher_generic.go: // +build !amd64 タグが付与されており、amd64 以外のアーキテクチャ(例: arm, 386 など)でのみコンパイルされます。このファイルでは、encryptBlock, decryptBlock, expandKey が常にGo言語実装 (encryptBlockGo など) を呼び出すように定義されています。
  • src/pkg/crypto/aes/block.go: 既存のGo言語による実装 (encryptBlock, decryptBlock, expandKey) が、それぞれ encryptBlockGo, decryptBlockGo, expandKeyGo にリネームされました。これにより、アセンブリ実装とGo言語実装の名前衝突を避け、明確に区別できるようになりました。

この構造により、開発者はプラットフォームを意識することなく crypto/aes パッケージを使用でき、Goツールチェーンが自動的に最適な実装を選択してくれます。

3. アセンブリによる高速化の実装 (asm_amd64.s)

src/pkg/crypto/aes/asm_amd64.s には、以下の主要なアセンブリ関数が実装されています。

  • encryptBlockAsm: AESの1ブロック暗号化をAES-NI命令 (AESENC, AESENCLAST) を用いて実行します。鍵長(128, 192, 256ビット)に応じて異なるラウンド数を処理します。
  • decryptBlockAsm: AESの1ブロック復号をAES-NI命令 (AESDEC, AESDECLAST, AESIMC) を用いて実行します。復号処理では、暗号化とは異なる逆変換と逆ラウンド鍵の適用が必要です。
  • expandKeyAsm: AESの鍵拡張処理をAES-NI命令 (AESKEYGENASSIST) を用いて高速化します。鍵拡張は、暗号化/復号のパフォーマンスに大きく影響するため、ここでの最適化も重要です。特に、128ビット、192ビット、256ビット鍵それぞれに対応した鍵拡張ロジックが実装されています。

これらのアセンブリ関数は、Goの関数呼び出し規約に従って引数を受け取り、結果を返します。SIMDレジスタ (XMMレジスタ) を効率的に使用し、メモリからのデータロード/ストアも最適化されています。

4. ベンチマーク結果

コミットメッセージに示されているベンチマーク結果は、この最適化の劇的な効果を明確に示しています。

ベンチマーク項目旧 ns/op新 ns/op改善率 (delta)旧 MB/s新 MB/s速度向上 (speedup)
BenchmarkEncrypt16123-85.71%99.32696.197.01x
BenchmarkDecrypt15824-84.24%100.93641.566.36x
BenchmarkExpand52662-88.21%---
  • ns/op (nanoseconds per operation): 1回の操作にかかる時間。値が小さいほど高速。
  • MB/s (megabytes per second): 1秒あたりに処理できるデータ量。値が大きいほど高速。

暗号化、復号、鍵拡張のいずれも、操作あたりの時間が84%〜88%削減され、スループットは6倍〜7倍に向上しています。これは、ソフトウェア実装からハードウェアアクセラレーションへの移行による典型的なパフォーマンスゲインであり、AES-NIの導入が非常に効果的であったことを示しています。

5. 依存関係 (This CL requires CL 5970055.)

コミットメッセージには「This CL requires CL 5970055.」と記載されています。これは、この変更リスト (Change List) が、別の変更リスト 5970055 に依存していることを意味します。Goのコードレビューシステム (Gerrit) における変更リストのIDです。通常、このような依存関係は、共通の基盤変更やAPIの変更が先行して適用されている必要があることを示唆します。この場合、おそらくAES-NIをサポートするためのGoランタイムやツールチェーンの変更、あるいは crypto/aes パッケージ内の他の準備作業が先行して行われたと考えられます。

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

このコミットでは、主に以下のファイルが変更または新規追加されています。

  1. src/pkg/crypto/aes/aes_test.go:

    • expandKey のテストが expandKeyGo を呼び出すように変更されました。これは、アセンブリバージョンの expandKeyAsm がGoバージョンとは異なる内部メモリレイアウトを使用する可能性があり、テストがGoバージョンに特化しているためです。外部に公開されない内部実装の詳細であるため、この変更は問題ありません。
  2. src/pkg/crypto/aes/asm_amd64.s (新規追加):

    • amd64 アーキテクチャ向けのアセンブリ実装ファイル。
    • hasAsm(): CPUがAES-NIをサポートしているか検出する関数。
    • encryptBlockAsm(): AESブロック暗号化のAES-NIアセンブリ実装。
    • decryptBlockAsm(): AESブロック復号のAES-NIアセンブリ実装。
    • expandKeyAsm(): AES鍵拡張のAES-NIアセンブリ実装。
    • _expand_key_128<>, _expand_key_192a<>, _expand_key_192b<>, _expand_key_256a<>, _expand_key_256b<>: 鍵拡張の内部ヘルパー関数。
  3. src/pkg/crypto/aes/block.go:

    • 既存のGo言語による実装関数がリネームされました。
      • encryptBlock -> encryptBlockGo
      • decryptBlock -> decryptBlockGo
      • expandKey -> expandKeyGo
    • これにより、アセンブリ実装とGo言語実装の名前が衝突せず、cipher_asm.gocipher_generic.go から適切に呼び分けられるようになりました。
  4. src/pkg/crypto/aes/cipher.go:

    • Encrypt および Decrypt メソッドの呼び出しが、それぞれ encryptBlock(c.enc, dst, src)decryptBlock(c.dec, dst, src) に変更されました。これは、cipher_asm.go または cipher_generic.go で定義された encryptBlock および decryptBlock 関数(内部でGo実装またはアセンブリ実装を呼び出すラッパー)を指します。
  5. src/pkg/crypto/aes/cipher_asm.go (新規追加):

    • amd64 アーキテクチャ専用のファイル。
    • hasAsm(), encryptBlockAsm(), decryptBlockAsm(), expandKeyAsm() の外部アセンブリ関数を宣言。
    • useAsm 変数 (hasAsm() の結果を格納) を定義。
    • encryptBlock(), decryptBlock(), expandKey() のGo関数を定義。これらの関数は useAsm の値に基づいて、アセンブリ実装 (*Asm) またはGo言語実装 (*Go) のどちらかを呼び出すロジックを含みます。
  6. src/pkg/crypto/aes/cipher_generic.go (新規追加):

    • amd64 以外のアーキテクチャ専用のファイル。
    • encryptBlock(), decryptBlock(), expandKey() のGo関数を定義。これらの関数は常にGo言語実装 (*Go) を呼び出します。

コアとなるコードの解説

src/pkg/crypto/aes/asm_amd64.s

このファイルは、AES-NI命令を直接使用してAESの主要な操作を実装しています。

  • TEXT ·hasAsm(SB),7,$0:

    • CPUID 命令を実行し、CPUの機能を問い合わせます。
    • SHRQ $25, CXANDQ $1, CX で、ECXレジスタのビット25(AESNIフラグ)を抽出します。
    • 結果を ret+0(FP) (戻り値) に格納し、RET で関数を終了します。
  • TEXT ·encryptBlockAsm(SB),7,$0:

    • nr (ラウンド数), xk (拡張鍵), dst (出力バッファ), src (入力バッファ) を引数として受け取ります。
    • MOVUPS で入力ブロックと最初のラウンド鍵をXMMレジスタにロードし、PXOR でXORします。
    • AESENC 命令を繰り返し使用して、各ラウンドの暗号化を実行します。
    • 最終ラウンドは AESENCLAST 命令を使用します。
    • 結果を MOVUPS X0, 0(DX) で出力バッファに書き込みます。
    • 鍵長(128, 192, 256ビット)に応じたラウンド数の分岐 (JE Lenc196, JB Lenc128) が含まれています。
  • TEXT ·decryptBlockAsm(SB),7,$0:

    • encryptBlockAsm と同様に引数を受け取ります。
    • AESDEC 命令を繰り返し使用して、各ラウンドの復号を実行します。
    • 最終ラウンドは AESDECLAST 命令を使用します。
    • 復号の鍵拡張には AESIMC 命令が使用されます。
    • 鍵長に応じたラウンド数の分岐も含まれています。
  • TEXT ·expandKeyAsm(SB),7,$0:

    • nr (ラウンド数), key (元の鍵), enc (暗号化用拡張鍵), dec (復号用拡張鍵) を引数として受け取ります。
    • AESKEYGENASSIST 命令を使用して、鍵拡張の各ステップを高速化します。
    • 鍵長(128, 192, 256ビット)に応じた異なる鍵拡張ロジック (Lexp_enc128, Lexp_enc196, Lexp_enc256) が実装されています。
    • _expand_key_128<>, _expand_key_192a<>, _expand_key_192b<>, _expand_key_256a<>, _expand_key_256b<> といった内部ヘルパー関数が、鍵拡張の具体的なステップを処理します。これらは、鍵の回転、XOR、S-boxルックアップなどの操作を効率的に行います。
    • 復号用の拡張鍵は、暗号化用の拡張鍵から AESIMC を用いて生成されます。

src/pkg/crypto/aes/cipher_asm.go

このファイルは、amd64 環境でGoの crypto/aes パッケージがどのようにアセンブリ実装とGo実装を切り替えるかを制御します。

// +build amd64

package aes

// defined in asm_$GOARCH.s
func hasAsm() bool
func encryptBlockAsm(nr int, xk *uint32, dst, src *byte)
func decryptBlockAsm(nr int, xk *uint32, dst, src *byte)
func expandKeyAsm(nr int, key *byte, enc *uint32, dec *uint32)

var useAsm = hasAsm() // プログラム起動時にAES-NIサポートを検出

func encryptBlock(xk []uint32, dst, src []byte) {
	if useAsm { // AES-NIが利用可能ならアセンブリ実装を呼び出す
		encryptBlockAsm(len(xk)/4-1, &xk[0], &dst[0], &src[0])
	} else { // そうでなければGo言語実装を呼び出す
		encryptBlockGo(xk, dst, src)
	}
}
// decryptBlock, expandKey も同様のロジック
  • hasAsm() の結果を useAsm にキャッシュすることで、毎回CPUID命令を実行するオーバーヘッドを避けています。
  • encryptBlock, decryptBlock, expandKey の各関数は、useAsm の値に基づいて、対応するアセンブリ関数 (*Asm) またはGo言語関数 (*Go) を呼び出します。これにより、ユーザーは透過的に最適なパフォーマンスを得ることができます。
  • expandKey 関数では、鍵長に応じてラウンド数を計算し、アセンブリ関数に渡しています。

src/pkg/crypto/aes/cipher_generic.go

このファイルは、amd64 以外の環境でGoの crypto/aes パッケージが常にGo言語実装を使用するように制御します。

// +build !amd64

package aes

func encryptBlock(xk []uint32, dst, src []byte) {
	encryptBlockGo(xk, dst, src) // 常にGo言語実装を呼び出す
}

func decryptBlock(xk []uint32, dst, src []byte) {
	decryptBlockGo(xk, dst, src) // 常にGo言語実装を呼び出す
}

func expandKey(key []byte, enc, dec []uint32) {
	expandKeyGo(key, enc, dec) // 常にGo言語実装を呼び出す
}
  • !amd64 ビルドタグにより、このファイルは amd64 以外のすべてのアーキテクチャでコンパイルされます。
  • これらの関数は、useAsm のようなチェックを行わず、直接 *Go バージョンの関数を呼び出します。これにより、非 amd64 環境でのビルドと実行がシンプルに保たれます。

これらの変更により、Go言語の crypto/aes パッケージは、AES-NIをサポートするハードウェア上で自動的にその恩恵を受け、暗号化処理のパフォーマンスを大幅に向上させることが可能になりました。

関連リンク

参考にした情報源リンク