このトピックの情報は、パフォーマンスの拡張手法を使用していて、最適化しているアプリケーション形式の解析が済んでいることを前提に説明しています。
最適な手法を判断するためにアプリケーションのプロファイリングを行った後、コンパイラーによって行われた最適化および制限を確認します。そして、コンパイラー・レポートを使用して、次に何を行うかを決定します。
レポートの内容に応じて、コンパイラーが重要なアーキテクチャー上の機能を活用して最高レベルのパフォーマンスを達成できるように、オプション、宣言子を選択し、必要なコード修正を行います。
コンパイラー・レポートは、コンパイラーによって行われた仮定に基づいて、行うことができる処理とできない処理を示します。オプションと宣言子を色々と試すことで、コンパイラーが行った仮定を理解し、また、新しい最適化の手法やテクニックを導き出すことができます。
いくつかの重要な方法でコンパイラーを効率的に利用できます。
適切なレポートを読み、コンパイラーが行っていることとコンパイラーがコードに関して行った仮定を理解する。
アプリケーションから最高レベルのパフォーマンスが得られるように、特定のオプション、組み込み関数、ライブラリー、および宣言子を使用する。
ユーザーコードの代わりにマス・カーネル・ライブラリー (MKL) を使用します。または、ユーザーコードの代わりに F90 組み込み関数を呼び出します。
他の推奨する手法は、「最適化手法の適用」を参照してください。
メモリー・エイリアシングは、IA-64 アーキテクチャー・ベース・システム用インテル® コンパイラーの最適化に最も大きな影響を与える問題です。メモリー・エイリアシングは、2 つ以上のポインターで指定されたメモリーの場所に書き込みます。このような場合、コンパイラーが最適化しすぎないように注意する必要があります。コンパイラーが最適化しすぎると、予期しない動作 (正しくない結果、異常終了、その他) を引き起こすことがあります。
コンパイラーは、通常、モジュールや関数ベースで最適化を行うので、関数へ渡されるグローバル変数やローカル変数の使用を全体から見て判断していません。このため、コンパイラーは、通常、関数に渡されたすべてのポインターがエイリアスされると仮定します。コンパイラーは、エイリアスされないことがわかっているポインターにもこの仮定を行います。このような動作は、完全に安全なループがパイプライン化またはベクトル化されず、パフォーマンスが向上しないことを意味します。
ポインターがエイリアスされないことをコンパイラーに知らせる方法はいくつかあります。
-fno-alias (Linux*) または /Oa (Windows*) のような、包括的なコンパイラー・オプションを使用します。これらのオプションは、すべてのモジュールのポインターがエイリアスされないことをコンパイラーに知らせます。プログラムが正確であることの責任は、開発者が負うことになります。
-fno-fnalias (Linux) または /Ow (Windows) のような、あまり包括的ではないコンパイラー・オプションを使用します。これらのオプションは、関数の引数で渡されたポインターがエイリアスされないことをコンパイラーに知らせます。
関数の引数は、コンパイラーに明確にできる潜在的なエイリアシングの一般的な例です。関数に渡される引数はエイリアスされないことがわかっていますが、コンパイラーはエイリアスされると仮定します。しかし、これらのオプションは、関数の引数がエイリアスされないと仮定しても安全であることをコンパイラーに知らせます。このオプションは、-fno-nalias (Linux) または /Ow (Windows) オプションを使用してコンパイルされたモジュールのすべての関数に影響する点に注意してください。
IDVEP 宣言子を使用します。または、関数の指定されたループに適用する宣言子を使用します。すべての関数を指定するよりも正確です。宣言子は、指定されたループについては、ベクトルの依存性がないと断定します。本質的には、ポインターが指定されたループでエイリアシングしていないと言うのと同じことです。
パフォーマンスに大きな影響を与える別の問題は、非ユニットストライド方式におけるメモリーアクセスです。これは、内部ループが連続的にインクリメントし、隣接していない場所からメモリーにアクセスしていることを意味します。例えば、次のような行列乗算コードを考えてみます。
例 |
---|
!Non-Unit Stride Memory Access subroutine non_unit_stride_memory_access(a,b,c, NUM) implicit none integer :: i,j,k,NUM real :: a(NUM,NUM), b(NUM,NUM), c(NUM,NUM) ! loop before loop interchange do i=1,NUM do j=1,NUM do k=1,NUM c(j,i) = c(j,i) + a(j,k) * b(k,i) end do end do end do end subroutine non_unit_stride_memory_access |
配列に関連した最内ループがインクリメントされるときに、c[i][j] および a[i][k] の両方が連続するメモリーの場所にアクセスする点に注意してください。しかし、配列 b は、インデックス k および j のループでメモリー・ユニット・ストライドにアクセスしません。ループが b[k=0][j=0] を読み取った後、k ループが 1 から b[k=1][j=0] にインクリメントします。NUM がスキップされ、メモリーの場所 b[k][1], b[k][2] .. b[k][NUM] がスキップされてしまいます。
この問題に対処するには、ループ変換 (ループ交換とも呼ばれます) を行います。コンパイラーでループ交換を自動的に行ったとしても、必ずしもすべての機会を認識するとは限りません。
上記の例のメモリー・アクセス・パターンは、次の図のようになります。
上記の例に、ループ交換を行うために次のような変更を加えたとします。
例 |
---|
subroutine unit_stride_memory_access(a,b,c, NUM) implicit none integer :: i,j,k,NUM real :: a(NUM,NUM), b(NUM,NUM), c(NUM,NUM) ! loop after interchange do i=1,NUM do k=1,NUM do j=1,NUM c(j,i) = c(j,i) + a(j,k) * b(k,i) end do end do end do end subroutine unit_stride_memory_access |
ループ交換の後、メモリー・アクセス・パターンは次の図のようになります。