コードの現代化を成功に導く 3 つのアドバイス

HPC

この記事は、インテル® デベロッパー・ゾーンに公開されている「Three Pieces of Advice for Code Modernization Success」(https://software.intel.com/en-us/blogs/2016/01/04/three-pieces-of-advice-for-code-modernization-success) の日本語参考訳です。


プログラムを高速化するための開発者への 3 つのアドバイスについてブログの執筆依頼を受けたとき、最初に思い浮かんだのは “Location! Location! Location!” という言葉でした。そこから不動産を連想し、映画「Glengarry Glen Ross (邦題: 摩天楼を夢見て)」のブレイク (Alec Baldwin) の台詞: 「A-B-C. A-Always, B-Be, C-Concurrent.Always be concurrent.」を思い出しました。

この簡単な台詞には、さまざまなものが凝縮されています。私はこれまで、粒度、ロードバランス、タスク分割、ループの並列化など、個々のトピックについて多数の IDZ ブログを執筆してきましたが、正にそれらがアドバイスと言えます。それらのブログを見つけ、そのうち 3 つを読めば、この記事を最後まで読む必要はないかもしれません。あるいは、この記事を読んだ後に、個別のブログで詳細を確認するほうが良いでしょう。また、コンカレントおよび並列プログラミングに関する本も執筆しました。お手元にある方は、後半の任意の 3 つのページをお読みになれば、コードを高速化するための 3 つのヒントが得られるでしょう。この 3 ページを破ってしまった方は、もう一部お求めください (笑)。


The Art of Concurrency

並列コンピューティング技法

それはさておき、私は再びチャレンジをしようと思っています。よく考えると、私はプログラマーがコードを高速化するために知っておくべき 3 つのことを思いつきました。それは、
1) アーキテクチャーを理解する、
2) データ構造を整理する、
3) 計算を整理する、
の 3 つです。これらのトピックは若干「メタ」であり、多くの異なる分野とそれぞれの詳細をカバーするため、以下に説明しています。

アーキテクチャーを理解する

私は、並列プログラミングとインテル® ソフトウェア・ツールの使い方に関する教示に携わってきましたが、新しいインテル® プロセッサーが発表されると、同僚が新しいアーキテクチャーに関する資料とスライドを渡してくれました。このプレゼンテーションは、新しいプロセッサーに追加された機能をかなり詳しく説明するため、1 時間にも及ぶ内容でした。この資料の第 3 版改定前に、私はコース開発者にソフトウェア側の担当者としてハードウェアの詳細は 10 分以内にすることを提案しました。私たちは、ソフトウェア開発者向けに教育を行ってきましたが、プログラムのパフォーマンスを改善するチップ上のバススピードやクロックティック、そしてメモリー・リフレッシュなど具体的なものに関連付けられない場合、参加者は関心を持たない可能性があります。

一方、プログラマーではない科学や芸術などの分野の利用者であっても、プロセッサー・アーキテクチャーの基礎を完全に無視するというわけにはいきません。具体的な機能の詳細は変わるかもしれませんが、キャッシュメモリーがどのように動作し、メモリー階層がどのように構成され、CPU のレジスターがどのように利用されるか、最近ではベクトルレジスターが通常のレジスターとどのように異なり何が達成されるかなど、いくつかの基礎を知ることは重要です。コンピューター・サイエンスの学位プログラムでは、学生はコンピューター・アーキテクチャーのコースを履修し、これらのトピックを深く学ぶ必要があります。教育の一部としてプログラミングを学ぶ、ほかの学部の学生は、おそらくアーキテクチャーまでは学習しないでしょう。

私は、これらのトピックに関してプログラマーに十分な知識を提供し、彼らがコードを記述し実行するのにどのように関連するか説明するため 10 分程度の概要にまとめました。このアイディアと原則は、プログラマーがシリアルまたは並列、大部分のプログラミング言語において、アプリケーションの実行性能を改善する長い道のりに適用されます。この 10 分間の概要の後、プログラマーはさらに詳細なアーキテクチャーの説明の間、眠りに落ちる私の許可を待っていいます。

データ構造を整理する

データがコンピューター・システム内を移動する仕組みを理解した上で、モダンなコードの書き手は、プログラムの実行中にデータの移動とデータ構造がアライメントされることを保証することが不可欠です。多くの場合、プログラマーは自身のアプリケーション内のデータを整理する方法を考えます。一方、「明白な」方法として、実際にプロセッサーでデータをアクセスして、部分的なパフォーマンスを計測することです。

複素数の集合を例にこの作業を紹介します。複素数は、実数部と虚数部から成ります。プログラムで複素数を扱う明白な方法は、実数値と虚数値を格納する部位/メンバーを持つ構造体/クラスを作成するものです。複素数値の集合を必要とする場合、それらの構造体の配列を定義します。この構造体配列は、連続したメモリー位置にすべての実数値を保持するわけではなく、また、連続したメモリー位置にすべての虚数値を保持するわけでもありません。それらは互いにインターリーブされます。プログラムコードがすべての実数または虚数部だけの計算を行う場合、連続したメモリーに配置される関連データはキャッシュへの読み込みとベクトル化において最も利点があります。

前述の問題を解決するには、データ配列を再配置して配列構造体 (SoA) にすることです。これにより、実数値と虚数値を格納するそれぞれの配列を作成します。その後、2 つの配列を単一の配列に集成します。すべての実数 (および虚数) 値は単一の配列内にあるため、連続したメモリー位置に格納されます。実数 (および虚数) のセット全体を通して計算を行うため、コンパイラーは効率良くキャッシュにロードする命令コードを生成することができ、さらにベクトル命令によりこれらの計算を行うことが可能となります。

この修正による欠点は、自身でまだこのような修正を実現していない場合、変更が必要なコードが明らかではないということです。このような変更は、コードベース全体を修正することになるか、以降の保守に労力を費やすことになります。主となるデータ構造の再構成を検討する場合、変更によるパフォーマンスの利点とコードを変更する作業量のバランスを考えます。数百、数千または数百万回実行されるコードについては、コードの変更後、費用便益比率は変化をもたらさなければいけません。

計算を整理する

現代のコーディングの原則とその機能をサポートするプロセッサーを最大限に活用するには、コード内の計算の順番を変更する必要があるかもしれません。データ構造を変更すると、それに関連するコードの変更が必要になります。しかし、私はその先を考えます。

独立したループの操作を考えてみましょう。それらは簡単に並列実行することができます。では、ループ反復間に依存性が存在する場合はどうでしょう? これは、ループ内の構文のロジックに応じて、オール・オア・ナッシングの命題となるかもしれません。または、アルゴリズムが要求する実行順序に基づいて、別々のループに分割することが可能で、それらの一部は並列化できます。ループのボディーが大きく複雑である場合、小さなループに分割することで、それらの一部が独立したループ反復を持つことは利点となります。

連続したメモリー位置にデータを配置する場合、コードは連続した順番でデータにアクセスする必要があります。多次元データ構造体のメモリーの 1 次元ブロックに格納されるデータを利用する場合、構造体内のすべての要素へのアクセス順が異なると、値はメモリー境界にまたがりコードのパフォーマンスに影響します。適切なアクセスパターンにするため、配列インデックスを実行する入れ子になったループの順番を変えることはできますか? 例えば、行アクセスから列アクセスへ変更するためループの順番を逆にできますか? いくつかのアルゴリズムは、正しい解を得るには特定のアクセス順番を必要とするため、アルゴリズムが適切な順番で大部分の要素にアクセスすることを保証するようコードを変更する必要があります。

最後まで読んでいただいた皆さんは、これまでのアドバイスは「ロケーション (位置)」に関連があると思われるかもしれません。より高い実行性能を得るため、知るべき最も重要なプロセッサーのアーキテクチャーは、メモリー階層の関係を理解することであり、それらの「ロケーション (位置)」を相対的に知ることです。そして、より適切なアクセスに向けてデータ構造の配置に影響する構造体の値の位置を制御することです。最後に、コードブロックの配置とどのようにデータ構造にアクセスするかは、シリアル実行のパフォーマンスを向上するだけでなく、コンパイラーが並列実行可能なコードを認識し、ベクトル化する可能性を高めます。私にとって、ここで触れた 3 つのアドバイスは、コードの現代化を成功に導く試みの原則です。

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

タイトルとURLをコピーしました