真・SSEを使って8flops/clockを実現する
カーネル/VM Advent Calendar の34日目として書きます.
記事の内容自体はこのイベントと関係ありませんので,どなたでもお楽しみ下さいませ.
今回は 自分の前回の記事 で見つけた誤りを訂正しつつ,
Intelの一部CPUがサポートするSSE(Streaming SIMD Extensions)命令により1clockで8個の単精度浮動小数点演算を行なう方法を紹介します.
なるべく前回の記事とは独立した記述を心がけます.
- 関連した記事
- 使用したCPU
- 乗算と加算を並列に行なって 8flops/clock を実現
- 8flops/clock を出すための制約
- ロード,シャッフル命令を使いつつも8flops/clock近く出るコード
- すぐに実験できるコードと実験方法
- 前回の記事の欠陥
- 測定環境についての注意
関連した記事
@takeoka さんが,カーネル/VM Advent Calendarの24日目の記事として,AVXの効き目について書かれています.
SSEが128bitレジスタを使って4つの単精度浮動小数をパックするのに対し,AVXは256bitで8個パックできます.
とはいえ,まだまだAVX命令はないけどSSE命令を積んでいるCPUも現役ですので,この記事も多少お役に立てるかと思います.
使用したCPU
この記事の内容は,
- Intel Xeon CPU E7540 @ 2.00GHz
- Intel Core2 Duo CPU P9400 @ 2.40GHz
で調査・実験しています.
それ以外の環境では,記述が当てはまらないことも考えられます.
もしお気づきの点がございましたらコメント頂けたら嬉しいです.
乗算と加算を並列に行なって 8flops/clock を実現
SSEでは,128-bitのxmmレジスタを使って計算を進めます.
例えば,
1 | float a[4], b[4]; |
という計算であれば,
- a[0]をxmm0番レジスタの0-31bit目に, … , a[3]をxmm0番レジスタの96-127bit目に格納
- b[0]をxmm1番レジスタの0-31bit目に, … , b[3]をxmm1番レジスタの96-127bit目に格納
- xmm1 += xmm0
とします.この xmm1 += xmm0 が1clockでできてしまうのがSSEの強みです.
さて,SSEでは掛算と足し算は同時にできてしまいます.
つまり,
1 | xmm1 += xmm0; 4つの単精度浮動小数の足し算 |
が1clockでできるので,8flops/clockが実現できます.
(※ flops: FLoating-point OPerationS の意で使っています.
似ていますがFLOPSと大文字で書くときは,FLoating-point Operations Per Secondの意ですので,
ごっちゃにしないように注意してください.)
SSEの実際の命令を使って掛算と足し算を表現すると,
1 | addps %xmm0, %xmm1 |
と書けます.
(この記事でのアセンブリの表記は,GCCのインラインアセンブラと同じです.
“Op Reg1, Reg2” だと,結果は Reg2 に格納されると考えてください.)
8flops/clock を出すための制約
The Architecture of the Nehalem Processor and Nehalem-EP SMP Platforms (PDF)
の14ページ目に,Nehalemアーキテクチャにおけるアウトオブオーダ実行の概要が図示されています.
これによると,SSE関係の命令については,
- ロード/ストア
- 足し算
- 掛算
- シャッフル
の4種類の命令は1clockで行えるようです.
実際のアプリケーションでSSE命令を使うとなると,xmmレジスタとメモリのやりとりでロード/ストア命令は使いますし,
xmmレジスタ内部の浮動小数の並び順を入れ替えるシャッフル命令がほしくなるときもあります.
SSEで高速な処理を記述するには,まず1clockで同時に行える命令を明確にしましょう.
同時に実行できる演算の種類が限られているのは演算器の都合でしょうが,レジスタの都合に起因する制約もあります.
例えば,
1 | addps %xmm0, %xmm8 |
のようなコードで,同じレジスタに同時に値を書き込むことはできません.
その一方で,
1 | addps %xmm8, %xmm0 |
のようなコードで,同じレジスタから同時に値を読み出すことはできます.
この辺りの詳しい条件は筆者は把握していません.
ロード,シャッフル命令を使いつつも8flops/clock近く出るコード
(この記事の終わりに,コンパイルしてすぐに実行できるフルのコードも掲載します)
実際に,
- ロード
- 足し算
- 掛算
- シャッフル
を1clock(近く)で行なうコードを書いてみます.
ざっくり読む上での注意点も述べておきます.
- movupsは(今回の使い方では)メモリからxmmレジスタに値をロードする命令
- shufpsはxmmレジスタの値をシャッフルする命令
- print_GFLOPS, print_throughput は計測結果を出力する関数
- rdtsc() は,CPUのRDTSC命令を用いて,(ほぼ)CPUクロックベースで時間を計測するための関数
- mulps SRC, DEST で,ソースにメモリ上のデータを使う場合,そのデータは16バイト境界にアラインされている必要がある
1 | void |
xmmレジスタが8本しかないCPUではコンパイルエラーになりますので,xmm8 - xmm15を適宜置き換えてください.
すぐに実験できるコードと実験方法
以下のコードをコピペし, “sse_8flops_per_clock.c” など適当なファイルを作ってください.
コンパイル方法は,
1 | $ gcc -O3 -funroll-loops sse_8flops_per_clock.c -o sse_8flops_per_clock |
辺りだと性能が出るはずです.
特にループ・アンローリングのための “-funroll-loops” は性能に直結します.
実行すると,8flops/clock程度出ていることが確認できるかと思います.
1 | $ ./sse_8flops_per_clock |
1 |
|
前回の記事の欠陥
前回の記事では,足し算と掛算を1clockで行なうための条件を「フォワーディングが重要です(どやぁ」と宣っていたのですが,
色々コードを書いてみたり The Architecture of the Nehalem Processor and Nehalem-EP SMP Platforms を
ざっと見た感じ,もっとアーキテクチャに根ざした複雑な問題なようです.
測定環境についての注意
測定は,RDTSC命令を用いて行っています.
この命令で読み取るカウンタは,CPUの通常動作時の速度で更新されます(自分はそう理解しています).
従って,CPUの動作周波数を動的に変化させる DVFS (Dynamic Voltage and Frequency Scaling) や TurboBoost が有効になっているマシンだと,正確な測定はできません(8flops/clockを超えるように見えたりする).
どちらもBIOSでオフにできる機能なはずですので,正確な測定環境を求める方はご検討ください.