[インデックス 16332] ファイルの概要
このコミットは、Go言語のランタイムにおけるmemmove
(Goの組み込み関数copy()
が内部的に利用するメモリ移動処理)の実装を、x86およびx86-64アーキテクチャ向けに高速化することを目的としています。具体的には、アセンブリコードレベルで最適化を行い、特に小規模なメモリコピーにおいてパフォーマンスを大幅に改善しています。また、この変更に伴い、memmove
の正確性とパフォーマンスを検証するための新しいテストファイルが追加されています。
コミット
commit 6021449236c8ef46a6c78518470d0355b56943f3
Author: Keith Randall <khr@golang.org>
Date: Fri May 17 12:53:49 2013 -0700
runtime: faster x86 memmove (a.k.a. built-in copy())
REP instructions have a high startup cost, so we handle small
sizes with some straightline code. The REP MOVSx instructions
are really fast for large sizes. The cutover is approximately
1K. We implement up to 128/256 because that is the maximum
SSE register load (loading all data into registers before any
stores lets us ignore copy direction).
(on a Sandy Bridge E5-1650 @ 3.20GHz)
benchmark old ns/op new ns/op delta
BenchmarkMemmove0 3 3 +0.86%
BenchmarkMemmove1 5 5 +5.40%
BenchmarkMemmove2 18 8 -56.84%
BenchmarkMemmove3 18 7 -58.45%
BenchmarkMemmove4 36 7 -78.63%
BenchmarkMemmove5 36 8 -77.91%
BenchmarkMemmove6 36 8 -77.76%
BenchmarkMemmove7 36 8 -77.82%
BenchmarkMemmove8 18 8 -56.33%
BenchmarkMemmove9 18 7 -58.34%
BenchmarkMemmove10 18 7 -58.34%
BenchmarkMemmove11 18 7 -58.45%
BenchmarkMemmove12 36 7 -78.51%
BenchmarkMemmove13 36 7 -78.48%
BenchmarkMemmove14 36 7 -78.56%
BenchmarkMemmove15 36 7 -78.56%
BenchmarkMemmove16 18 7 -58.24%
BenchmarkMemmove32 18 8 -54.33%
BenchmarkMemmove64 18 8 -53.37%
BenchmarkMemmove128 20 9 -55.93%
BenchmarkMemmove256 25 11 -55.16%
BenchmarkMemmove512 33 33 -1.19%
BenchmarkMemmove1024 43 44 +2.06%
BenchmarkMemmove2048 61 61 +0.16%
BenchmarkMemmove4096 95 95 +0.00%
R=golang-dev, bradfitz, remyoudompheng, khr, iant, dominik.honnef
CC=golang-dev
https://golang.org/cl/9038048
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/6021449236c8ef46a6c78518470d0355b56943f3
元コミット内容
Go言語のランタイムにおけるx86およびx86-64アーキテクチャ向けのmemmove
(Goの組み込み関数copy()
が内部的に利用)の実装を高速化する。
REP
命令は起動コストが高いため、小規模なデータサイズに対しては直線的な(ストレートライン)コードで処理する。REP MOVSx
命令は大規模なデータサイズに対して非常に高速である。この切り替えの閾値は約1KBである。最大128バイト(x86)または256バイト(x86-64)までのコピーを実装しているのは、これがSSEレジスタにロードできる最大量であり、全てのデータをレジスタにロードしてからストアすることで、コピー方向(順方向か逆方向か)を考慮する必要がなくなるためである。
ベンチマーク結果(Sandy Bridge E5-1650 @ 3.20GHz上)では、特に小規模なコピーにおいて大幅な性能改善が見られる。例えば、2バイトから256バイトの範囲で50%以上の高速化が達成されている。
変更の背景
Go言語のランタイムにおいて、メモリコピー操作は非常に頻繁に行われる基本的な処理です。Goの組み込み関数であるcopy()
は、内部的にこのmemmove
関数を呼び出しています。したがって、memmove
の性能は、Goプログラム全体のパフォーマンスに直接的な影響を与えます。
従来のmemmove
実装では、Intel/AMDプロセッサが提供するREP MOVSx
(Repeat String Operation)命令が主に利用されていました。この命令は、大量のデータを効率的にコピーするために設計されており、非常に高速です。しかし、コミットメッセージにもあるように、REP
命令には「高い起動コスト(high startup cost)」という特性があります。これは、コピーするデータサイズが小さい場合、REP
命令のセットアップにかかるオーバーヘッドが、実際のデータコピーにかかる時間よりも大きくなってしまい、結果として非効率になることを意味します。
この問題に対処するため、本コミットでは、小規模なメモリコピーに対してはREP
命令を使用せず、代わりに「直線的なコード(straightline code)」、つまり、特定のサイズに最適化された一連のロード(MOV
)およびストア(MOV
)命令を直接記述することで、オーバーヘッドを削減し、パフォーマンスを向上させることを目指しました。これにより、Goプログラムが頻繁に行う小規模なメモリコピー操作が高速化され、全体的な実行速度の向上が期待されます。
前提知識の解説
1. memmove
関数
memmove
は、C言語の標準ライブラリ関数の一つで、メモリブロックをコピーするために使用されます。memcpy
と似ていますが、memmove
はコピー元とコピー先のメモリ領域が重なっている場合でも正しく動作することが保証されています。Go言語の組み込み関数copy()
は、このmemmove
と同様のセマンティクスを持ち、内部的にはランタイムのmemmove
実装を呼び出します。
2. x86/x86-64アセンブリ言語
このコミットで変更されているファイルは.s
拡張子を持つアセンブリ言語のソースファイルです。Go言語のランタイムは、パフォーマンスが重要な部分やハードウェアに密接に関わる部分でアセンブリ言語を使用することがあります。
_386.s
: 32ビットx86アーキテクチャ向けのアセンブリコード。_amd64.s
: 64ビットx86-64アーキテクチャ向けのアセンブリコード。
3. REP MOVSx
命令
Intel/AMDプロセッサには、文字列操作を高速化するための特別な命令群があります。MOVSx
(Move String)命令は、メモリからメモリへデータを移動させます。
MOVSB
: 1バイト単位で移動。MOVSW
: 2バイト(ワード)単位で移動。MOVSL
: 4バイト(ダブルワード)単位で移動。MOVSQ
: 8バイト(クアッドワード)単位で移動(x86-64のみ)。
これらの命令の前にREP
プレフィックスを付けると、CX
(またはRCX
)レジスタに指定された回数だけMOVSx
命令を繰り返します。これにより、ループ処理をCPU内部で効率的に実行でき、ソフトウェアによるループよりも高速になります。しかし、REP
命令自体をセットアップするための内部的なオーバーヘッドが存在します。
4. SSE/SSE2 (Streaming SIMD Extensions)
SSEは、Intelが開発したSIMD(Single Instruction, Multiple Data)命令セットの拡張機能です。複数のデータを単一の命令で並行処理することで、浮動小数点演算やメディア処理などのパフォーマンスを向上させます。
- SSE2: SSEの拡張版で、整数演算にも対応し、128ビットのXMMレジスタ(X0からX15)を導入しました。これらのレジスタは、16バイト(128ビット)のデータを一度にロード・ストアするのに使用できます。
MOVOU
(Move Unaligned Oword): SSE命令の一つで、メモリからXMMレジスタへ、またはXMMレジスタからメモリへ、128ビット(16バイト)のデータを移動させます。アラインメントされていないメモリにも対応しています。
memmove
のようなメモリコピーでは、これらのSIMD命令を利用して一度に大量のデータを処理することで、効率を大幅に向上させることができます。特に、コピー元とコピー先が重なっていない場合、データをレジスタに一括でロードし、その後一括でストアすることで、コピー方向を気にすることなく処理を進めることが可能になります。
5. Goのcopy()
組み込み関数
Go言語には、スライス間で要素をコピーするための組み込み関数copy(dst, src []Type)
があります。この関数は、src
スライスからdst
スライスへ要素をコピーし、コピーされた要素の数を返します。内部的には、Goランタイムのmemmove
実装が呼び出されます。したがって、memmove
の最適化は、Goプログラムでcopy()
を使用する際のパフォーマンスに直接貢献します。
技術的詳細
このコミットの主要な技術的アプローチは、メモリコピーのサイズに応じて異なる最適化戦略を適用する「ハイブリッドアプローチ」です。
-
小規模コピーの最適化(ストレートラインコード):
REP
命令の起動コストを回避するため、非常に小さいサイズ(1バイトから128バイト/256バイト)のコピーに対しては、アセンブリ言語で直接、バイト、ワード、ダブルワード、クアッドワード、またはSSEレジスタ(XMMレジスタ)を使ったロード/ストア命令のシーケンスを記述しています。- 例えば、
move_1or2
、move_3or4
、move_5through8
、move_9through16
といったラベルにジャンプし、それぞれのサイズに特化した効率的なコピー処理を行います。 - SSE2が利用可能な環境では、
MOVOU
命令(Move Unaligned Oword)を使用して、16バイト単位でデータをXMMレジスタにロードし、その後ストアします。これにより、一度に大量のデータを処理できるため、ループのオーバーヘッドを削減し、キャッシュ効率も向上させます。特に、コピー方向(順方向か逆方向か)を考慮する必要がなくなるという利点があります。これは、全てのデータをレジスタに読み込んでから書き出すため、メモリの重なりによる問題が発生しないためです。
-
大規模コピーの最適化(
REP MOVSx
命令):- コピーサイズが約1KB(1024バイト)を超える場合、
REP MOVSx
命令の効率が最大化されます。このサイズでは、REP
命令の起動コストは全体の処理時間に比べて無視できるほど小さくなります。 - したがって、この閾値を超えた場合は、引き続き
REP MOVSL
(32ビット)またはREP MOVSQ
(64ビット)命令を使用して高速なブロックコピーを行います。
- コピーサイズが約1KB(1024バイト)を超える場合、
-
切り替えの閾値:
- コミットメッセージによると、ストレートラインコードと
REP
命令の切り替えの閾値は「約1KB」とされています。これは、ベンチマーク結果からも裏付けられており、BenchmarkMemmove512
までは改善が見られるものの、BenchmarkMemmove1024
以降は改善がほとんど見られないか、わずかに悪化していることから、このあたりで切り替えていることがわかります。
- コミットメッセージによると、ストレートラインコードと
-
ベンチマーク結果の分析:
- コミットメッセージに記載されているベンチマーク結果は、この最適化の効果を明確に示しています。
BenchmarkMemmove2
からBenchmarkMemmove256
までの範囲で、旧バージョンと比較して50%から78%という大幅な性能改善が達成されています。これは、小規模なコピーに対するストレートラインコードとSSE命令の導入が成功したことを示しています。BenchmarkMemmove0
とBenchmarkMemmove1
のような非常に小さいサイズでは、改善幅は小さいですが、これは既に非常に高速であるため、これ以上の最適化が難しいことを示唆しています。BenchmarkMemmove512
以降の大きなサイズでは、性能改善がほとんど見られません。これは、これらのサイズでは既にREP MOVSx
命令が効率的に機能しており、今回の最適化の対象外であるか、あるいはその効果が相対的に小さいためと考えられます。
このハイブリッドアプローチにより、Goのcopy()
関数は、あらゆるサイズのメモリコピーにおいて、より効率的に動作するようになりました。
コアとなるコードの変更箇所
このコミットでは、以下の3つのファイルが変更されています。
-
src/pkg/runtime/memmove_386.s
: 32ビットx86アーキテクチャ向けのmemmove
アセンブリ実装。- 小規模なコピーを処理するための新しい分岐ロジック(
tail
ラベルとそれに続くCMPL
/JBE
命令群)が追加されました。 move_1or2
、move_3or4
、move_5through8
、move_9through16
、move_17through32
、move_33through64
、move_65through128
といった新しいラベルと、それぞれのサイズに特化したストレートラインコピーコードが追加されました。- 特に、SSE2が利用可能な場合に
MOVOU
命令を使用するセクションが追加されています。 - 既存の
REP MOVSL
の後に、残りのバイトを処理するためのREP MOVSB
の呼び出しが削除され、代わりに新しいtail
ロジックにジャンプするように変更されました。
- 小規模なコピーを処理するための新しい分岐ロジック(
-
src/pkg/runtime/memmove_amd64.s
: 64ビットx86-64アーキテクチャ向けのmemmove
アセンブリ実装。memmove_386.s
と同様に、小規模なコピーを処理するための新しい分岐ロジック(tail
ラベルとそれに続くCMPQ
/JBE
命令群)が追加されました。move_1or2
からmove_129through256
までの新しいラベルと、それぞれのサイズに特化したストレートラインコピーコードが追加されました。MOVOU
命令を使用したSSE最適化が、より大きなサイズ(最大256バイト)まで拡張されています。- 既存の
REP MOVSQ
の後に、残りのバイトを処理するためのREP MOVSB
の呼び出しが削除され、代わりに新しいtail
ロジックにジャンプするように変更されました。
-
src/pkg/runtime/memmove_test.go
:memmove
関数の正確性とパフォーマンスを検証するための新しいGoテストファイル。TestMemmove
: さまざまなサイズとオフセットでcopy()
関数をテストし、コピーが正しく行われることを検証します。特に、コピー元とコピー先が重ならないケースを網羅しています。TestMemmoveAlias
: コピー元とコピー先が重なる(エイリアシング)ケースでcopy()
関数をテストし、memmove
のセマンティクスが正しく実装されていることを検証します。bmMemmove
: ベンチマークヘルパー関数。BenchmarkMemmoveX
: 0バイトから4096バイトまでの様々なサイズのメモリコピーのパフォーマンスを測定するためのベンチマーク関数群。コミットメッセージに記載されているベンチマーク結果は、これらの関数によって生成されたものです。
コアとなるコードの解説
src/pkg/runtime/memmove_386.s
および src/pkg/runtime/memmove_amd64.s
これらのファイルは、Goのアセンブリ言語(Plan 9アセンブラ)で書かれています。基本的な構造は、コピー元(SI
レジスタ)、コピー先(DI
レジスタ)、コピーサイズ(BX
レジスタ)を受け取り、メモリコピーを実行するというものです。
変更の核心は、tail
ラベルから始まる新しい分岐ロジックです。
tail:
TESTL BX, BX // 32-bit: TESTL, 64-bit: TESTQ
JEQ move_0 // サイズが0なら即座に終了
CMPL BX, $2 // 32-bit: CMPL, 64-bit: CMPQ
JBE move_1or2 // サイズが2以下ならmove_1or2へ
CMPL BX, $4
JBE move_3or4 // サイズが4以下ならmove_3or4へ
// ... (同様の分岐が続く)
このコードは、コピーサイズBX
を段階的に比較し、適切なサイズのハンドリングルーチンにジャンプします。
move_0
: サイズが0の場合、何もせずにRET
(リターン)します。move_1or2
: 1バイトまたは2バイトのコピーを処理します。MOVB (SI), AX
/MOVB -1(SI)(BX*1), CX
: コピー元からバイトをロード。BX*1
はサイズに応じてオフセットを調整し、重なりを考慮したコピーを可能にします。MOVB AX, (DI)
/MOVB CX, -1(DI)(BX*1)
: コピー先へバイトをストア。
move_3or4
: 3バイトまたは4バイトのコピーを処理します。ワード(2バイト)単位のMOVW
命令を使用します。move_5through8
: 5バイトから8バイトのコピーを処理します。ダブルワード(4バイト)単位のMOVL
命令を使用します。move_9through16
: 9バイトから16バイトのコピーを処理します。複数のMOVL
命令を使用します。move_17through32
(x86/x86-64),move_33through64
,move_65through128
(x86/x86-64),move_129through256
(x86-64):- これらのルーチンでは、SSE2命令である
MOVOU
が活用されます。 MOVOU (SI), X0
: コピー元から16バイト(128ビット)をXMMレジスタX0
にロードします。MOVOU X0, (DI)
:X0
の内容をコピー先へ16バイトストアします。- 複数の
MOVOU
命令を組み合わせることで、より大きなブロックを効率的にコピーします。例えば、move_65through128
では8つのXMMレジスタ(X0-X7)を使用し、合計128バイトを一度に処理します。 - 重要なのは、これらのルーチンでは全てのデータをレジスタにロードしてからストアするため、コピー元とコピー先が重なっていても、コピー方向を考慮する必要がない点です。これは、
memmove
の重なり対応の要件を満たしつつ、パフォーマンスを向上させるための鍵となります。
- これらのルーチンでは、SSE2命令である
既存のREP MOVSx
命令を使用するパスでは、コピー後に残ったバイトを処理するために、以前はREP MOVSB
にフォールバックしていましたが、今回の変更でその部分が削除され、代わりにtail
ラベルにジャンプするように変更されました。これにより、残りのバイトも新しい最適化されたストレートラインコードで処理されるようになります。
src/pkg/runtime/memmove_test.go
この新しいテストファイルは、memmove
の正確性と性能を保証するために非常に重要です。
TestMemmove
とTestMemmoveAlias
は、様々なサイズのデータ、様々なオフセット、そして重なり合うメモリ領域でのコピーが、期待通りに動作するかを検証します。これは、アセンブリコードの変更が正しい結果を生成することを保証するための機能テストです。BenchmarkMemmoveX
関数群は、Goの標準ベンチマークフレームワーク(testing
パッケージ)を使用して、異なるサイズのメモリコピー操作の実行時間を測定します。これにより、最適化の効果を定量的に評価し、回帰がないことを確認できます。b.SetBytes(int64(n))
は、ベンチマーク結果に「bytes/op」の統計を追加するために使用され、コピーされたバイト数あたりの操作時間をより明確に示します。
これらのテストとベンチマークは、ランタイムの低レベルな変更が、Go言語のユーザーに提供されるcopy()
関数の安定性とパフォーマンスに悪影響を与えないことを確認するための重要な安全網となります。
関連リンク
- Go CL 9038048: https://golang.org/cl/9038048
参考にした情報源リンク
- Intel 64 and IA-32 Architectures Software Developer's Manuals (特にVolume 2A: Instruction Set Reference, A-M):
REP
,MOVSx
,MOVOU
命令の詳細について。 - Go言語の
testing
パッケージドキュメント: ベンチマークの書き方について。 - Go言語のソースコード(runtimeパッケージ):
memmove
の実装と関連するアセンブリコードの慣習について。