14. 14/76
計算機の仕組みと高速化
高速化とは何か?
ここでは「アルゴリズムを変えずに、実装方法によって
実行時間を短くする方法」と定義
→ アルゴリズムレベルの最適化は終了していることを想定
遅いアルゴリズムを実装で高速化しても意味がないので、
十分にアルゴリズムレベルで最適化が済んでいることを確認すること
実装レベルの高速化とは何か?
「コンパイラや計算機にやさしいコードを書く事」
※やさしい=理解しやすい、向いている
そのためには、計算機の仕組みを理解しておかなければならない
University of Tokyo
15. 15/76
プログラム階層
ノード間通信
(並列化)
Network
メモリ転送
(キャッシュ最適化)
Memory Memory Memory
CPUコア
(チューニング) Cache Cache Cache
SIMD化、if分岐削除
CPU CPU CPU
この中で最も重要なのはメモリ転送
University of Tokyo
16. 16/76
記憶階層
Register
FPU
Register
Register
L1 L2 Memory
FPU Register
Floating Processing Unit
CPU
・計算はレジスタ上で行う
・レジスタにのせられた情報はFPUに渡され、レジスタにかえってくる
・メモリアクセスで大事なのは、バンド幅とレイテンシ
Latency
CPUにデータを要求されてから、レジスタに届くまでのサイクル数
・L1キャッシュで数サイクル、L2で10 20サイクル、主記憶は 数百サイクル
Bandwidth
CPUにデータが届き始めてからのデータ転送速度
絶対値(GB/s)よりも、計算速度との比(B/F)の方が良く使われる
University of Tokyo
18. 18/76
仮想メモリとページング (1/2)
Virtual Memory
現在の一般的なOSでは、メモリをページと呼ばれる仮想記憶で管理する。
ハードディスク
仮想メモリ 物理メモリ
(論理メモリ)
連続配列が、物理メモリ内
では不連続に割り当てら
れてるかもしれない
一部がハードディスクにスワップさ
れているかもしれない
仮想メモリを使う理由
・不連続なメモリ空間を論理的には連続に見せることができる
仮想メモリなしでは、メモリに余裕があっても連続領域が取れない場合にメモリ割り当てができない
・スワッピングが可能になる
記憶容量としてハードディスクも使えるため、物理メモリより広い論理 メモリ空間が取れる。
論理メモリと実メモリの対応が書いてあるのがTLB (テキスト参照)
University of Tokyo
19. 19/76
仮想メモリとページング (2/2)
数値計算で何が問題になるか?
F90のallocateや、Cでnew、mallocされた時点では物理メモリの
割り当てがされていない場合がある
real*8, allocatable :: work(:)
allocate (work(10000)) この時点では予約だけされて、まだ物理アドレスが割り当てられない
do i=1, 10000
work(i) = i はじめて触った時点で、アドレスがページ単位で割り当てられる
end do
(First touch の原則)
よくある問題:
最初に馬鹿でかい配列を動的に確保しているプログラムの初期化がすごく遅い
地球シミュレータからT2Kへの移植で問題になることが多い。OSがLinuxである
京でも発生する
解決策:
メモリを静的に確保する(?)
mallocではなくcallocを使う (Fortranではどうやるのか知りません)
メモリを確保した直後、ページ単位で全体を触っておく
まぁ、そういうこともあると覚えておいてください
University of Tokyo
20. 20/76
NUMA (1/2)
NUMAとは
非対称メモリアクセス(Non-Uniform Memory Access)の略
CPUにつながっている物理メモリに近いメモリと遠いメモリの区別がある
京コンピュータはNUMAではないが、物性研System BはNUMA
Fast access
Memory
Memory
Memory
CPU
QPI
CPU
Memory
Memory
Memory
Slow access
特にlatency
University of Tokyo
21. 21/76
NUMA (2/2)
数値計算で何が問題になるか?
OpenMPでCPUをまたぐ並列化をする際、初期化を適当にやると
作業領域がコアから遠い物理メモリに割り当てられてしまう
Touch
CORE
CORE
CORE
CORE
QPI
CORE
CORE
CORE
CORE
OpenMPのスレッドを作る前に配列を初期化
→root プロセスの近くにメモリ割り当て
その後OpenMPでスレッド並列
→遠いメモリをアクセス(遅い)
解決策:
スレッドに対応する物理コアに近いところにメモリが割り当てられるように、スレッドごと
に配列を用意した上、OpenMPでスレッド並列化した後にtouchとか
詳しくはNUMA最適化でググれ
University of Tokyo
22. 22/76
CPU (1/2)
RISCと CISC
世の中のCPUアーキテクチャは大別して RISC型とCISC型の二種類
CISC:
・比較的複雑な命令セット
・if分岐などに強い (Out of order 実行)
・Intel Xeon, AMD Opteronなど。
RISC:
・比較的単純な命令セット
・行列演算などに強い
・IBM Power, SPARCなど (「京」はSPARC VIIIfx)
RISCでOoOだったりするので、現在はあまり意味のある区別ではない。
要するに「Intel系か、それ以外か」という区別
マルチコアとSIMD化
最近のCPUはほぼマルチコア化とSIMD化で性能をかせいでいる
・マルチコアで性能を出すには、キャッシュとメモリの構造を把握する必要あり
・京(8コア) FX10 (16コア) IBM POWER6 (32コア)、SGI Altix ICE(4コア*2)
・SIMDについては後述
University of Tokyo
23. 23/76
CPU (2/2)
ゲーム機とCPU
第六世代
DC SH-4
PS2 MIPS (Emotion Engine), VLIW
GC IBM PowerPC カスタム (Gekko)
Xbox Intel Celeron (PenIII ベース)
第七世代
Wii IBM PowerPC カスタム
Xbox 360 IBM PowerPC カスタム
PS3 IBM Cell 3.2
University of Tokyo
32. 32/76
メモリ最適化(1/2)
セル情報
相互作用粒子を探すさい、空間をセルに分割し、それぞれに登録する
どの空間に粒子が存在するかを多次元配列で実装すると効率が悪い
int GridParticleNumber[GridX][GridY];
int GridParticleIndex[GridX][GridY][GridMax];
一次元配列実装
O(N) Partial Sort
キャッシュ効率向上
レイテンシ
University of Tokyo
33. 33/76
メモリ最適化(2/2)
相互作用ペアリスト
短距離相互作用の場合、相互作用ペアリストを使う (Bookkeeping 法)
何も考えずに実装すると・・・
メモリ空間をランダムアクセスする(キャッシュ効率低下)
二つ分の粒子の情報をレジスタに持ってくる必要がある(要求バンド幅増大)
相互作用相手でソートする
相互作用粒子の一方がレジスタに載る
データの読み込みが半分になる
University of Tokyo
34. 34/76
キャッシュ最適化 (1/2)
空間ソート
時間発展を続けると、空間的には近い粒子が、メモリ上では
遠くに保存されている状態になる → ソート
University of Tokyo
35. 35/76
キャッシュ最適化(2/2)
空間ソートの効果
ソートあり
L2 Cache L3 Cache
(256 KB) (8 MB) ソートなし
粒子数
ソートなし:粒子数がキャッシュサイズをあふれる度に性能が劣化する
ソートあり:性能が粒子数に依存しない
University of Tokyo
36. 36/76
メモリ管理のまとめ
CPUチューニングの前にメモリアクセスのチェックを
とにかくキャッシュにのせる
連続アクセスにする (Partial Sortが有効)
メモリアクセスの最適化は、計算機を選ばないことが多い
(CPUチューニングはアーキテクチャに強く依存する)
並列化の前にメモリアクセスの最適化をする
・メモリアクセスの最適化をすると、データの持ち方が変わる
→並列化の通信ルーチンが強く影響を受ける
・逆にNUMA最適化など、並列化をにらんだデータ構造を持つ必要もある
→要するになんども組み直す必要があるということ
必要なデータがほぼキャッシュに載っており、CPUの
計算待ちがほとんどになって初めてCPUチューニングへ
University of Tokyo
40. 40/76
条件分岐コスト (2/2)
条件分岐なし
経過時間[s]
条件分岐あり
条件分岐がなければIBM POWERの方がIntel Xeonより早いが
条件分岐があると、大幅に遅くなる
IBM POWER、SPARC (京やFX10)は、条件分岐に弱い
University of Tokyo
41. 41/76
条件分岐削除 (1/2)
1. foreach interacting particles
2. r ← particle distance
3. if distance > cutoff length then continue
ここが重い
4. f ← calculate force
(次のループが回るかわからないから)
5. p ← update momenta
6. next
1. foreach interacting particles
2. r ← particle distance
3. f ← calculate force
4. if distance > cutoff length then f ←0 fsel 命令が使われる
5. p ← update momenta
(全てのループが確実にまわる)
6. next
余計な計算量が増えるが、パイプラインがスムーズに流れる
ために計算が早くなる(こともある)
University of Tokyo
42. 42/76
条件分岐削除 (2/2)
条件分岐なし
条件分岐あり
条件分岐削除
経過時間[s]
IBM POWERでの実行が大幅に高速化された
IBM POWERや、京には条件代入命令(fsel)がある
fsel
fp0,fp1,fp2,fp3
fp0
=
(fp1
>
0)?
fp2:
fp3;
これにより、マスク処理が可能
University of Tokyo
43. 43/76
SIMD化 (1/4)
SIMD化とは何か
コンパイラが出力する命令のうち、SIMD命令の割合を増やす事
スカラ命令: 一度にひとつの演算をする
SIMD命令(ベクトル命令):複数の対象にたいして、同種の演算を一度に行う
実際にやること
コンパイラがSIMD命令を出しやすいようにループを変形する
SIMD命令が出しやすいループ:
演算に依存関係がすくないループ
コンパイラがどうしてもSIMD命令を出せなかったら?
手作業によるSIMD化 ほとんどアセン
(Hand-SIMDize)
ブリで書くよう
なものです
University of Tokyo
44. 44/76
SIMD化 (2/4)
ループアンローリング
DO
I
=
1,
N
C[i]
=
A[i]
+
B[i]
依存関係があるのでSIMD命令が発行されない。
E[i]
=
C[i]
+
D[i]
G[i]
=
E[i]
+
F[i]
I[i]
=
G[i]
+
H[i]
END
DO
ループを二倍展開し、独立な計算を作る (馬鹿SIMD化)
コンパイラが
A[1]+B[1]
A[2]+B[2]
Loop Unrolled x times
C[1]+D[1]
C[2]+D[2]
とメッセージ出したらおそらくこれ
E[1]+F[1]
E[2]+F[2]
問題点:
I[1]+G[1]
I[2]+G[2]
・レイテンシを隠しづらい
・積和が作りにくい
→ 遅い
University of Tokyo
45. 45/76
SIMD化 (3/4)
ソフトウェアパイプライニング
DO I = 1, N
ループインデックス
C[i] = A[i] + B[i]
E[i] = C[i] + D[i]
A[1]+B[1]
H[i] = F[i] + G[i]
C[1]+D[1]
A[2]+B[2]
H[i] = H[i] + I[i]
END DO
E[1]+F[1]
C[2]+D[2]
A[3]+B[3]
I[1]+G[1]
E[2]+F[2]
C[3]+D[3]
A[4]+B[4]
I[2]+G[2]
E[3]+F[3]
C[4]+D[4]
I[3]+G[3]
E[4]+F[4]
I[4]+G[4]
独立な計算を増やしやすい
レイテンシを隠し易い
人間の手でやるのはかなり厳しい
だが、FX10ではこれをやらないと速度が出ない
→いかにコンパイラに「Loop software pipelined」を出させるか
University of Tokyo
46. 46/76
SIMD化 (4/4)
我々が実際にやったこと
作用反作用を使わない (二倍計算する)
→ FX10ではメモリへの書き戻しがボトルネックだった
ループを2倍展開した馬鹿SIMDループをさらに二倍アンロール (4倍展開)
→ レイテンシ隠蔽のため、手でソフトウェアパイプライニング
局所一次元配列に座標データをコピー
→ 運動量の書き戻しは行わないので、局所座標の読み出しがネックに
→ 「グローバル、static配列でないとsoftware pipeliningできない」
というコンパイラの仕様に対応するため(もうなおったかも)
除算のSIMD化
→ FX10には除算のSIMD命令がない(たぶん)
→ 逆数近似命令(低精度)+精度補正コードを手で書いた。
チューニング成果
力の計算の速度は2倍に、全体でも30%程度の速度向上
University of Tokyo
47. 47/76
CPUチューニングのまとめ
CPU依存のチューニングは、当然のことながらCPUに依存する
→ CPUを変えるたびにチューニングしなおし
→ プログラムの管理も面倒になる
そこまでやる必要あるんですか?
基本的には趣味の世界です。
世の中には全部intrinsic 命令でホットスポットを書く廃人もいるにはいる
University of Tokyo
58. 58/76
ハイブリッド並列化(3/6)
一般的方法
MPI: 空間分割
OpenMP: ループ分割
DO I=1,N ここか、
DO J in Pair(I) ここにスレッド並列をかける
CalcForce(I,J)
ENDDO
ENDDO
問題点
運動量の書き戻しで同じ粒子にアクセスする可能性がある
→ テンポラリバッファを用意して衝突をさける
→ 作用反作用を使わない
ペアリスト作成もスレッド並列化しなければならない
力の計算というボトルネックで、SIMD化とOpenMP化を同時に扱う必要がある
University of Tokyo