「認識を超えて最適化された」計算負荷の高いコードを文書化して教える方法は?


11

時折、最も重い種類の低レベルの最適化を必要とする十分に計算集約的なコードの1%があります。一般的な例としては、ビデオ処理、画像処理、およびあらゆる種類の信号処理があります。

目標は、コードが保守不能になったり、新しい開発者によって削除されたりしないように、最適化手法を文書化し、教えることです。(*)

(*)予測できない将来のCPUで特定の最適化が完全に役に立たない可能性があるにもかかわらず、コードは削除されます。

ソフトウェア製品(商用またはオープンソース)が最速のコードを持ち、最新のCPUアーキテクチャを使用することで競争上の優位性を保持していることを考えると、ソフトウェア作成者は特定の同じ出力を取得しながらコードを微調整する必要があることがよくあります少量の丸め誤差を許容するwhlist。

通常、ソフトウェアライターは、実行される各最適化/アルゴリズムの書き換えのドキュメントとして、関数の多くのバージョンを保持できます。これらのバージョンを他の人がどのようにして最適化手法を研究できるようにするのですか?

関連:


1
コードにさまざまなバージョンを残してコメントアウトし、読者に何が起こっているのかを多くのコメントで伝えることができます。
マイクダンラベイ

1
そして、コードが何をしているのかを伝えるだけでなく、なぜそのように高速であるのかを伝えてください。必要に応じて、独自の、Wikiのような、ドキュメントまたはインターネットで利用可能なリソースへのアルゴリズムへのリンクを含めます(その場合は、リンク腐敗に注意してください。 。)
マージャンヴェネマ

1
@MikeDunlavey:おっと、コメントアウトしないでください。同じ関数のいくつかの実装を用意し、最速のものを呼び出してください。そうすれば、コードの別のバージョンに簡単に切り替えて、すべてをベンチマークできます。
sleske

2
@sleskeバイナリコードを増やすだけで速度が低下する場合があります。
quant_dev

@quant_dev:はい、それは起こり得ます。コードを最新の状態に保つために、コードを(理想的には)定期的に構築および実行することが重要だと思います。たぶんデバッグモードでのみビルドしてください。
sleske

回答:


10

短い答え

最適化をローカルに保ち、明確にし、文書化し、ソースコードとランタイムパフォーマンスの両方の観点から、最適化されたバージョンを相互に、および最適化されていないバージョンと簡単に比較します。

完全な答え

そのような最適化が本当に場合はある製品への重要な、あなたは最適化の前に有用であっただけでなく、なぜ知っているが、また、彼らが将来的に有用であろうかどうかを知る助け開発者に十分な情報を提供する必要があります。

理想的には、ビルドプロセスにパフォーマンステストを組み込む必要があります。そのため、新しいテクノロジーが古い最適化を無効にする時期を見つけます。

覚えておいてください:

プログラムの最適化の最初のルール:しないでください。

プログラム最適化の2番目のルール(専門家のみ!):まだやらないでください。」

—マイケルA.ジャクソン

がその時かどうかを知るには、ベンチマークとテストが必要です。

あなたが言及したように、高度に最適化されたコードの最大の問題は、維持が難しいことです。そのため、可能な限り、最適化された部分を最適化されていない部分から分離する必要があります。コンパイル時のリンク、ランタイム仮想関数呼び出し、またはその間の何かを介してこれを行うかどうかは関係ありません。重要なのは、テストを実行するときに、現在関心のあるすべてのバージョンに対してテストできるようにすることです。

生産コードの基本的な最適化されていないバージョンを常に使用してコードの意図を理解できるようにシステムを構築し、最適化されたバージョンを含むこれと一緒に異なる最適化されたモジュールを構築し、どこでも明示的に文書化します最適化されたバージョンはベースラインとは異なります。テスト(ユニットおよび統合)を実行するときは、最適化されていないバージョンおよび現在最適化されているすべてのモジュールで実行します。

たとえば、高速フーリエ変換関数があるとします。たぶん、あなたはで基本的なアルゴリズムの実装fft.cとテストをしていfft_tests.cます。

次にPentiumが登場しfft_mmx.cMMX命令を使用して固定小数点バージョンを実装することにします。その後、pentium 3が登場し、Streaming SIMD Extensionsを使用するバージョンをに追加することにしましたfft_sse.c

CUDAを追加したいので、を追加fft_cuda.cしますが、何年も使用しているテストデータセットでは、CUDAバージョンはSSEバージョンよりも遅いことがわかります!いくつかの分析を行って、100倍大きいデータセットを追加すると、期待どおりのスピードアップが得られますが、CUDAバージョンを使用するためのセットアップ時間が非常に長く、小さなデータセットでは、そのセットアップコストのないアルゴリズム。

これらの各ケースでは、同じアルゴリズムを実装し、すべて同じように動作する必要がありますが、異なるアーキテクチャで異なる効率と速度で実行されます(まったく実行される場合)。ただし、コードの観点からは、ソースファイルの任意のペアを比較して、同じインターフェイスが異なる方法で実装されている理由を確認できます。通常、最も簡単な方法は、元の最適化されていないバージョンを参照することです。

同じことは、最適化されていないアルゴリズムを実装する基本クラスと派生クラスが異なる最適化を実装するOOP実装にも当てはまります。

重要なことは、同じものに保つことです同じになるように、違いは明白です


7

具体的には、ビデオおよび画像処理の例を取り上げているため、同じバージョンの一部としてコードを保持できますが、コンテキストに応じてアクティブまたは非アクティブにできます。

あなたが言及していない間、私はCここで仮定しています。

Cコードで最も簡単な方法は、最適化を行うことです(また、物事を移植可能にしようとする場合にも適用されます)。

 
#ifdef OPTIMIZATION_XYZ_ENABLE 
   // your optimzied code here... 
#else  
   // your basic code here...

#define OPTIMIZATION_XYZ_ENABLEMakefileでコンパイル中に有効にすると、すべてがそれに応じて機能します。

通常、最適化されている関数が多すぎると、関数の途中で数行のコードをカットするのが面倒になります。したがって、この場合、特定の機能を実行するために異なる機能ポインターを定義します。

メインコードは常に次のような関数ポインタを介して実行されます


   codec->computed_idct(blocks); 

ただし、関数ポインタは例のタイプに応じて定義されます(たとえば、ここでidct関数は異なるCPUアーキテクチャ向けに最適化されています。



if(OPTIMIZE_X86) {
  codec->computed_idct = compute_idct_x86; 
}
else if(OPTIMZE_ARM) {
  codec->computed_idct = compute_idct_ARM;
}
else {
  codec->computed_idct = compute_idct_C; 
}

あなたが表示されるはずのlibjpegコードとlibmpeg2コードをあってもよいffmpegのような技術のために。


6

研究者として、私はかなりの「ボトルネック」コードを書くことになります。ただし、いったん実稼働環境に取り込まれると、製品に統合してその後のサポートを提供する責任は開発者にあります。ご想像のとおり、プログラムの動作内容と動作方法を明確に伝えることが最も重要です。

このステップを正常に完了するには、3つの重要な要素があることがわかりました

  1. 使用するアルゴリズムは完全に明確でなければなりません。
  2. 実装の各ラインの目的は明確でなければなりません。
  3. 予想される結果からの逸脱は、できるだけ早く特定する必要があります。

最初のステップでは、アルゴリズムを文書化した短いホワイトペーパーを常に書きます。ここでの目的は、他の人がホワイトペーパーのみを使用してゼロから実装できるように、実際に書き上げることです。既知の公開されたアルゴリズムである場合、参照を提供し、重要な方程式を繰り返すだけで十分です。それがオリジナルの作品である場合、かなり明確にする必要があります。これはあなたを教えてくれますどのようなコードがされて行うことになって

開発に引き渡される実際の実装は、すべての微妙な点が明確になるように文書化する必要があります。デッドロックを回避するために特定の順序でロックを取得する場合は、コメントを追加してください。キャッシュの一貫性の問題のために、マトリックスの行ではなく列を反復処理する場合は、コメントを追加します。少しでも賢いことをしたら、コメントしてください。ホワイトペーパーを保証でき、コードが(VCSまたは同様のシステムを介して)決して分離されない場合は、ホワイトペーパーを参照できます。結果は簡単に50%を超えるコメントになる可能性があります。それは大丈夫だ。これにより、コードが何をするのがわかります。

最後に、変更に直面しても正確性を保証できる必要があります。幸いなことに、自動テスト継続的インテグレーションプラットフォームの便利なツールです。これらは、コードが実際にをしているを教えてくれます。

私の最も心のこもった推奨事項は、どのステップも省略しないことです。後で必要になります;)


包括的な回答をありがとう。あなたのすべてのポイントに同意します。自動化されたテストに関しては、固定小数点演算とSIMDコードの数値範囲を適切にカバーすることは困難であることがわかりました。コメントにのみ記載されている前提条件(強化するコードなし)は、常に満たされているわけではありません。
rwong

私がまだあなたの答えを受け入れていないのは、「短いホワイトペーパー」の意味と、それを作成するためにどのような努力が必要かについて、より多くのガイダンスが必要だからです。一部の業界では、これは主要な事業分野の一部ですが、他の業界ではコストを考慮しなければならず、合法的に利用可能なショートカットを使用する必要があります。
rwong

まず、自動テスト、浮動小数点演算、並列コードに関する苦痛を感じます。すべての場合に有効な解決策はないのではないかと心配しています。通常、私はかなり寛容な仕事をしますが、あなたの業界では不可能かもしれません。
drxzcl

2
実際には、このホワイトペーパーは科学論文の最初の草案のように見えますが、「毛羽立った」部分はありません(意味のある紹介も、抽象的な最小限の結論/議論も、それを理解するのに必要な参考文献のみ)。この論文は、アルゴリズム開発および/またはアルゴリズム選択のレポートであり、不可欠な部分であると考えています。このアルゴリズムの実装を選択しました(スペクトルFFTなど)。それは何ですか?なぜ他のものよりもこれを選んだのですか?並列化の特徴は何ですか?労力は、選択/開発作業に比例する必要があります。
drxzcl

5

これは、コードの包括的なコメント化により、コードの重要なブロックごとに事前に説明的なコメントが付けられるまで、最もよく解決されると考えています。

コメントには、仕様またはハードウェア参照資料への引用を含める必要があります。

必要に応じて業界全体の用語とアルゴリズム名を使用します。たとえば、「アーキテクチャXはアライメントされていない読み取りに対してCPUトラップを生成するため、このDuffのデバイスは次のアライメント境界を満たします」。

何が起こっているのかを誤解しないように、面内変数の命名を使用します。ハンガリー語ではなく、2つの垂直ピクセル間の距離をバイト単位で記述する「ストライド」など。

また、高レベルの図とブロック設計を含む、人間が読める短い文書でこれを補完します。


1
同じプロジェクトで1つのものに一貫した用語を使用する(たとえば、「ステップ」、「アライメント」などの類似した意味の用語に対して「ストライド」を使用する)と役立ちます。複数のプロジェクトのコードベースを1つのプロジェクトに統合する場合、これはやや困難です。
rwong
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.