しかし,自分で組んだものが遅い,というのは悔しいものがあるので,なんとかしてチューニングできないものだろうかと思い立ち,いろいろやってみようと思う。
subroutine multmm(n, A, lda, B, ldb, C, ldc) implicit real*8 (a-h, o-z) dimension A(lda,*), B(ldb,*), C(ldc,*) C do j=1,n do i=1,n C(i,j) = 0.0d0 end do end do C do i=1,n do j=1,n do k=1,n C(i,j) = C(i,j) + A(i,k)*B(k,j) end do end do end do C return endC言語では,以下のよう。
void multmm(int n, double A[][lda], double B[][ldb], double C[][ldc]) { int i, j, k; for(i=0; i<n; i++){ for(j=0; j<n; j++){ C[i][j] = 0.0; } } for(i=0; i<n; i++){ for(j=0; j<n; j++){ for(k=0; k<n; k++){ C[i][j] += A[i][k]*B[k][j]; } } } }MATLABなどでは,以下のように簡潔である。
>> C=A*B;
<計算環境> PC : PC/AT互換機 CPU : Intel Pentium III 1066MHz L1 Cache : 32KByte L2 Cache : 512KByte Main Memory : 640MByte Compiler : DIGITAL Visual Fortran 6.0まず,参考までに,MATLAB 6での実行結果を示しておく。
>> n=1000; >> A=rand(n); B=rand(n); >> tic; C=A*B; toc
n = 1000 | 計算時間(秒) | MFLOPS | |
C=A*B | 2.88 | 693 |
<計算順序の見直し>
プログラムをチューニングする上で,まず気をつけるのはメモリの参照方向であろう。上記のFortranのプログラムを見ると,ループの順番が外側から(i,j,k)の順になっている。この場合,順番は可変であるので,(j,i,k)や(k,j,i)などと順番を変更してみたところ,以下のような結果になった。尚,特別な記述がない限り,コンパイルオプションは「Full Optimizations(Releaseモードのデフォルト)」で実行してある。
n = 1000 | 計算時間(秒) | MFLOPS | |
(i,j,k) | 13.23 | 151 | |
(i,k,j) | 33.45 | 59 | |
(j,i,k) | 24.46 | 75 | |
(j,k,i) | 9.76 | 204 | |
(k,i,j) | 64.97 | 30 | |
(k,j,i) | 38.44 | 52 |
do i=1,n do j=1,n s = 0.0d0 do k=1,n s = s + A(i,k)*B(k,j) end do C(i,j) = s end do end do(i,j,k)と(j,i,k)は,そのまま内積計算のように変更できる。実行結果は以下のようになった
n = 1000 | 計算時間(秒) | MFLOPS | |
(i,j,k) | 13.23 | 151 | |
(i,j,k)+内積化 | 12.23 | 163 | |
(j,i,k) | 24.46 | 75 | |
(j,i,k)+内積化 | 25.92 | 77 |
do i=1,n do k=1,n W(k) = A(i,k) end do do j=1,n s = 0.0d0 do k=1,n s = s + W(k)*B(k,j) end do C(i,j) = s end do end do
n = 1000 | 計算時間(秒) | MFLOPS | |
(i,j,k) | 13.23 | 151 | |
+内積化 | 12.23 | 163 | |
+内積化+ワーク | 10.21 | 195 |
do i=1,n do j=1,n s0 = 0.0d0 s1 = 0.0d0 s2 = 0.0d0 s3 = 0.0d0 do k=1,n,4 s0 = s0 + A(i,k)*B(k,j) s1 = s1 + A(i,k+1)*B(k+1,j) s2 = s2 + A(i,k+2)*B(k+2,j) s3 = s3 + A(i,k+3)*B(k+3,j) end do C(i,j) = s0 + s1 + s2 + s3 end do end doこの場合では,本当は,nが4の倍数でないときの処理を加えなければならないが,ここでは省略する。結果を以下に示す。
n = 1000 | 計算時間(秒) | MFLOPS | |
(i,j,k) | 13.23 | 151 | |
+内積化 | 12.23 | 163 | |
+内積化+ワーク | 10.21 | 195 | |
+内積化+ワーク+unrolling(2) | 10.29 | 194 | |
+内積化+ワーク+unrolling(4) | 10.33 | 193 |
n = 1000 | 計算時間(秒) | MFLOPS | |
(i,j,k), Full Opt. | 13.23 | 151 | |
(i,j,k), Max. Opt. | 5.16 | 387 |
一体,Max. Opt.でコンパイラが何をしてくれたのかは定かではないが,キャッシュを利用したとしか考えられないので,そっちがその気なら・・,という意気込みで次へ。
<行列のブロック化(blocking)>
さて,このままでは悔しいので,通常では最高のコンパイルオプションである「Full Optimizations」において,なんとか「Maximum Optimizations」のMFLOPS値を上回ることができないものかと考えた。そのためには,キャッシュメモリ(特に,L2 Cache)を有効に利用することを考えなければならないだろう。そこで,まず,どれくらいの行列サイズまでキャッシュが効くのかをテストした。その結果(MFLOPS)を以下に示す。
type / n | 20 | 40 | 60 | 80 | 100 | 120 | 140 | 160 | 180 | 200 | 220 | 240 | 260 | 280 | 300 |
(i,j,k) | 307 | 363 | 356 | 368 | 377 | 381 | 378 | 304 | 364 | 338 | 321 | 213 | 225 | 195 | 192 |
(i,j,k)+内積化 | 499 | 444 | 480 | 491 | 500 | 491 | 507 | 453 | 485 | 459 | 425 | 258 | 257 | 224 | 215 |
(i,j,k)+内積化+ワーク | 400 | 444 | 480 | 491 | 487 | 491 | 488 | 483 | 494 | 481 | 442 | 294 | 265 | 246 | 219 |
(i,k,j) | 285 | 320 | 345 | 307 | 357 | 351 | 271 | 185 | 224 | 193 | 189 | 111 | 107 | 94 | 82 |
(j,i,k) | 363 | 347 | 356 | 359 | 384 | 388 | 384 | 338 | 373 | 338 | 279 | 143 | 138 | 127 | 103 |
(j,i,k)+内積化 | 363 | 470 | 502 | 526 | 540 | 526 | 548 | 499 | 511 | 481 | 370 | 162 | 152 | 140 | 103 |
(j,k,i) | 333 | 400 | 425 | 433 | 444 | 451 | 448 | 446 | 448 | 439 | 420 | 289 | 263 | 265 | 226 |
(k,i,j) | 307 | 347 | 335 | 307 | 350 | 340 | 271 | 187 | 227 | 195 | 200 | 141 | 108 | 74 | 58 |
(k,j,i) | 333 | 421 | 442 | 446 | 454 | 451 | 448 | 453 | 455 | 454 | 425 | 289 | 189 | 119 | 90 |
(n = 1000)
ブロックサイズ | 計算時間(秒) | MFLOPS | |
(10) | 6.01 | 332 | |
(20) | 5.00 | 400 | |
(30) | 5.54 | 361 | |
(40) | 4.95 | 404 | |
(50) | 4.91 | 407 | |
(60) | 5.47 | 365 | |
(70) | 6.03 | 331 | |
(80) | 6.94 | 288 | |
(90) | 7.48 | 267 | |
(100) | 7.71 | 259 |
整理したプログラムのソースはここ(C言語用)とここ(Fortran用)で公開中。
<Linuxでの実行結果>
上記の作業をLinux上でも実行し(今までと同じPCを使用),さらにBLASのサブルーチンDGEMM(ATLAS最適化)での実行結果も示す。尚,コンパイル方法や実行方法などは以下の通り。
% g77 -O3 multmm.f xclock_ux.c -latlas
% ./a.out
n = 1000 | 計算時間(秒) | MFLOPS | |
(i,j,k) | 12.95 | 154 | |
(i,j,k)+内積化 | 12.40 | 161 | |
(i,j,k)+内積化+ワーク | 9.89 | 202 | |
blocking(50) | 5.30 | 377 | |
ATLAS最適化BLAS/DGEMM | 2.48 | 806 |