Code Modernization – コード・モダニゼーション (現代化) とは?

同カテゴリーの次の記事

インテル® Xeon Phi™ コプロセッサー開発者向けリソースガイド

この記事は、インテル® デベロッパー・ゾーンに公開されている「What is Code Modernization?」の日本語参考訳です。


現代のハイパフォーマンス・コンピューターは、次のリソースを組み合わせて構築されています:
マルチコア・プロセッサーメニーコア・プロセッサー、大きなキャッシュ、高速メモリー、高帯域幅のプロセッサー間通信ファブリックそして高スピード I/O 能力。

ハイパフォーマンス・ソフトウェアは、これら豊富なリソースの利点を完全に活用するように設計される必要があります。最大限のパフォーマンスを得る、もしくは既存のシステムと将来のシステムに向けて新しいアプリケーションを設計するため、既存のアプリケーションを再設計および (もしくは) チューニングするかどうかにかかわらず、プログラミング・モデルとリソースの効率良い利用の相互関係を認識することが重要です。Code Modernization (コード・モダニゼーション) に関する情報向けの出発点として検討してください。パフォーマンスが求められるとき、コード自身が鍵となります。

ソフトウェアを並列化すると、データセットを処理する時間の短縮、一定時間に処理できるデータセット数の増加、スレッド化されていないソフトウェアでは扱うことが困難な大規模なデータセットの処理などが可能になります。並列化が成功したかどうかは、通常、シリアルバージョンに対する並列バージョンのスピードアップを測定して判断します。また、得られたスピードアップの値を期待値の上限と比較することも大切です。スピードアップの期待値は、アムダールの法則とグスタフソンの法則から求めることができます。

優れたコード設計では、いくつかのレベルの並列性を考慮します。

  • 最初のレベルの並列性は、特定の計算命令で大きなデータのチャンクを処理するベクトル並列 (コア内で) です。スカラーとコードの並列領域 (スレッドやプロセス) の両方で、ベクトル演算を使用した効率化の恩恵を受けられます。
  • 2 番目のレベルはスレッド並列と呼ばれる並列性であり、共有メモリーを介して通信するシングルプロセス内の複数のスレッドが協調し、与えられたタスクを一括して処理する特徴を持ちます。
  • 3 番目のレベルは、多くのコアがメッセージ・パッシング・システムを介して相互に通信する、独立したプロセスが強調する方式で開発されます。これは、分散メモリーランク並列と呼ばれ、各プロセスには固有の番号が与えられます。

これら 3 つのレベルの並列性を効果的に、効率良く、そして高いパフォーマンスのコードを開発することで、コードのモダン化が達成されます。

これらの事項の要因として考慮するのは、マシンのメモリーモデルの影響です: それには、 メインメモリーのスピードと容量、メモリー位置に対するアクセス時間、キャッシュサイズと数、そしてメモリー・コヒーレンシーの要件などがあります。

ベクトル並列におけるデータアライメントの欠如は、パフォーマンスに大きな影響を与えます。データは、キャッシュを意識して構成されるべきです。そうしないと、アプリケーションがキャッシュにないデータを要求すると、パフォーマンスが低下します。最も高速なメモリーアクセスは、要求されたデータがすでにキャッシュに格納されている場合に達成できます。キャッシュへの (からの) データ転送はキャッシュライン単位で行われます。次にアクセスされるデータが現在のキャッシュラインに存在しないか、2 つのキャッシュラインに分割されている場合、アプリケーションはキャッシュを効率良く活用しているとは言えません。

除算や超越数数学関数は、命令セットで直接サポートされる場合であってもコストが高い演算です。アプリケーションが除算や平方根演算を多用している場合、パフォーマンスはハードウェア内の機能ユニットの制限により低下します。そして、これらのユニットへのパイプラインが影響を受けます。これらの命令はコストが高く、開発者はパフォーマンスを向上させるため頻繁に使用される値をキャッシュすることを期待するでしょう。

“1 つの秘訣、1つの解決策” のようなテクニックはありません。コードの問題に依存する多くが解決されるか、長期的な要求であるかによって異なりますが、優れた開発者は現在と将来の両方の要件について最適化のすべてのレベルに注意を払うでしょう。

コンパイラー、ライブラリー、デバッガー、パフォーマンス・アナライザー、並列化向けの最適化ツールなど、インテルは Code Modernization を支援する完全なツールスイートを構築しています。また、並列コンピューター開発の先駆者としての 30 年以上の経験に基づいて、ウェビナー、ドキュメント、トレーニング、そしてよく知られた手法やケーススタディーを提供します。

マルチレベルの並列性のための Code Modernization の 5 つのステージのフレームワーク

Code Modernization 最適化フレームワークは、アプリケーションのパフォーマンスを向上するため体系的なアプローチを取ります。このフレームワークは、5 つの最適化ステージを通して、各ステージは反復的にアプリケーションのパフォーマンスを改善します。しかし、最適化のプロセスに取り掛かる前に、アプリケーションのパフォーマンスを最大限に高めるため再設計 (以下にガイドラインを示します) が必要であるかどうかを検討し、Code Modernization のフレームワークに従ってください。

このフレームワークに従うことで、アプリケーションはインテル® アーキテクチャー上で引き出すことができる最高のパフォーマンスを達成できます。段階的なアプローチにより、開発者は最短時間で最高のアプリケーション・パフォーマンスを達成できます。それは、プログラムが実行環境のすべての並列ハードウェア・リソースを最大限に利用することを可能にする、と言い換えることができます。
ステージ:

  1. 最適化ツールとライブラリーの活用:: インテル® VTune™ Amplifier を使用してワークロードをプロファイルすることでホットスポットを特定し、インテル® Advisor XE でベクトル化とスレッド化の可能性を見つけ出します。インテル® コンパイラーを使用して最適なコードを生成し、可能であればインテル® MKLインテル® TBB、そして OpenMP* など最適化されたライブラリーを適用します。
  2. スカラー、シリアルの最適化: 適切な精度と型定数を維持し、適切な機能と精度フラグを使用します。
  3. ベクトル化: データレイアウトの最適化によりキャッシュにアラインしたデータ構造にして関連する SIMD 機能を利用し、構造体配列から配列構造体へ変換して、条件付きロジックを最小限に抑えます。
  4. スレッド並列: スレッドのスケーリングをプロファイルし、スレッドのコアへのアフィニティーを制御します。スケーリングの問題は、一般的にスレッド間の同期とメモリー利用の非効率によって生じます。
  5. マルチコアからメニーコアへアプリケーションをスケールさせる (分散メモリーランク整列): 高度に並列化されたアプリケーションにとって、スケーリングは特に重要です。実行ターゲットが特定のインテル® アーキテクチャー (例えばインテル® Xeon® プロセッサー) から別のアーキテクチャー (インテル® Xeon Phi™ コプロセッサー) へ変更された場合、最小限の修正でパフォーマンスを最大化します。

5 Stages of code modernization

Code Modernization – 5 つのステージの実践

ステージ 1
最適化のプロジェクトを開始する前に、最適化する開発環境を選択します。ここでの決定は、後のステップに大きな影響を持つことになります。取得する結果に影響するだけでなく、作業量を大幅に軽減できます。最適化の適切な開発環境は、優れたコンパイラー・ツール、最適化され、すぐに利用できるライブラリー、および実行時にコードで何が起こっているかを正確に特定するデバッグとプロファイルのためのツールを提供します。インテル® Advisor XEツールのウェビナーをご覧ください。このツールは、ベクトル化とスレッド化の可能性を特定してくれます。

ステージ 2
アプリケーションから高いパフォーマンスを引き出すためのソリューションを使い果たしてしまったら、ソースコードの最適化プロセスを開始する必要があります。実際に並列プログラミングを開始するに際し、ベクトル化や並列化を行う前にアプリケーションが正しい結果を生成していることを確認する必要があります。同様に重要なことは、正しい答えを得るため最小限の操作を行っていることを確認することです。データとアルゴリズムに関連する次のような問題を見つけます:

  • 正しい浮動小数点精度を選択している。
  • 正しい近似の制度を選択します; 多項式と合理式。
  • 分岐するアルゴリズムを避ける。
  • 反復計算を使用することで、ループ処理の強度を軽減させます。
  • アルゴリズム内の条件分岐を無くすか最小限にします。
  • 一度計算された結果を流用することで、計算の重複を回避します。

言語に依存するパフォーマンスの問題に対処する必要があります。C/C++ を使用する場合、次の言語依存の問題があります:

  • 自動拡張 (プロモーション) を避けるためすべての定数に明示的な型を指定します。
  • double や float など C ランタイム関数の正しい型を選択します: exp() と expf()、abs() と fabs() など。
  • ポインターのエイリアシングを明示的にコンパイラーに伝えます。
  • 呼び出しのオーバーヘッドを排除するため、明示的に関数のインライン展開を指示します。

ステージ 3
ベクトルレベルの並列処理を試みます。最初に最内のループをベクトル化します。効率良いベクトルループのため、制御フローの分岐は最小限にし、メモリーアクセスに一貫性があることを確認してください。外部ループのベクトル化は、パフォーマンスを向上するテクニックの 1 つです。デフォルトでは、コンパイラーは入れ子のループ構造の最内ループをベクトル化しようと試みます。しかし、最内ループのボディーが小さすぎることがあります。その場合、ベクトル化は有益ではありません。しかしながら、外部ループに多くのワークが含まれている場合、要素関数、ストリップマイニング、SIMD プラグマ/宣言子を組み合わせて、効果的に外部ループをベクトル化できます。

  1. SIMD は、”パックされ” アライメントされた入力データを最も効率良く実行し、制御分岐によるペナルティーを被ります。さらに、アプリケーションがデータの近似性に焦点を置いて実装されている場合、最新のハードウェア上で良好な SIMD およびスレッドのパフォーマンスを得ることができます。
  2. 最内のループが十分なワークを持っていない (例えば、ループカウントが非常に少ない場合、ベクトル化のパフォーマンス上の利点を測定できます)、または依存関係が最内ループのベクトル化を妨げているなら、外部ループのベクトル化を試みます。外部ループは制御フローの分岐を有する可能性があります; 特に内部ループの反復数は、外部ループの反復ごとに異なる可能性があります。これは、ベクトル化の利点を制限します。外部ループでのメモリーアクセスは、内部ループより発散的である可能性があります。これにより、ベクトルロードとストア命令の代わりに scatter/gather 命令が生成され、ベクトル化のスケーリングが大きく制限されることになります。二次元配列の置き換えなどのデータ変換は、それらを軽減します。構造体配列から配列構造体への変換を参照ください。
  3. ループ階層が浅い場合、上記のガイドラインは、ループは並列化とベクトル化の両方の対象となる可能性があります。その場合、ループはオーバーヘッドを相殺できる十分なワークと、制御フローの均一性とメモリーアクセスの一貫性を持たなければいけません。
  4. 詳細は、ベクトル化の基本をご覧ください。

ステージ 4
スレッドレベルの並列性に取り組みます。最外レベルを特定し、並列化を試みてください。ここでは明らかに潜在的なデータ競合を制御し、必要に応じてデータ宣言をループ内部に移動する必要があります。また、複数の並列パスによるデータアクセス維持のオーバーヘッドを軽減するため、データはキャッシュを効率良く活用する必要があります。最外レベルの原理は、個々のスレッドにできるだけ多くのワークを提供することです。アムダールの法則の状態: 並列コンピューティングにおけるマルチプロセッサーを使用したプログラムのスピードアップは、プログラムのシーケンシャル領域の実行時間によって制限されます。ワークの量が並列化のオーバーヘッドを相殺する必要があるため、可能な限り各スレッドは大きな並列処理を持つことになります。データの依存関係を避けられないため、最外レベルの並列化が不可能である場合、正しく並列化できる 1 つ内側のレベルで並列化を試してください。

  1. ターゲット・ハードウェア向けに最外レベルで十分な量の並列ワークが達成でき、並列リソースの増加に伴うスケーリングが妥当であるならば、皆さんの作業は終了です。オーバーヘッドが顕著になり (スレッド制御のオーバーヘッドはパフォーマンスの向上に悪影響を与えます)、ゲインが期待できないような多くの並列性は追加しないでください。
  2. 並列ワークの量が十分でない場合 (例えば、実際のコア数ではなく、コア数にスケールする) 、適用可能な他の外部レイヤーの並列化を試してください。必ずしもすべての利用可能なコアにループ階層をスケールする必要がないことに注意してください。並列に実行されるその他のループ階層があるかもしれません。
  3. ステップ 2 でスケーラブルなコードを作成できなかった場合、アルゴリズムに十分な並列ワークがない可能性があります。これは、多くのスレッド間で固定量のワークを分割することは、各スレッドのワークが極めて小さくなり、スレッドの開始と終了のオーバーヘッドがワークを相殺してしまうことになります。その場合、アルゴリズムにより大きな問題サイズを与えることで、スケールするようになります。
  4. さらに並列アルゴリズムが、キャッシュを効率良く利用することを確認してください。そうでない場合、並列アルゴリズムはスケールしないためキャッシュ効率を見直す必要があります。
  5. 詳しくは、マルチスレッド・アプリケーション開発のためのガイドをご覧ください。

ステージ 5
最後に複数ノード (ランク) 並列を導入します。多くの開発者にとって、メッセージ・パッシング・インターフェイス (MPI) は、ある MPI タスク (プロセス) からほかへデータを転送するために動作している「ブラックボックス」として見えます。開発者にとって MPI の優れた点は、アルゴリズムのコーディングがハードウェアに依存しないことです。開発者が持つ懸念は、60 個以上のコア・アーキテクチャーとタスク間通信が、ノード内もしくはノード間で通信ストームを構築できるかどうかということでしょう。通信のボトルネックを軽減するため、アプリケーションはいくつかの MPI タスクと多くの OPenMP* スレッドによるハイブリッド技術を導入する必要があります。

うまく最適化されたアプリケーションは、ベクトル並列、マルチスレッドにより並列、そして複数ノード (ランク) による並列化に取り組むべきです。しかし、これらの作業を効率良く行うには、各ステージレベルが考慮されたことを確認することで標準化されたステップごとの方法論を導入することは有用です。ここで説明するステージは、個々のアプリケーションにおける固有のニーズに応じて順番が変えることができます (そのほうが多いでしょう)。目標とするパフォーマンスを達成するため、ステージを複数回繰り返すこともできます。

経験的に、すべてのステージは、アプリケーションのパフォーマンスが今日利用可能なハードウェア上でスケールするだけでなく、将来登場するハードウェアでも効率良くスケールするように考慮される必要があります。

ぜひ試してください!

コンパイラーの最適化に関する詳細は、最適化に関する注意事項を参照してください。

関連記事