ソフテック・トップページへ
ホーム 製品 セキュリティ・サービス HPCサービス ダウンロード 企業情報

PGI compiler Tutorial
ソフトウェア・プリフェッチで性能向上
PGI チュートリアル > プリフェッチ

 
メモリからのプリフェッチを制御し、キャッシュ利用の最適化を図る

 現在のプロセッサ技術は、メモリからプロセッサのレジスタまでの間に階層的なキャッシュを置き、データのロード・ストアによる遅延を隠蔽する技術が使用されているため、プロセッサのキャッシュ利用の最適化の如何によりプログラム性能は大きく異なります。従来からキャッシュ利用の最適化を図る方法は、ソースレベルで行うものとして様々な方法が考えられてきました。例えば、ループのアンロール処理、1次元の配列のループ処理においては「ストリップマイニング方法」、二次元以上の配列のループ処理においては「ループのブロッキング手法」等、キャッシュヒット率を向上させるための方法があります。これらの最適化方法に関して、ここでは詳述しませんが、ループのインデックスの変更を行うという煩雑な手間がかかり、さらにソースレベルの直感性を損なうため、性能向上と言う強い目的がない限り、ソースレベルの最適化は行わないのが現実です。
 一方、コンパイラによるキャッシュ利用の最適化技術は従来に較べ向上したとは言え、自動アンロール処理と言う一般的な方法や、キャッシュサイズに応じたループブロッキング(Cache tiling)等が行われるに過ぎませんでした。特に後者のループブロッキングの最適化は常に行えるわけではないため、自動アンロール処理や自動インライン展開と言う手法がコンパイラによる一般的な自動キャッシュ利用最適化技術と言えます。また、コンパイラのプリフェッチ・ディレクティブ(指示行)等の挿入によって、コンパイラにヒントを与える間接的な最適化手法も存在しますが、これらは一般のユーザが使用できるものではありません。
 しかし、プロセッサに SSE 機構(同時に二組以上のデータを処理できるストリーミング SIMD データ並列処理機構)が付加されてから、コンパイラの最適化手法にも幅が出てきました。最新のプロセッサは、インテル(R)、AMD 製のどちらにも SSE、SSE2、SSE3 の機構を備えるようになり、1 インストラクションで二組以上のデータ並列処理(ベクトル処理)が可能となりました。これらの動作を総称して、MPU レベルでの「ベクトル処理」と言っています。ここで説明する、「データのプリフェッチ」を行うインストラクション命令は、実は、このストリーミング SIMD 拡張命令の中に導入されたものであり、このプリフェッチ命令とキャッシュ制御命令によって、キャッシュ利用の最適化に自由度が出てきました。特に、PGI コンパイラ 6.0 以降では、コンパイラレベルでプリフェッチを行う対象を見出して、さらに、コンパイラ・オプションでキャッシュ制御命令のパラメータを指定することが可能で、プリフェッチのスケジューリング距離の調整やプリフェッチ命令の種類を変更することにより、キャッシュ利用の最適化を行うことができるようになりました。本稿では、プリフェッチ関連のコンパイラ・オプションの指定により、性能が大きく異なる場合があることを一つの例を示して説明します。

ここで例題に使用したソースファイルは、以下のものです。ご自由にダウンロードして試して見てください。

  使用ソースファイル : mm1.f (一般的なコーディング)
               : mm2.f (キャッシュ有効活用のため一時配列を追加)
  時間計測ルーチン  : wallclock.c (Elapsed timeベース:並列処理時の時間計測に便利、CPU 時間ベースではない)
  

KEYWORD: メモリ階層、 キャッシュ利用の最適化、 プリフェッチ処理、 データ参照パターン、 prefetchnta


プリフェッチ処理とは

 始めに、プリフェッチ処理について簡単に説明します。以下の図 1 と図 2 を見てください。
図 1 に示した様子は、プリフェッチを行わず、ループ内の処理で実行と必要なデータをその度にロードする形態を示しています。見ての通り、メモリ・ロード中、実行はアイドリングします。これはキャッシュ内に必要なデータがない場合の極端な例を示したものですが、キャッシュ内にデータが存在すると実行時のパイプライン上のアイドルが減少し、その結果、性能も向上します。もし、実行時に使用するデータが、前以ってキャッシュに存在していればと言うことを実現するのが、「プリフェッチ」です。ある配列の実行前に、それより先に使用するデータを前以ってフェッチすることをソフトウェア命令で発行しておくことです。こうすると、実行と並行にメモリアクセスも開始され、実行パイプライン上で必要な時にキャッシュ内のデータを即座に使用できる体制(キャッシュミスが生じない状況)になります。

           図 1 プリフェッチを行わない場合の実行とメモリアクセス


 プリフェッチを行った場合の模式図が以下の図 2 です。実行パイプラインとメモリデータの取り込みが独立に動作して、実行に必要なデータがキャッシュ上に存在することになります。また、メモリアクセス時間がループ1回の処理時間より長い場合は、数回あとの分までのデータを予め、プリフェッチしておけば効率的となります。この数回分と言う数が、「プリフェッチ・スケジューリング距離」と称します。PGIコンパイラでは、この距離を明示的にコンパイラに指示することが可能です。

            図 2 プリフェッチを行った場合の実行とメモリアクセス


 プリフェッチの方法には、ハードウェアで自動的に行う方法と上記で述べたソフトウェアでプリフェッチ命令を明示的に指示する方法があります。一般的には、ハードウェアが自動的にプリフェッチを行っています。これはハードウェアが勝手に推測して行う方法ですが、規則的なメモリアクセスパターンをあるルールに基づいて行うものですので、プログラムの特性に応じた自由度があるわけではありません。そこで、これを補完するソフトウェア・プリフェッチ命令ができたわけです。

 ソフトウェア・プリフェッチ命令は、前述したとおり SSE2 で加えられたプリフェッチ命令です。プリフェッチ命令には、いくつかの種類 (prefetchnta, prefetcht0〜2) があり、データ参照パターンへの最適化やキャッシュ利用の制御を行います。プリフェッチ命令は、基本的にキャッシュ内のデータを有効活用できるようにするための命令です。そこで、大事な点が浮上します。プログラムには、データ参照のパターンがあります。極端な例として、ループ内の処理で、一回のみ参照すればよいデータとあるデータが何度も参照される場合と二通りあります。キャッシュ内のデータが繰り返し、再利用される場合は、長くキャッシュ内に留まっていることが性能を高めます。一方、一回のみ参照するデータは、一回ロードした後キャッシュ内に保持する必要はありません。キャッシュ内に保持すると言うことは、L1 / L2 キャッシュ管理まで影響し、キャッシュが一杯になるまで追い出されません。これが一回しか参照されないということを予め、ハードウェアが知ることができたならば、限られたキャッシュサイズを有効に活用できる余地が出ます(これをキャッシュの汚染を防ぐと言う)。
 そこで、プリフェッチ命令に、このデータ参照のパターンを指示する命令が加わりました。これが、 prefetchnta、 prefetcht0〜2 と言うものですが、現在は、主に prefetchnta と prefetcht0 が使用されています。これらの詳細な機能は、プロセッサの実装依存となっていますが、prefetchnta と prefetcht0 に関しては明らかに異なるものとなっています。まず、データ参照パターンについて説明しましょう。データ参照パターンは、以下の三種類のものに分類できます。

 (1) ノンテンポラル(non-temporal):一度参照されたら暫く使用されないパターン
 (2) テンポラル(temporal):時間的にアクセス頻度の高い参照パターン
 (3) 空間的(spacial):隣接したキャッシュライン上でデータが利用されるパターン

このような参照パターンの種類に応じて、プロセッサに先読みのヒントを与えるのがソフトウェア・プリフェッチ命令です。特に、(1) と(2) に関しての区別は、プリフェッチにおいて重要です。(1) に相当する命令は、prefetchnta、(2)に相当するのは prefetcht0 となります。PGI コンパイラでは、この nta と t0 命令の挿入を指示できます。AMD とインテル(R)のプリフェッチ命令は、同じ命令であっても、キャッシュ上での取り扱い方に違いがありますが、ここでは詳細を述べません。大事なのは、プログラムの最も実行時間を要するループ部分において、右辺式の配列のメモリロードが一度参照されたら暫く参照されないパターン(nta)なのか、あるいは時間的にあまり間をおかずにすぐにデータを再利用されるパターン(t0)なのかを判断することです。

 プリフェッチに関しては、上記の基礎知識があれば、以下で述べるコンパイラのプリフェッチ命令制御オプションを使用することができます。

コンパイラがプリフェッチ命令を生成した場合の確認方法

 行列積を求める例題のプログラム(mm2.f)を用いて、PGI コンパイラでコンパイルしてみます。コンパイルの情報を出力するために、-Minfo オプションを付けています。また、プリフェッチ命令は、ベクトル化最適化の一つですので -fastsse オプションを付けます。コンパイル情報に、prefetch 命令を生成したことが記載されていることが理解できると思います。なお、EM64T ターゲットのプロセッサでは、本例題の場合、プリフェッチ命令を生成しません。この場合は、-tp x64 オプションを付けてコンパイルしてください。なお、次に述べるプリフェッチ命令制御オプションは、コンパイラがプリフェッチするデータを認識して、以下のメッセージのようにプリフェッチ命令が生成してなければ、たとえ指定しても意味を成しませんのでご注意ください。

【 行列積のプログラム (mm2.f)以下は、AMD64 CPU の場合
(   38)          do i = 1, m
(   39)             do ii = 1, n
(   40)                arow(ii) = a(i,ii)
(   41)             enddo
(   42)             do j = 1, p
(   43)                do k = 1, n
(   44)                   c(i,j) = c(i,j) + arow(k) * b(k,j)
(   45)                enddo
(   46)             enddo
(   47)          enddo

$ pgf95 -fastsse -Minfo mm2.f wallclock.o
    43, Generated an alternate loop for the inner loop
        Generated vector sse code for inner loop
        Generated 2 prefetch instructions for this loop
        Generated vector sse code for inner loop

43 行目のループである k のループにおいて、arow(k)、b(k,j) 配列のプリフェッチ命令を
生成している。コンパイラのメッセージに「2 prefetch instructions」が出力されている。

PGI コンパイラが提供するプリフェッチ・ストア命令制御

ソフトウェア・プリフェッチ命令制御オプション

 PGI コンパイラが提供するプリフェッチ制御オプションは、次のように -Mprefetch オプションに各フラグを指定することにより制御できます。このオプションが有効になるのは、-Mvect のベクトル最適化オプションが同時に指定されていることが必要であり、一般的に使用する -fastsse は、このベクトル最適化が含まれているオプションですので、これを指定することをお勧めします。厳密には、ソフトウェア・プリフェッチ命令は、 SSE インストラクションの中の一つであるため、必ず SSE ベクトル化最適化に伴うものと思って下さい。以下に、各フラグの意味を示しました。

  pgf95 -fastsse -Mprefetch=distance:8,n:4,[nta,t0,plain,w]

 -Mprefetch の主なフラグの機能を以下の表に纏めました。この中でよく使用されるプリフェッチ命令は、nta、t0 ですが、何も指定しない場合のデフォルトは、 t0 です。これらの二つの区別を簡単に言えば、nta は全てのキャッシュ内にデータを留めない方式(ただし、プロセッサにより実装依存性有り)、t0 は、プリフェッチしたデータをできるだけキャッシュ内に留めるように指示するものです。例えば、演算式の右辺で、ループ内で一度の参照だけで終わる配列参照が多いケースでは、nta の方が、性能が向上します。
 プリフェッチを行う候補は、コンパイラが自動的に検出します。コンパイラ・ディレクティブでの指示はできません。これは、ループ内で行う、SSEインストラクションを使用した「ベクトル化最適化(ソフトウェア・パイプライン)」の中の一環として行われるため、コンパイラの判断が優先されます。また、オプション・フラグの中で 「n:数字」 がありますが、これはコンパイラがプリフェッチ命令を挿入する最大数を指示するものです。ただし、たとえ大きな数字を指示しても、プリフェッチ候補が少なければ、コンパイラが判断した数となります。
 オプション・フラグの中で 「distance:数字」 は、distance すなわち、キャッシュライン・スケジューリングの距離を指定するものです。このパラメータを制御すると性能が向上する場合があります。キャッシュライン・スケジューリングの距離とは、前以って、どの位の数のキャッシュライン数をプリフェッチするかと言う数字となります。一度にプリフェッチするデータ数(キャッシュラインサイズと言う)は、プロセッサによって異なります。AMD 系では 64Byte で、Pentium(R) 4 系では 128Byte です。このデータの塊を何セット事前にフェッチするかと言う数字となりますが、大きすぎても小さすぎてもいけません。プログラム特性に合わせた最適な数字を試行錯誤して決める必要があります。
 これらのオプション・フラグは、プログラム全体に対して指示されるものですので、もし個々のサブルーチン毎に、nta あるいは、t0 の特性が異なる場合、Makefile 等でサブルーチン毎にコンパイラ・オプションを指定するような形態が必要となります。

表 1  -Mprefetch オプションの各フラグの説明
フラグ 意味 数字の意味 デフォルト
distance:数字 キャッシュライン・スケジューリングの距離を指定 プリフェッチする cache-line の数 2
n:数字 プリフェッチ命令の最大挿入数をコンパイラに指示 コンパイラが認識可能となる挿入数 最大 8 以内
指定するプリフェッチ命令 プリフェッチの種類
nta prefetchnta 命令を挿入 ノンテンポラル・フェッチ(キャッシュ汚染少ない)
t0 prefetcht0 命令を挿入 テンポラル・フェッチ(キャッシュの再利用可能性がある場合)
w prefetchw 命令を挿入(AMDのみ) キャッシュラインの変更可能性のあるプリフェッチ


ストリーミング・ストア(ノンテンポラルなストア命令)を指示するオプション

 ストリーミングストア命令とは、キャッシュ内で再利用の可能性の少ないデータをキャッシュをバイパスして、直接、メモリに書き込む命令です。これによって、再利用されないデータがキャッシュに留まらず、キャッシュ上で再利用されるデータのための空きスペースが確保されキャッシュの汚染を少なくします。ただし、このオプションを使用した場合、演算の特性に依存するため、全て、性能が向上するわけではありませんので、性能を比較してご使用ください。PGI 6.1 以降では、-Mnontemporal の代わりに新しいオプション -Mmovnt が新設されました。

  pgf95 -fastsse -Mnontemporal あるいは -Mmovnt


AMD64(AMD系) と EM64T(インテル系)上でのプリフェッチ

 現時点での PGI コンパイラでは、AMD64(AMD系) と EM64T(インテル系)の CPU ターゲットによって、プリフェッチ命令を使用するコードを生成するかどうかは、コンパイラ依存となっています。ただ、言えることは EM64T ターゲットの場合は、ソフトウェア・プリフェッチを多用しないでプロセッサ内部の自動ハードウェア・プリフェッチに委ねることが多いようです。AMD64 ターゲットの場合は、極力プリフェッチ命令を挿入するコードを生成します。EM64T において、ソフトウェア・プリフェッチ付きのコードを生成したい場合は、コンパイラ・オプションに -tp x64 を付けて Unified Binary 生成を行うとプリフェッチ付きのコード生成が可能となる場合があります(全てではありませんのでご注意ください)。EM64T ターゲットの場合は、-tp x64 を指定すると性能が多少向上することが多いですので、お試しください。

行列積のプログラムをプリフェッチを制御して得る性能

 行列積のメモリアクセスに伴う最適化の考え方は、別の記事で説明しました。その記事で説明した、mm1.f と mm2.f のソースファイルを用いて、プリフェッチ制御を行った場合、一般的なコンパイル・オプション時の性能に較べてどの程度の差があるかを示したものが以下の表です。なお、使用したシステムの仕様は、こちらに示しました。使用したコンパイル・オプションは、以下の通りです。プロセッサによって、キャッシュラインのスケジューリング距離(distance:数字)が異なっています。また、データ参照パターンは、nta を指定しています。プリフェッチ・オプションを指定しない場合に較べて、大幅な性能向上が見られます。データ参照パターンとスケジューリング距離を的確に指定することにより性能が向上したと言うことになります。

 Athlon64x2 の場合 : pgf95 -fastsse -Minfo -tp x64 -Mprefetch=distance:8,nta mm2.f wallclock.o
 Pentium(R) Dの場合 : pgf95 -fastsse -Minfo -tp x64 -Mprefetch=distance:16,nta mm2.f wallclock.o

表 2  行列積計算 性能の推移 (1000x1000)
システム クロック Prefetchなし
mm1.f
(MFLOPS)
Prefetchなし
mm2.f
(MFLOPS)
プリフェッチ
制御をすると
mm1.f
-Mprefetch
(MFLOPS)
mm2.f
-Mprefetch
(MFLOPS)
Athlon64x2 2.2GHz 625 668    ==>   1088 1392
Pentium(R)D 2.8GHz 1142 1150    ==> 1365 1492
(prefetchnta 命令を使用したプリフェッチ処理)


行列積のプログラムのデータ参照パターン

 以下に、mm2.f の行列積の計算部分を示しました。この場合、43行目のループが最も時間を消費するところとなりますが、プリフェッチの対象は、arow(k)、b(k,j) となります。k インデックスで回るループとなりますので、これらは、メモリ上では連続アクセスとなります。また、参照パターンは、arow(k)、b(k,j) 共に一度参照すれば、後で再利用されることはありませんので、nta (ノンテンポラル・フェッチ)命令が適切です。また、ループ内は単純な積・和ですので、演算コストは相対的に小さいものと思われます。そこで、ある程度の距離のプリフェッチを予め行っておき、一気にその部分の計算を行うという指針が得られます。そこで、今回は、distance:8 あるいは distance:16 を指定しました。Pentium (R) の場合は、 distance:8 よりも distance:16 の方が、性能が伸びました。Athlon64x2 の場合は、 distance:8 以上にしても性能はほとんど変わりません。なお、distance:数字 の距離を表す数字は、どんな数字(偶数、奇数問わず)でも良いのですが、あまり長い数字を指定するとプリフェッチのコストが大きくなり、性能が低下することもあります。従って、いくつか性能実験を行い、プログラムに合った適切な数字を選ぶことが必要です。

プログラム 2 (mm2.f) 】
(   38)          do i = 1, m
(   39)             do ii = 1, n
(   40)                arow(ii) = a(i,ii)
(   41)             enddo
(   42)             do j = 1, p
(   43)                do k = 1, n
(   44)                   c(i,j) = c(i,j) + arow(k) * b(k,j)
(   45)                enddo
(   46)             enddo
(   47)          enddo

$ pgf95 -fastsse -Minfo -tp x64 -Mprefetch=distance:8,nta mm2.f wallclock.o
    43, Generated an alternate loop for the inner loop
        Generated vector sse code for inner loop
        Generated 2 prefetch instructions for this loop
        Generated vector sse code for inner loop

それでは、データ参照パターンを nta ではなく、t0 で指定した場合は、どうなるでしょうか? 表 2 で示した mm2.f の nta の場合と較べて明らかに性能が低下していることが分かります。(以下、参照) 行列積の参照パターンでは、nta のプリフェッチの方法が望ましいことが分かります。

Athlon64x2 】

$ pgf95 -fastsse -Minfo -Mprefetch=distance:8,t0 mm2.f wallclock.o 
$ a.out
 Elapsed time =    2.447646498680115       (sec)
 M =         1000 , N =         1000 , P =         1000
 MFLOPS =     816.7029025955970
  
【 Pentium(R) D 】

$ pgf95 -fastsse -Minfo -Mprefetch=distance:16,t0 mm2.f wallclock.o
$ a.out
 Elapsed time =    1.494485497474670       (sec)
 M =         1000 , N =         1000 , P =         1000
 MFLOPS =     1337.584073835337 

以上、行列積のプログラムを例にプリフェッチの使用法を説明しましたが、全ての場合において、このプリフェッチ制御を行えば性能が向上するわけではありません。実際は、ループ内ではもっと複雑な演算を行うことが多く、キャッシュの活用の状況を見ることもできません。従って、この機能を有効に活用するためには、プログラムの中で最も計算コストが掛かっている部分を分析し、そのデータ参照パターンを理解した上で、何回かコンパイラ・フラグを変更して試行錯誤を行う必要があります。


この画面トップへ

技術情報 TIPS トップに戻る >>




 ソフテックは、PGI 製品の公認正規代理店です

サイトマップ お問合せ
Copyright 2004-2006 SofTek Systems Inc. All Rights Reserved.