なぜ Athlon は Pentium3 よりたいして速くないのか?

2001 年 1 月 11 日
2001 年 2 月 3 日 新構想で再登場.
2001 年 6 月 15 日 CuMine667+VCSDRAM のグラフを追加.
2001 年 8 月 2 日 ひざの原因は TLB だった.

1. はじめに

僕は AMD Athlon が好きです. intel Pentium3 よりも基本設計が新しく, トランジスタ数も多く,消費電力も大きい. AMD は「Athlon は第七世代の CPU であり,第六世代の P3 とは 一世代違う」と主張する. 一世代違うというなら,性能も大差がついて然るべきだろう. しかし実際には,同じ周波数で比べたベンチマーク性能は, 若干 Athlon の方が速い程度で,大差はない. SETI@home などでは,むしろ P3 の方が有利だといわれている. Athlon/Duron CPU の優位は,アーキテクチャ上のものというより, 同じ値段でより周波数の高い CPU が買える,という点にある. そこで疑問.
何故,これだけ物量を投入した Athlon CPU が, Pentium3 CPU と比べられてしまうほどの性能しかでないのか?
これまでは,漠然と「intel チップセットのメモリアクセス能力が 優秀だから」などと言われていたが,本当だろうか? 去年の秋立ち読みしていた何かのパソコン雑誌に, 目の醒めるようなグラフが載っていた. それを再現したくなってはじめたするためにやった実験がこれである. しかし,思いもよらず不可思議な現象が現れてしまった. 皆さんと一緒に考えたい.

2. 行列の乗算コード

プログラムは,行列のかけ算 A=B×C を計算するもの. 「行列演算なんて普通の使い方じゃない」という人は ここでさようなら. プログラムのカーネルは,二種類用意した.

A.定義通りの計算

    for (i = 0; i < size; i++)
        for (j = 0; j < size; j++)
        {
            sum = 0;
            for (k = 0; k < size; k++)
                sum += a[i*size + k] * b[k*size + j];
            c[i*size + j] = sum;
        }
横軸は,NxN 行列の N (= size) を表している. 配列 a のアクセスは行について連続なので, 一行分(N x 8バイト)キャッシュに入ればよい. 一方配列 b に対するアクセスは列について連続なので, 一列分キャッシュに入ればよい.

列の要素は主記憶上で飛び飛びになっている. 主記憶とキャッシュの間でやりとりするデータの大きさは, キャッシュの一ライン分,AMD が 64 バイト,P3 が 32 バイトである. よって,一列アクセスしたときの,総転送量は一行アクセスしたときの 8/4 倍に増える. ただし,余分にくっついてきたデータは, 次の列を計算するときに使われるから, 一列すべてがキャッシュ収まってしまえば,無駄にはならず, 乗算を終るまでの総転送量も同じになる.

B.メモリを連続アクセスするようにループを入れ換えたもの

今度は,演算順序を換えて,最内側ループでは配列 a は 常に同じ要素,b, c は行について連続なアクセスになるようにする.

    for (i = 0; i < size*size; i++)
        c[i] = 0;
    for (i = 0; i < size; i++)
        for (k = 0; k < size; k++)
            for (j = 0; j < size; j++)
                c[i*size + j] += a[i*size + k] * b[k*size + j];
下に,実験に使った四つのシステムを示す.

表1:実験条件.
CPU一次キャッシュ二次キャッシュM/BChipsetMemory OS
Coppermine 667命令 16KB
データ 16KB
256KB 等速バックサイド
Abit VA6Apollo Pro133PC133CL3 128MBFreeBSD4.0R
Katmai 500命令 16KB
データ 16KB
512KB 半速バックサイドGA-6BXD440BXPC100CL2 128MBVineLinux 1.1
Duron 700命令 64KB
データ 64KB
64KB 等速エクスクルーシブ
16 Way セットアソシアティブ
AsusTeK A7VKT133VC133CL2 128MBFreeBSD4.1R
K7 Athlon 650命令 64KB
データ 64KB
512KB 半速バックサイド
2 Way セットアソシアティブ
AsusTeK K7VKX133VC133CL2 128MBFreeBSD4.2R

3. Coppermine Pentium3 667

まず現行ペンティアム3から見てみよう. 縦軸は getrusage() で計ったユーザ時間から求めた MFLOPS の値である.

MFLOPS vs Matrix size: Coppermine CPU

二本のグラフは,それぞれ三つの領域に分かれていることが分かる.

  1. N < 171 のときは,もっとも性能が高い. このときは一つの行列の大きさが 228KB であり, 行列 a, b のすべては二次キャッシュに収まらないものの, 行列 a 一行分と行列 b すべてが収まっている状態と 考えられる. コード A の方が速いのは,添字の計算が一つ少ないからと 考えられる.

  2. 172 < N < 191 のときは,狭い範囲で演算速度が急激に落ちる.

  3. 192 < N では,40MFLOPS 辺りでグラフは定規で引いたように水平となり, 行列の大きさを変えても演算速度は一定であることが分かる. 行列 b が二次キャッシュからはみ出した状態である. このときはコード B の方が速い. 配列のアクセスが a,b とも連続なため,若干キャッシュの ヒット率が良くなるのであろう. またコード A では,極端に遅くなる特異点が点々と存在する. これは,行列 b についてのメモリアクセスの飛び(ストライド)と, 二次キャッシュの構成が特別な関係になると, キャッシュアクセスがエルゴード的でなくなり, 容量の一部しか使われない状態になるためと考えられる.

ついでに,メモリが Virtual Channel SDRAM (133MHz CL2) のときを 見ておこう.

Coppermine CPU with VC SDRAM

192 < N のとき平均一割位速くなっていることが分かる. KT133 あたりだと,この差は 5% 程度なので,Apollo Pro133 は VC SDRAM の効果が大きいことがいえる. 不思議なのは,N < 172 のときの速度も影響を受けていることである. コード A は速く,コード B は遅くなっている.

4. Duron 700

次に Duron 700 を見てみよう.

MFLOPS vs Matrix size: Duron CPU

コード B (連続アクセス)は,Coppermine と似た形をしている. 領域1での速度は,100MFLOPS 強で,クロックが近い Coppermine667 と 大体同じである. 速度低下は N = 124 から始まる.このとき行列の大きさは 120KB である. オンチップのデータキャッシュは 一次64KB+二次64KB の128KB であるから, 計算は合う. 領域3では P3 より 1.5 倍速いが,これは主記憶が速いことを 意味しているのだろうか. Apollo Pro133 の主記憶アクセス性能が悪いことは有名であり, 連続アクセスでのスループットは 440BX の半分くらいしか出ない.

問題はコードAである.領域3がさらに二つに分かれている. N〜360 あたりにもう一つ謎のひざがあり, それ以上で性能が悪くなっている. N〜1000 では平均 17MFLOPS 程度まで落ち込む. よく見ると領域1でも,コードBとの性能が Coppermine とは 逆転してむしろ遅くなっている. これは,Duron が,連続でないアクセスが相対的に遅いことを 表しているのだろうか. またこの Duron でも,性能が平均より著しく下がる「特異点」 の存在が目立つ.

5. K7-650

次に初期型 Athlon (K7) を見てみよう.

MFLOPS vs Matrix size

コードAでの,N〜360 の謎のひざは Duron と同じところに現れ, それ以上で性能低下はさらにひどい. N〜1000 では平均 11MFLOPS くらいしかでない. 第一のひざは N〜183 あたりにあるが, これはちょうど行列 a,b のすべてが二次キャッシュに収まる かどうかの境界に当たっている. そこからの落ち込みは緩やかである. Coppermine や Duron ではひざを作っていた, 行列 a 一行と行列 b がすべて収まるの限界は, N〜250 あたりになるが,今度はそのあたりにひざはない. 二次キャッシュが Coppermine や Duron に比べ大きく遅いために, このような違いが生じるのであろう.

6. Katmai Pentium3 500

K7 Athlon と二次キャッシュの構成が似ている例として, 旧型の Katmai Pentium3 CPU も見てみよう.

MFLOPS vs Matrix size: Katmai

N > 180 での性能の低下は K7 Athlon と同じような緩やかさをもっている. しかし,N > 360 での第二のひざはやはり存在しない.

7. 考察

7.1 ひざの原因はなにか

僕にとって半年以上謎だったのだが, AMD Palomino プロセッサの Thunderbird からの改良点として, TLB (Table Lookaside Buffer) が挙げられていることについて, もう一度考え直してみて,ようやく解けた. TLB とは,以下のようなものである.

アドレス空間はプロセスごとに与えられるが, マルチプロセス環境では,実際にどこの物理アドレスが 割り当てられるか予め分からない. よってプロセスに割り当てられた「仮想アドレス」から 主記憶の「物理アドレス」に変換する表が必要である. これが「アドレス変換テーブル」であり, これ自体主記憶(あるいはキャッシュメモリ)上におかれている.

しかし,これだと,主記憶にアクセスするたびアドレス変換 テーブルを引かなければならず,性能が数分の一に低下してしまう. そこで,最近使った主記憶ページについては,CPU 内の バッファメモリに記憶しておいて,高速にアクセスできるようにする. これが TLB である.

N〜360 は行列のサイズ 1MB に対応している. Athlon,Duron ともにデータ二次 TLB は 4KB ページ 256 個分であり, 合計でちょうど 1MB のメモリに相当する. つまり行列のサイズが 1MB までは,すべて TLB に収まるが, それ以上だと TLB ミスが発生して,アドレス変換テーブルへの アクセスという余分な主記憶アクセスが発生して性能が落ちる わけだ.

オフダイ二次キャッシュの K7 Athlon よりもオンダイの Athlon/Duron のほうが落ち込みが少ないわけは,二次キャッシュへのアクセスが 速いからだろう. Athlon4 (Palomino) では,データ TLB は合計 288 個分に増えているので, ひざがわずかに右にずれると予想される. 落ち込みが改善されるかどうかは不明.

7.2 PIII の場合

それでは PIII だとどうか. PIII のデータ TLB は 4KB ページ 64 個分なので,主記憶 256KB になる. これはちょうど Coppermine の二次キャッシュの容量と同じであり, キャッシュミスと TLB ミスが同時に起こるため, 重なってしまうのだろう. Katmai のでは,512KB の半速二次キャッシュのミスによる性能低下は, 滑らかにな起こるため,やはり TLB ミスによる性能低下だけ 分離して見えないのだろう.

しかし,PIII では TLB ミスしているはずの時としていない時の 性能があまり変わらないのである.

K7 アーキテクチャは TLB ミスのペナルティが大きいのだろうか?

shida@ac.cs.musashi-tech.ac.jp