本文に進む 日本−日本語
日本HPホーム 製品とサービス お客様サポート/ ダウンロード ソリューション ご購入の方法
≫ お問い合わせ
詳細検索オプション
日本HPホーム
HP-UX パラレル プログラミング ガイド > 第14章 不具合の究明と対策

誤ったキャッシュラインの共有

≫ 

テクニカル ドキュメント

PDF版
フィードバック
ここから本文が始まります

 ≫ 目次

 ≫ 用語集

 ≫ 索引

誤ったキャッシュラインの共有は一種のキャッシュスラッシングです。これは、パラレルプログラムの複数のスレッドが同一のキャッシュラインの異なるデータ項目を割り当てている場合に必ず発生します。本項では、データレイアウトの再構築とスレッド間にループ繰り返しを分散することによって誤ったキャッシュラインの共有を避ける方法について説明します。

次の Fortran のコードを考えてみましょう。

REAL*4 A(8)
DO I = 1, 8
A(I) = ...
.
.
.
ENDDO

1 回の繰り返しをそれぞれ実行する、8 つのスレッドがあると仮定します。A(1) はプロセッサのキャッシュラインの境界 (V2250 サーバーでは 32 バイト境界) にあるので、8 つの要素はすべて同一のキャッシュラインに存在します。たった 1 つのスレッドしか同時にキャッシュラインを「所有」できないので、このループが実際に順次に実行されるだけでなく、スレッドによるすべての割り当てにとって、直前の「所有者」のキャッシュの行を無効にすることが必要となります。これらの問題はパラレル化の利点をなくしてしまう可能性があります。

これらのことをすべて考慮に入れて、次のコードを考えてみましょう。

REAL*4 B(100,100)
DO I = 1, 100
DO J = 1, 100
B(I,J) = ...B(I,J-1)...
ENDDO
ENDDO

I ループでパラレルに作動する 8 つのスレッドがあると仮定してください。J ループは依存のため、パラレル化できません。表 14-2 「I ループのデフォルト分散」では、B(1,1) がキャッシュラインの境界にあると仮定して、配列がキャッシュラインにマップする方法を示しています。キャッシュラインの境界に位置する配列要素は、網掛けで示しています。

表 14-1 キャッシュラインへの配列の初期マッピング

1, 1 1, 2 1, 3 1, 4. . . 1, 99 1, 100
2, 1 2, 2 2, 3 2, 4. . . 2, 99 2, 100
3, 1 3, 2 3, 3 3, 4. . . 3, 99 3, 100
4, 1 4, 2 4, 3 4, 4. . . 4, 99 4, 100
5, 1 5, 2 5, 3 5, 4. . . 5, 99 5, 100
6, 1 6, 2 6, 3 6, 4. . . 6, 99 6, 100
7, 1 7, 2 7, 3 7, 4. . . 7, 99 7, 100
8, 1 8, 2 8, 3 8, 4. . . 8, 99 8, 100
9, 1 9, 2 9, 3 9, 4. . . 9, 99 9, 100
10, 1 10, 2 10, 3 10, 4. . . 10, 99 10, 100
11, 1 11, 2 11, 3 11, 4. . . 11, 99 11, 100
12, 1 12, 2 12, 3 12, 4. . . 12, 99 12, 100
13, 1 13, 2 13, 3 13, 4. . . 13, 99 13, 100
. . .. . .. . .. . .. . .. . .. . .
97, 1 97, 2 97, 3 97, 4. . . 97, 99 97, 100
98, 1 98, 2 98, 3 98, 4. . . 98, 99 98, 100
99, 1 99, 2 99, 3 99, 4. . . 99, 99 99, 100
100, 1100, 2100, 3100, 4. . .100, 99100, 100

 

HP コンパイラはデフォルトで各スレッドにほぼ同じ繰り返し回数を設定します。必要であれば、スレッドによって 1 つ余分の繰り返しを割り当てることがあります。これは、1 つのスレッドに対してすべての繰り返しを割り当てると終わります。表 14-2 「I ループのデフォルト分散」では、8 つのスレッドへの I ループのデフォルト分散を示しています。

表 14-2 I ループのデフォルト分散

スレッド ID繰り返し範囲繰り返し回数
0 1-1212
113-2513
226-3712
338-5013
451-6212
563-7513
676-8712
788-10013

 

このように繰り返しを分散すれば、スレッドはキャッシュラインを共有できます。たとえば、スレッド 0 は要素B(9:12,1) を、スレッド 1 は要素B(13:16,1) をそれぞれ同一のキャッシュラインに割り当てます。実際、すべてのスレッドはキャッシュラインを少なくとも 1 つの他のスレッドと共有します。ほとんどのスレッドは他のスレッド 2 つとキャッシュラインを共有します。この種の共有は、データレイアウトとコンパイラによる繰り返しの分散の結果なので、誤りだと言われます。この共有はアルゴリズム自体に固有なものではありません。したがって、この共有は次の処理によって縮小したり、除去することができます。

  1. データをキャッシュラインの境界に整列することによってデータレイアウトを再構築します。

  2. 繰り返しの分散を制御します。

誤った共有を避けるためにデータを整列する

誤ったキャッシュラインの共有の原因の一部はデータのレイアウトにあるので、誤った共有を避けるための第 1 の方法はこのレイアウトを調整することになります。典型的な調整方法はデータをキャッシュラインの境界に整列することです。一般に、配列を整列するとパフォーマンスが向上します。しかし、場合によってはパフォーマンスが低下することもあります。

誤った共有を避けるための第 2 の方法はループ繰り返しの分散を調整することです。この方法の説明は「キャッシュラインの境界に繰り返しを分散する」にあります。

キャッシュラインの境界に配列を整列する

前述の例での配列B はキャッシュラインの境界で始まるという仮定に注意してください。次の方法は、Fortran の配列を強制的にキャッシュラインの境界で開始させます。

  • 初期化していない COMMON ブロックを使用します (DATA 文のないブロック)。これらのブロックは 64 バイト境界で始まります。

  • ALLOCATE 文を使用します。これらの文は 64 バイト境界のアドレスを返します。この方法はパラレルな実行可能プログラムだけに適用できます。

次の方法は、C の配列を強制的にキャッシュラインの境界で開始させます。

  • 関数 malloc またはmemory_class_malloc を使用します。これらの関数は 64 バイト境界のポインタを返します。

  • 少なくとも 32 バイトの初期化していないグローバル配列または構造体を使用します。このような配列や構造体は 64 バイト境界に整列します。

  • 少なくとも 32 バイトの、C の external 記憶クラスの初期化していないデータを使用します。データは 64 バイト境界に整列します。

キャッシュラインの境界に繰り返しを分散する

デフォルトの繰り返しの分散によってスレッド 0 が繰り返し範囲 1〜12 で作動し、スレッド 1 が繰り返し範囲 13〜25 で作動する (スレッド 2、スレッド 3...) ということを思い出してください。キャッシュラインが配列の列に対して整列していても (表 14-2 「I ループのデフォルト分散」を参照してください)、依然として繰り返しの分散を変更する必要があります。 CHUNK_SIZE 属性を使用して、この分散を変更してください。

      REAL*4 B(112,100) 
COMMON /ALIGNED/ B
C$DIR PREFER_PARALLEL (CHUNK_SIZE=16)
DO I = 1, 100
DO J = 1, 100
B(I,J) = ...B(I,J-1)...
ENDDO
ENDDO

定数のCHUNK_SIZE 属性を指定しなければなりません。しかし、作動を分散して、1 つ以外のすべてのスレッドが同じ数の全キャッシュラインで作動し、残りのスレッドがキャッシュラインのどの部分でも作動するように、作動を分散することが理想的です。たとえば、次のことを仮定します。

NITS = 繰り返し回数

NTHDS = スレッドの数

LSIZE = ワードで表した行サイズ (8 は 4 バイトデータ、4 は 8 バイトデータ、2 は 16 バイトデータをそれぞれ表します)

理想的なCHUNK_SIZE は次のとおりです。

CHUNK_SIZE = LSIZE * (1 + ( (1 + (NITS - 1) / LSIZE ) - 1 )/NTHDS)

前述のコードでは、これらの数値は次のようになります。

NITS = 100

LSIZE = 8 ( 4 バイトデータ用の V2250 の境界での整列)

NTHDS =8

CHUNK_SIZE = 8 * (1 + ( (1 + (100 - 1) / 8 ) - 1) / 8) 
= 8 * (1 + ( (1 + 12 ) - 1) / 8)
= 8 * (1 + ( 12 ) / 8)
= 8 * (1 + 1 )
= 16

CHUNK_SIZE = 16 によって、スレッド 0, 1, ..., 6 はそれぞれ繰り返し 1〜16, 17〜32, ..., 81〜96 を実行します。スレッド 7 は繰り返し 97〜100 を実行します。この結果、誤ったキャッシュラインの共有はなくなり、パラレルパフォーマンスが大幅に向上します。

すべてのループに理想的なCHUNK_SIZE を指定することはできません。式

CHUNK_SIZE = x

(データサイズ (バイト) のx 倍が 32 の整数倍) を使用して、誤ったキャッシュラインの共有を削除してください。これは次の 2 つの条件を満たしている場合のみ可能です。

  • 配列がすでに正しく整列している (本項で説明したように)。

  • 最初の繰り返しが、割り当てられている各配列の最初の要素をアクセスする。たとえば、ループ DO I = 2, N ではループがI = 2 で始まるので、最初の繰り返しは配列の最初の要素にアクセスしません。したがって、この繰り返しの分散はキャッシュラインの整列に適合しません。

数字 32 を使用する理由は、V2250 サーバー用のキャッシュラインのサイズが 32 バイトだからです。

スレッド固有の配列要素

パラレルループで各スレッドが共有配列の 1 つの要素を更新することがあります。この配列はループ外でスレッド 0 によって次の処理を受けます。

次の Fortran のコードを考えてみましょう。このコードでは誤った共有が起ります。

      REAL*4 S(8) 
C$DIR LOOP_PARALLEL
DO I = 1, N
.
.
.
S(MY_THREAD()+1) = ... ! EACH THREAD ASSIGNS ONE ELEMENT OF S
.
.
.
ENDDO
C$DIR NO_PARALLEL
DO J = 1, NUM_THREADS()
= ...S(J) ! THREAD 0 POST-PROCESSES S
ENDDO

このコードの問題は、S のすべての要素が単一のキャッシュラインに存在する可能性があるので、代入を行うと誤った共有が生じるということです。解決法の 1 つは、次のコードで示すように、このコードを変更して異なるキャッシュラインにそれぞれ 1 つの要素を強制的に入れることです。

      REAL*4 S(8,8)
C$DIR LOOP_PARALLEL
DO I = 1, N
.
.
.
S(1,MY_THREAD()+1) = ... ! EACH THREAD ASSIGNS ONE ELEMENT OF S
.
.
.
ENDDO
C$DIR NO_PARALLEL
DO J = 1, NUM_THREADS()
= ...S(1,J) ! THREAD 0 POST-PROCESSES S
ENDDO

キャッシュラインを共有するスカラー

次のコードのように、パラレルタスクが、同一のキャッシュラインにある 1 つのスカラー変数に代入することがあります。

      COMMON /RESULTS/ SUM, PRODUCT 
C$DIR BEGIN_TASKS
DO I = 1, N
.
.
.
SUM = SUM + ...
.
.
.
ENDDO
C$DIR NEXT_TASK
DO J = 1, M
.
.
.
PRODUCT = PRODUCT * ...
.
.
.
ENDDO
C$DIR END_TASKS

不整列の配列を処理する

配列とループを使用する場合のキャッシュスラッシングの問題で一番多いのは、ループ内で代入される配列が互いに整列していないときに起こるものです。この原因としては、次のものが考えられます。

  • ルーチンに対してローカルな配列をスタックに割り当てている。

  • 配列の仮引き数に実際の引き数の最初の要素以外の要素を渡している。

  • 配列要素を異なるオフセットインデックスで割り当てている。

次の Fortran のコードを考えてみましょう。

COMMON /OKAY/ X(112,100) 
...
CALL UNALIGNED (X(I,J))
...
SUBROUTINE UNALIGNED (Y)
REAL*4 Y(*)
! Y(1) PROBABLY NOT ON A CACHE LINE BOUNDARY

Y(1) のアドレスはわかりません。しかし、Y の要素をこのルーチンで何度も代入している場合は、次の公式でデータの割り付け境界を計算してみる価値があります。

LREM = LSIZE - ( ( 
( LOC(Y(1))-4, LSIZE*
x) + 4) /x)

ここで

LSIZE 

は適切なキャッシュラインのワードサイズです。

x 

Y の要素のデータサイズです。

この場合、V2250 サーバー上のLSIZE は単精度ワード (8 ワード) で 32 バイトです。次のことに注意してください。

( ( MOD ( LOC(Y(1))-4, LSIZE*4) + 4) /4)

は 1, 2, 3, ..., LSIZE の集合の値を返すので、LREM の範囲は 0 から 7 までであるということです。

次に、

DO I = 1, N 
Y(I) = ...
ENDDO

のようなループを次のように変換します。

C$DIR NO_PARALLEL 
DO I = 1, MIN (LREM, N) ! 0 <= LREM < 8
Y(I) = ...
ENDDO
C$DIR PREFER_PARALLEL (CHUNK_SIZE = 16)
DO I = LREM+1, N
! Y(LREM+1) IS ON A CACHE LINE BOUNDARY
Y(I) = ...
ENDDO

最初のループでは、データの最初の (もしあれば) 部分のキャッシュラインからの要素を考慮しています。次のループはキャッシュラインの境界で始まり、CHUNK_SIZE で制御されているので、スレッド間の誤った共有を避けることができます。

依存を処理する

ループでのデータ依存は、パラレル化を妨害し、誤ったキャッシュラインの共有の削除を妨害することがあります。このような場合、一定の条件を満たせばパフォーマンスが向上します。

例として、次のコードを考えてみましょう。

COMMON /ALIGNED / P(128,128), Q(128,128), R(128,128)
REAL*4 P, Q, R
DO J = 2, 128
DO I = 2, 127
P(I-1,J) = SQRT (P(I-1,J-1) + 1./3.)
Q(I ,J) = SQRT (Q(I ,J-1) + 1./3.)
R(I+1,J) = SQRT (R(I+1,J-1) + 1./3.)
ENDDO
ENDDO

J ループ内のループ搬送依存が原因で、I ループしかパラレル化されていません。このループで誤ったキャッシュラインの共有が生じないように、繰り返しを分散することは不可能です。これらの配列に関係するすべてのループで同一のオフセットを常に使用すれば (できそうもありませんが)、次元を調整して、より良い繰り返しの分散が可能でしょう。

たとえば、次のコードは 8 つのスレッドで正しく作動する可能性があります。

      COMMON /ADJUSTED/ P(128,128), PAD1(15), Q(128,128),
> PAD2(15), R(128,128)

DO J = 2, 128
C$DIR PREFER_PARALLEL (CHUNK_SIZE=16)
DO I = 2, 127
P(I-1,J) = SQRT (P(I-1,J-1) + 1./3.)
Q(I ,J) = SQRT (Q(I ,J-1) + 1./3.)
R(I+1,J) = SQRT (R(I+1,J-1) + 1./3.)
ENDDO
ENDDO

QR の両方の宣言の前に 60 バイトをパディングすれば、すべてのJ に対してP(1,J)Q(2,J)、およびR(3,J) が 64 バイト境界に整列します。CHUNK_SIZE を 16 にしてこれを行えば、スレッドが、重複しない全キャッシュラインにデータを割り当てます。

CPU を多用するループでは、前述の問題すべてが組み合わさった問題に出会うことがよくあります。誤ったキャッシュラインの共有すべてを避けることは不可能です。しかし、問題を注意深く調べ、ここで説明した対処法を慎重に行えば、パラレルループのパフォーマンスを大幅に強化できます。

印刷用画面へ
プライバシー 本サイト利用時の合意事項
© Hewlett-Packard Development Company, L.P.