最後の手段のパフォーマンス最適化戦略[終了]


609

このサイトにはすでにパフォーマンスに関する質問がたくさんありますが、ほとんどすべてが非常に問題固有であり、かなり狭いと思います。そして、ほとんどすべてが時期尚早な最適化を避けるためにアドバイスを繰り返します。

仮定しましょう:

  • コードはすでに正しく機能しています
  • 選択したアルゴリズムは、問題の状況にすでに最適です
  • コードが測定され、問題のあるルーチンが分離されている
  • 最適化のすべての試みは、問題を悪化させないように測定されます

私がここで探しているのは、重要なアルゴリズムで最後の数パーセントまで絞り出すための戦略とトリックです。

理想的には、回答を言語にとらわれないようにし、該当する場合は提案された戦略の欠点を示してください。

私は自分の最初の提案とともに返信を追加し、Stack Overflowコミュニティが他に考えられることを楽しみにしています。

回答:


427

OK、あなたはそれが改善の余地がないと思われるところまで問題を定義しています。私の経験では、それはかなりまれです。1993年11月のドブス博士の記事でこれを説明しようとしましたが、従来のようにうまく設計された簡単なプログラムから始めて、明らかな無駄を省き、一連の最適化を行って、実時間を48秒から削減しました。 1.1秒になり、ソースコードのサイズは4分の1に減少しました。私の診断ツールはこれでした。変更のシーケンスは次のとおりです。

  • 最初に見つかった問題は、半分以上の時間を占めるリストクラスター(現在は「イテレーター」と「コンテナークラス」と呼ばれる)の使用でした。これらはかなり単純なコードに置き換えられ、時間が20秒に短縮されました。

  • 今や最大の時間を費やすのは、より多くのリスト作成です。パーセンテージとしては、以前はそれほど大きくありませんでしたが、現在は、より大きな問題が取り除かれたためです。私はそれをスピードアップする方法を見つけ、そして時間は17秒に落ちます。

  • 現在、明らかな犯人を見つけるのは難しいですが、私が何かできる小さなものはいくつかあり、時間は13秒に短縮されます。

今、壁にぶつかったようです。サンプルはそれが何をしているのか正確に教えてくれますが、改善できることは何も見つからないようです。次に、プログラムの基本設計とそのトランザクション駆動型の構造について考察し、実行中のすべてのリスト検索が実際に問題の要件によって義務付けられているかどうかを尋ねます。

次に、プログラムコードが実際には(プリプロセッサマクロを介して)より少ないソースセットから生成され、プログラマーがかなり予測可能であることをプログラムが常に把握しているわけではない、再設計を思いつきました。つまり、一連の処理を「解釈」せず、「コンパイル」します。

  • その再設計が行われ、ソースコードが4分の1に縮小され、時間が10秒に短縮されます。

さて、速くなっているためサンプリングが難しいので、10倍の労力を費やしていますが、以下の時間は元のワークロードに基づいています。

  • さらに診断すると、キュー管理に時間を費やしていることがわかります。これらをインライン化すると、時間が7秒に短縮されます。

  • 今や大いに時間をかけているのは、私が行っていた診断印刷です。それを洗い流してください-4秒。

  • 現在、最大の時間を費やしているのは、mallocfreeの呼び出しです。オブジェクトのリサイクル-2.6秒。

  • サンプリングを続けると、厳密に必要ではない操作がまだ見つかります-1.1秒。

総スピードアップ係数:43.6

現在、2つのプログラムが似ていることはありませんが、おもちゃ以外のソフトウェアでは、常にこのような進展が見られます。最初は簡単なものを手に入れ、次にリターンが減少するポイントに到達するまで、さらに難しくします。次に、得られた洞察は再設計につながり、再び減少するリターンに到達するまで、新しいラウンドのスピードアップを開始する可能性があります。さて、これが高速かどう++ii++for(;;)またはwhile(1)高速かどうか疑問に思うポイントです。スタックオーバーフローでよくある質問の種類です。

PSなぜプロファイラーを使わなかったのか不思議に思うかもしれません。その答えは、これらの「問題」のほとんどすべてが、サンプルを正確に示す関数呼び出しサイトであったということです。プロファイラーは、今日でも、関数全体よりもステートメントと呼び出し命令の方が見つけやすく、修正が簡単であるという考えにやっと近づいています。

私は実際にこれを行うためにプロファイラーを作成しましたが、コードが何をしているのかという実際の汚い親密さのために、あなたの指を正しくすることに代わるものはありません。見つかる問題は非常に小さいため、見逃されがちなので、サンプル数が少ないことは問題ではありません。

追加:jerryjvlがいくつかの例を要求しました。これが最初の問題です。これは少数の別々のコード行で構成され、合計で半分の時間を占めます。

 /* IF ALL TASKS DONE, SEND ITC_ACKOP, AND DELETE OP */
if (ptop->current_task >= ILST_LENGTH(ptop->tasklist){
. . .
/* FOR EACH OPERATION REQUEST */
for ( ptop = ILST_FIRST(oplist); ptop != NULL; ptop = ILST_NEXT(oplist, ptop)){
. . .
/* GET CURRENT TASK */
ptask = ILST_NTH(ptop->tasklist, ptop->current_task)

これらは、リストクラスターILST(リストクラスと同様)を使用していました。これらは通常の方法で実装され、「情報の非表示」は、クラスのユーザーが実装方法を気にする必要がないことを意味します。これらの行が記述されたとき(約800行のコードから)、これらは「ボトルネック」になる可能性があるとは考えられませんでした(その単語は嫌いです)。それらは単に物事を行うために推奨される方法です。これらを避けるべきだったと後で考えるのは簡単ですが、私の経験では、すべてのパフォーマンスの問題はそのようなものです。一般に、パフォーマンスの問題が発生しないようにすることをお勧めします。作成されたものを「避けなければならない」(後から見て)としても、作成して修正することをお勧めします。

2番目の問題が2行に分かれています。

 /* ADD TASK TO TASK LIST */
ILST_APPEND(ptop->tasklist, ptask)
. . .
/* ADD TRANSACTION TO TRANSACTION QUEUE */
ILST_APPEND(trnque, ptrn)

これらは、末尾にアイテムを追加してリストを作成しています。(修正は、配列で項目を収集し、リストを一度に作成することでした。)興味深いのは、これらのステートメントは元の時間の3/48しか費やさない(つまり、コールスタックにあった)ため、最初は大きな問題でした。しかし、最初の問題を取り除いた後、それらは時間の3/20を要し、現在は「より大きな魚」でした。一般に、それはそれが行く方法です。

このプロジェクトは、私が手助けした実際のプロジェクトから抽出されたものだと付け加えるかもしれません。そのプロジェクトでは、内部ループ内でデータベースアクセスルーチンを呼び出してタスクが終了したかどうかを確認するなど、パフォーマンスの問題ははるかに劇的でした(スピードアップと同様)。

追加された参照:オリジナルおよび再設計されたソースコードは、www.ddj.comの 1993年のファイル9311.zip、ファイルslug.ascおよびslug.zipにあります。

EDIT 2011/11/26:Visual C ++のソースコードと、それがどのように調整されたかについての詳細な説明が含まれているSourceForgeプロジェクトがあります。上記のシナリオの前半のみを通過し、まったく同じシーケンスには従いませんが、2〜3桁のスピードアップが得られます。


3
上記で概要を説明したステップの詳細の一部を読みたいと思います。フレーバーの最適化の一部を含めることは可能ですか?(投稿を長くしすぎないで?)
jerryjvl 2009年

8
...絶版の本も書いたので、Amazonでとんでもない価格になってしまいます-"Building Better Applications" ISBN0442017405。基本的に同じ資料が最初の章にあります。
Mike Dunlavey、2009年

3
@Mike Dunlavey、Googleにスキャン済みであることを伝えてください。彼らはおそらくあなたの出版社を買った人とすでに合意しているでしょう。
するThorbjörnRavnアンデルセン

19
@Thorbjørn:ただフォローアップするために、GoogleBooksに接続してすべてのフォームに記入し、ハードコピーを送った。私が本当に著作権を所有しているかどうかを尋ねるメールを受け取りました。出版社のVan Nostrand Reinholdは、International Thompsonが購入し、Reutersが購入しました。私が電話やメールを送信しようとすると、ブラックホールのようなものです。だから、それは大げさです-私はまだ本当にそれを追い払うエネルギーを持っていません。
Mike Dunlavey、2011

5
Googleブックスリンク: books.google.dk/books
id

188

提案:

  • 再計算ではなく事前計算:入力の範囲が比較的限定されている計算を含むループまたは繰り返し呼び出しの場合、有効範囲内のすべての値の計算結果を含むルックアップ(配列または辞書)の作成を検討してください入力。次に、代わりにアルゴリズム内の単純な検索を使用します。
    欠点:事前に計算された値の一部が実際に使用される場合、これは問題を悪化させる可能性があり、ルックアップにかなりのメモリが必要になる場合もあります。
  • ライブラリメソッドを使用しないでください。ほとんどのライブラリは、幅広いシナリオで正しく動作し、パラメーターなどに対してnullチェックを実行するように作成する必要があります。メソッドを再実装することで、多くのロジックを取り除くことができる場合があります。あなたがそれを使用している正確な状況では適用されません。
    欠点:追加のコードを書くことは、バグの表面積を増やすことを意味します。
  • ライブラリメソッドを使用する:私と矛盾するために、言語ライブラリはあなたや私よりもはるかに賢い人々によって書かれます。オッズは彼らがそれをより良くそしてより速くしたということです。実際に速くすることができない限り、自分で実装しないでください(つまり、常に測定してください!)。
  • チート:場合によっては、問題に対して正確な計算が存在する場合がありますが、「正確」でなくてもかまいません。場合によっては、「十分十分」であり、取引がはるかに速い場合があります。自問してみてください、答えが1%アウトであるかどうかは本当に重要ですか?5%?10%でも?
    欠点:ええと...答えは正確ではありません。

32
事前計算は常に役立つわけではなく、場合によっては害を及ぼす可能性もあります。ルックアップテーブルが大きすぎると、キャッシュのパフォーマンスが低下する可能性があります。
Adam Rosenfield、

37
多くの場合、不正行為が勝利につながります。私は、コアで3x3マトリックスが点在する3つのベクトルである色補正プロセスを持っていました。CPUにはハードウェアで行列乗算があり、いくつかのクロスタームが省略されており、他のすべての方法と比べて非常に高速でしたが、4x4行列とフロートの4ベクトルのみがサポートされていました。余分な空のスロットを処理するようにコードを変更し、計算を固定小数点から浮動小数点に変換すると、精度はやや低くなりますが、はるかに高速になります。
RBerteig 2009年

6
不正行為は、いくつかの内積を省略した行列乗算を使用していたため、個々の命令の同等のシーケンスよりも速く完了する単一のCPU命令のマイクロコードでの実装が可能になりました。「正しい」答えが得られないので、これはごまかしです。「十分に正しい」答えだけです。
RBerteig 2011

6
@RBerteig:ちょうど「十分に正しい」は、ほとんどの人が私の経験で見落としている最適化の機会です。
Martin Thompson

5
誰もがあなたより頭が良いといつも思うことはできません。最後に、私たちはすべてプロです。ただし、使用する特定のライブラリーが存在し、その品質のために環境に達していると想定することができます。したがって、このライブラリーの作成は非常に綿密である必要があり、そのことに特化していないという理由だけでそれを行うこともできませんフィールド、そしてあなたはそれに同じ種類の時間を投資しないでください。賢くないからではありません。いい加減にして。
v.oddou 2014

164

パフォーマンスをこれ以上改善できない場合- 知覚されるパフォーマンスを改善できるかどうかを確認してください。

fooCalcアルゴリズムを高速化することはできないかもしれませんが、多くの場合、アプリケーションをユーザーに対してより応答しやすく見せるための方法がいくつかあります。

いくつかの例:

  • ユーザーが何を要求するかを予測し、それまでにその作業を開始する
  • 最後に一度にすべてではなく、入ってくると同時に結果を表示する
  • 正確な進捗メーター

これらはあなたのプログラムを速くしませんが、あなたが持っている速度でユーザーをより幸せにするかもしれません。


27
最後にスピードアップするプログレスバーは、完全に正確なバーよりも速く認識される場合があります。「プログレスバーの再考」(2007)では、ハリソン、アメント、クズネツォフ、ベルがユーザーグループの複数のタイプのバーをテストし、進行をより速く感じるように操作を再配置する方法について説明しています。
EmilVikström12年

9
ナクサ、フローの大きく異なる複数のステップを単一のパーセンテージに予測することは困難または時々不可能であるため、ほとんどのプログレスバーは偽物です。99%で止まっているすべてのバーを見てください:-(
EmilVikströmJun

138

私は人生のほとんどをこの場所で過ごします。広範なストロークは、プロファイラーを実行して記録することです。

  • キャッシュミス。データキャッシュは、ほとんどのプログラムでストールの最大の原因です。問題のデータ構造を再編成して局所性を向上させることにより、キャッシュヒット率を向上させます。無駄なバイトを排除するために構造体と数値型をパックします(したがって、無駄なキャッシュフェッチ)。ストールを減らすために、可能な限りデータをプリフェッチします。
  • ヒットストアをロードします。ポインターのエイリアシングに関するコンパイラーの想定、およびデータがメモリを介して切断されたレジスタセット間で移動される場合、特定の異常な動作が発生し、CPUパイプライン全体がロード操作でクリアされます。float、vector、intが互いにキャストされている場所を見つけて、それらを排除します。__restrictエイリアシングについてコンパイラに約束するために自由に使用します。
  • マイクロコード化された操作。ほとんどのプロセッサには、パイプライン化できないいくつかの操作がありますが、ROMに格納されている小さなサブルーチンを実行します。PowerPCの例は、整数の乗算、除算、および変数量によるシフトです。問題は、この操作の実行中にパイプライン全体が停止することです。これらの演算の使用を排除するか、少なくともそれらを構成するパイプライン演算に分解して、プログラムの残りの部分でスーパースカラーディスパッチのメリットを享受できるようにしてください。
  • ブランチの予測ミス。これらもパイプラインを空にします。CPUが分岐後にパイプの再充填に多くの時間を費やしているケースを見つけ、可能であれば分岐ヒントを使用して、より正確に予測できるようにします。さらに良いことに、パイプは通常より深く、fcmpの後で条件フラグを読み取るとストールが発生する可能性があるため、特に浮動小数点演算の後で、可能な限り分岐を条件付き移動に置き換えます。
  • 順次浮動小数点演算。これらをSIMDにします。

そして、私がしたいもう一つのこと:

  • コンパイラーがアセンブリー・リストを出力するように設定し、コード内のホットスポット関数で何が発生するかを調べます。「優れたコンパイラーが自動的に実行できるはずの」これらすべての巧妙な最適化?おそらく、実際のコンパイラはそれらを実行しないでしょう。GCCが本当にWTFコードを出力するのを見てきました。

8
私は主にIntel VTuneとPIXを使用しています。それらがC#に適応できるかどうかはわかりませんが、実際にJIT抽象化レイヤーを取得すると、これらの最適化のほとんどは、キャッシュの局所性の向上と一部の分岐の回避を除いて、手の届かないものになります。
Crashworks、

6
それでも、JIT後の出力を確認すると、JITステージで最適化されていない構成があるかどうかを判断するのに役立つ場合があります。行き止まりが判明しても、調査によって害が及ぶことはありません。
jerryjvl 2009年

5
私を含め、多くの人がgccが作成するこの「wtfアセンブリ」に興味を持っていると思います。あなたの仕事は非常に興味深い仕事のように聞こえます:)
BlueRaja-Danny Pflughoeft

1
Examples on the PowerPC ...<-つまり、PowerPCのいくつかの実装。PowerPCはISAであり、CPUではありません。
ビリーONeal 2013

1
@BillyONeal最新のx86ハードウェアでも、imulはパイプラインを停止させることができます。「インテル®64およびIA-32アーキテクチャー最適化リファレンスマニュアル」を参照してください。§13.3.2.3:「整数乗算命令は、実行に数サイクルかかります。整数乗算命令と別の長いレイテンシー命令がパイプライン化され、実行フェーズ。ただし、整数乗算命令は、プログラムの順序の要件により、他のシングルサイクル整数命令の発行をブロックします。」そのため、通常はワード境界で配列された配列サイズとを使用する方が適切leaです。
Crashworks 2013年

78

さらに多くのハードウェアを投入してください!


30
すでに現場に出ているハードウェアで実行することが予想されるソフトウェアがある場合、ハードウェアを増やすことは必ずしも選択肢ではありません。
Doug T.

76
消費者向けソフトウェアを作っている人にとってはあまり役に立たない回答です。顧客は、「より高速なコンピュータを購入する」というあなたの言うことを聞きたくないでしょう。特に、ビデオゲームコンソールなどを対象とするソフトウェアを作成している場合は、
Crashworks、

19
@Crashworks、またはさらに言えば、組み込みシステム。最後の機能がようやく
導入

71
メモリリークが大きいプログラムをデバッグする必要がありました。そのVMサイズは1時間あたり約1Mb増加しました。同僚は、私がする必要があるのは一定の割合でメモリ追加することだけだと冗談を言った。:)
j_random_hacker

9
ハードウェアの増加:ああそうです、平凡な開発者のライフライン。「別のマシンを追加して容量を2倍にする」と聞いた回数がわかりません。
Olof Forshell、2011年

58

その他の提案:

  • I / Oを回避する:I / O(ディスク、ネットワーク、ポートなど)は常に、計算を実行するコードよりもはるかに遅くなるため、厳密に必要としないI / Oは削除してください。

  • I / Oを前に移動する:重要なアルゴリズムのコア内でI / O待機が繰り返されないように(そして結果としておそらく繰り返されないように)、計算に必要なすべてのデータを前もってロードします。ディスクシーク。1回のヒットですべてのデータをロードする場合、シークを回避できます)。

  • I / Oの遅延:計算が終了するまで結果を書き出さず、データ構造に保存し、ハードワークが完了したときに最後に一度にダンプします。

  • スレッド化されたI / O:十分に大胆な場合は、「I / O先行」または「遅延I / O」を実際の計算と組み合わせて、ロードを並列スレッドに移動して、より多くのデータをロードしているときに作業できるようにします。既存のデータの計算時、またはデータの次のバッチの計算中に、最後のバッチの結果を同時に書き出すことができます。


3
「IOの並列スレッドへの移動」は、多くのプラットフォーム(Windows NTなど)で非同期IOとして実行する必要があることに注意してください。
ビリーONeal 2013

2
I / Oは非常に重要なポイントです。低速で非常に長いレイテンシがあり、このアドバイスを使用すると速くなる可能性がありますが、それでも根本的な欠陥があります。ポイントはレイテンシ(非表示にする必要があります)とsyscallオーバーヘッド(これは、I / O呼び出しのを減らすことによって減らす必要があります)。最善のアドバイスはmmap()、入力に使用し、適切なmadvise()呼び出しを行いaio_write()、出力の大きなチャンク(=数MiB)を書き込むために使用することです。
cmaster-モニカを2013年

1
この最後のオプションは、特にJavaでの実装がかなり簡単です。私が書いたアプリケーションのパフォーマンスが大幅に向上しました。もう1つの重要な点(I / Oを前に移動する以上のこと)は、それを順次およびラージブロックI / Oにすることです。ディスクのシーク時間のため、大量の小さな読み取りは1つの大きな読み取りよりもはるかに高価です。
BobMcGee 2013

ある時点で、計算の前に一時的にすべてのファイルをRAMディスクに移動し、後でそれらを元に戻すことで、I / Oを回避することで浮気をしました。これはダーティですが、I / O呼び出しを行うロジックを制御しない状況で役立つ場合があります。
-MD

48

パフォーマンスの問題の多くにはデータベースの問題が関係しているので、クエリとストアドプロシージャをチューニングする際に注意すべき点をいくつか示します。

ほとんどのデータベースではカーソルを避けてください。ループも避けてください。ほとんどの場合、データアクセスはレコードごとの処理ではなく、セットベースで行う必要があります。これには、1,000,000レコードを一度に挿入する場合に、単一レコードのストアドプロシージャを再利用しないことが含まれます。

select *を使用しないでください。実際に必要なフィールドのみを返します。結合フィールドが繰り返され、サーバーとネットワークの両方に不要な負荷がかかるため、結合がある場合は特にそうです。

相関サブクエリの使用を避けます。結合を使用します(可能な場合は派生テーブルへの結合を含みます)(これはMicrosoft SQL Serverにも当てはまりますが、別のバックエンドを使用する場合はアドバイスをテストしてください)。

インデックス、インデックス、インデックス。データベースに該当する場合は、これらの統計を更新してください。

クエリを検索可能にします。like句の最初の文字にワイルドカードを使用するなどのインデックスの使用を不可能にするものや、結合内の、またはwhereステートメントの左側として関数を使用しないことを意味します。

正しいデータ型を使用してください。文字列データ型を日付データ型に変換してから計算を行うよりも、日付フィールドで日付計算を行う方が高速です。

いかなるループもトリガーに含めないでください!

ほとんどのデータベースには、クエリの実行方法を確認する方法があります。Microsoft SQL Serverでは、これは実行プランと呼ばれます。最初にそれらをチェックして、問題のある領域がどこにあるかを確認してください。

最適化する必要があるものを決定するときは、クエリの実行頻度と実行にかかる時間を考慮してください。1か月に1回しか実行されないlong_runningクエリから時間を一掃するよりも、1日数百万回実行されるクエリを少し調整するだけで、パフォーマンスを向上できる場合があります。

ある種のプロファイラーツールを使用して、データベースとの間で実際に何が送信されているかを調べます。ストアードプロシージャが高速で、プロファイリングによってWebページがクエリを1回ではなく何度も要求していることが判明したときに、ページの読み込みが非常に遅い理由を理解できなかった過去のことを1回覚えています。

プロファイラーは、誰が誰をブロックしているかを見つけるのにも役立ちます。単独で実行しているときにすばやく実行されるクエリの中には、他のクエリからのロックが原因で非常に遅くなるものがあります。


29

今日の最も重要な制限要因の1つは、限られたメモリ帯域幅です。です。帯域幅はコア間で共有されるため、マルチコアはこれをさらに悪化させています。また、キャッシュの実装専用の限られたチップ領域もコアとスレッドに分割されるため、この問題はさらに悪化します。最後に、コアの数が増えると、異なるキャッシュの一貫性を保つために必要なチップ間シグナリングも増加します。これもペナルティを追加します。

これらは、管理する必要がある影響です。コードをマイクロ管理することもあれば、慎重に検討してリファクタリングすることもあります。

多くのコメントがすでにキャッシュフレンドリーなコードについて言及しています。これには少なくとも2つの異なるフレーバーがあります。

  • メモリフェッチのレイテンシを回避します。
  • メモリバスの負荷(帯域幅)を下げます。

最初の問題は、具体的にはデータアクセスパターンをより規則的にし、ハードウェアプリフェッチャーが効率的に機能できるようにすることと関係があります。データオブジェクトをメモリ内に分散する動的メモリ割り当てを回避します。リンクされたリスト、ハッシュ、ツリーの代わりに線形コンテナーを使用します。

2番目の問題は、データの再利用の改善に関係しています。使用可能なキャッシュに収まるデータのサブセットで動作するようにアルゴリズムを変更し、キャッシュ内にある間はそのデータを可能な限り再利用します。

データをより密にパックし、すべてのデータをホットループのキャッシュラインで確実に使用すると、これらの他の影響を回避し、より有用なデータをキャッシュに収めることができます。


25
  • どのハードウェアを実行していますか?プラットフォーム固有の最適化(ベクトル化など)を使用できますか?
  • より良いコンパイラを入手できますか?たとえば、GCCからIntelに切り替えますか?
  • アルゴリズムを並行して実行できますか?
  • データを再編成してキャッシュミスを減らすことはできますか?
  • アサーションを無効にできますか?
  • コンパイラとプラットフォームに合わせてマイクロ最適化します。「if / elseでは、最も一般的なステートメントを最初に置く」のスタイルで

4
「GCCからLLVMに切り替える」である必要があります:)
Zifre 2009年

4
アルゴリズムを並行して実行できますか?-逆も適用されます
ジャスティン

4
確かに、スレッドの量を減らすことは、同様に優れた最適化となる可能性があります
Johan Kotlinski '30 / 4/30

re:マイクロ最適化:コンパイラーのasm出力を確認する場合、ソースを調整してそれを手で保持し、より良いasmを生成することができます。Collat​​z予想をテストするために、このC ++コードが手書きのアセンブリよりも高速である理由を参照してください最新のx86でのコンパイラーの支援または打撃についての詳細。
Peter Cordes 2017年

17

私はマイク・ダンレイビーの答えが好きですが、実際にはそれは裏付けとなる例としては素晴らしい答えですが、次のように非常に簡単に表すことができると思います。

最初に最も時間がかかるものを見つけ、その理由を理解します。

アルゴリズムをどこに改良する必要があるかを理解するのに役立つのは、タイムホッグの識別プロセスです。これは、すでに完全に最適化されているはずの問題に対して私が見つけることができる、言語にとらわれない唯一の包括的な回答です。また、速度の追求において、アーキテクチャに依存しないことを想定しています。

したがって、アルゴリズムは最適化されているかもしれませんが、実装はそうではないかもしれません。識別により、どの部分がどれであるかを知ることができます:アルゴリズムまたは実装。したがって、最も時間を費やすのはどれでも、レビューの最有力候補です。しかし、最後の数%を絞る必要があると言ったので、小さい部分、つまり最初は詳しく調べていなかった部分も調べることをお勧めします。

最後に、同じソリューションを実装するためのさまざまな方法、または潜在的に異なるアルゴリズムに関するパフォーマンスの数値を少し試行錯誤することで、時間の浪費者と時間の節約者を特定するのに役立つ洞察を得ることができます。

HPH、asoudmove。


16

おそらく「Googleの視点」を検討する必要があります。つまり、アプリケーションがどのように大幅に並列化および並行化できるかを決定する必要があります。これは、ある時点で、異なるマシンやネットワークにアプリケーションを分散することを検討することを必然的に意味し、理想的にはほぼ線形にスケーリングできるようにします。あなたがそれに投げかけるハードウェアで。

一方、Googleの人々は、使用しているプロジェクト、ツール、インフラストラクチャの問題を解決するために多くの人材とリソースを投入することでも知られています。たとえば、専任のエンジニアチームによるgccのプログラム全体の最適化などです。 Googleの一般的なユースケースシナリオに備えてgccの内部をハッキングする。

同様に、アプリケーションのプロファイリングは、プログラムコードだけでなく、システムの観点から冗長性と最適化の可能性を特定するために、すべての周囲のシステムとインフラストラクチャ(ネットワーク、スイッチ、サーバー、RAIDアレイなど)もプロファイリングすることを意味しません。


15
  • インラインルーチン(呼び出し/戻りとパラメーターのプッシュを排除)
  • テーブルルックアップでテスト/スイッチを排除してみてください(高速の場合)。
  • ループ(Duffのデバイス)を、CPUキャッシュにちょうど収まるところまで展開します。
  • キャッシュを破壊しないようにメモリアクセスをローカライズする
  • オプティマイザがまだ実行していない場合は、関連する計算をローカライズします
  • オプティマイザがまだ実行していない場合は、ループの不変条件を排除します

2
IIRCダフのデバイスは非常にまれに高速です。演算が非常に短い場合のみ(単一の小さな数式のように)
BCS

12
  • あなたが効率的なアルゴリズムを使用しているという点に到達したとき、それはあなたがより多くの速度またはメモリを必要とするかという問題です。キャッシュを使用してメモリで「支払う」か、計算を使用してメモリフットプリントを減らします。
  • 可能であれば(そしてより費用対効果の高い)問題にハードウェアを投入する -より高速なCPU、より多くのメモリ、またはHDが問題をより速く解決し、それをコーディングしようとします。
  • 並列化を使用する可能であればます-コードの一部を複数のスレッドで実行します。
  • 仕事に適したツールを使用する。一部のプログラミング言語は、マネージコード(つまりJava / .NET)を使用してより効率的なコードを作成し、開発を高速化しますが、ネイティブプログラミング言語はより高速に実行されるコードを作成します。
  • マイクロ最適化。最適化されたアセンブリを使用してコードの小さな部分を高速化できる場合にのみ、適切な場所でSSE /ベクトル最適化を使用すると、パフォーマンスが大幅に向上します。

12

分割統治

処理中のデータセットが大きすぎる場合は、そのデータのチャンクをループします。コードを正しく実行していれば、実装は簡単です。あなたがモノリシックなプログラムを持っているなら、今あなたはよりよく知っています。


9
最後の文を読んでいるときに聞いたハエたたきの「スマック」音の+1
ブライアンベッチャー、2011

11

まず、これまでのいくつかの回答で述べたように、パフォーマンスに影響を与えるもの、つまりメモリかプロセッサか、ネットワークかデータベースか、その他の何かを学びます。それに応じて...

  • ...それが記憶なら-「コンピュータプログラミングの芸術」シリーズの1つであるKnuthが昔に書いた本を1つ見つけてください。ほとんどの場合、それはソートと検索に関するものです-私のメモリが間違っている場合は、遅いテープデータストレージの扱い方について彼が話しているところを見つける必要があります。彼のメモリ/テープのペアをそれぞれキャッシュ/メインメモリのペア(またはL1 / L2キャッシュのペア)に精神的に変換します。彼が説明するすべてのトリックを研究してください。問題を解決する何かが見つからない場合は、専門のコンピューター科学者を雇って専門的な研究を行ってください。メモリの問題が偶然にFFT(基数2の蝶を実行するときにビット反転インデックスでのキャッシュミス)である場合は、科学者を雇わないでください。代わりに、パスが1つずつ手動で最適化されます。勝つか、行き止まりになる。あなたは言及しました最後の数パーセントまで絞ってみませんか?それはだ場合は、いくつか実際にあなたが最も可能性の高い勝ちます。

  • ...プロセッサの場合-アセンブリ言語に切り替えます。プロセッサの仕様を検討する- ティック、VLIW、SIMD が必要もの。関数呼び出しは、おそらく交換可能なダニを食べる人です。ループ変換を学ぶ-パイプライン、アンロール。乗算と除算は、ビットシフトで置き換え可能/補間されている可能性があります(小さな整数による乗算は、加算で置き換え可能です)。短いデータでトリックを試してください-運が良ければ、64ビットの1つの命令が32ビットでは2つ、16ビットでは4つ、8ビットでは8つに置き換えられるかもしれません。長くしてみてくださいデータ-たとえば、特定のプロセッサでは、浮動小数点の計算が二重の計算よりも遅くなる場合があります。三角関数を使用している場合は、事前に計算された表を使用してください。また、精度の低下が許容範囲内である場合、小さい値の正弦はその値に置き換えられる可能性があることにも注意してください。

  • ...それがネットワークの場合-渡すデータを圧縮することを考えてください。XML転送をバイナリに置き換えます。研究プロトコル。データ損失を何らかの形で処理できる場合は、TCPではなくUDPを試してください。

  • ...それがデータベースの場合は、データベースフォーラムにアクセスしてアドバイスを求めてください。インメモリデータグリッド、クエリプランなどの最適化など

HTH :)


9

キャッシング!ほとんどすべてをより速くするための(プログラマーの努力による)安価な方法は、プログラムのデータ移動領域にキャッシング抽象化レイヤーを追加することです。それがI / Oであるか、オブジェクトまたは構造の単なる受け渡し/作成であるか。多くの場合、ファクトリクラスとリーダー/ライターにキャッシュを追加するのは簡単です。

キャッシュはあまり効果がない場合がありますが、キャッシュを全体に追加し、役に立たない場合はキャッシュを無効にする簡単な方法です。私はこれをコードをマイクロ分析する必要なしに巨大なパフォーマンスを得るためにしばしば見つけました。


8

これはすでに別の言い方をしていると思います。しかし、プロセッサ集約型のアルゴリズムを扱う場合は、他のすべてのものを犠牲にして、最も内側のループ内のすべてのものを単純化する必要があります。

当たり前のように思えるかもしれませんが、使用している言語に関係なく、これに焦点を当てようとしています。たとえば、ネストされたループを処理していて、コードを1レベル下げる機会を見つけた場合、コードを大幅に高速化できる場合があります。別の例として、可能な場合は常に浮動小数点変数の代わりに整数を使用し、可能な場合は除算の代わりに乗算を使用するなど、少し考えなければならないことがあります。繰り返しになりますが、これらは最も内側のループで考慮すべきものです。

場合によっては、内側のループ内の整数に対して数学演算を実行し、それを後で操作できる浮動小数点変数に縮小することの利点を見つけることがあります。これは、あるセクションで速度を犠牲にして別のセクションで速度を向上させる例ですが、場合によっては見返りが得に値することもあります。


8

低帯域幅で待ち時間の長いネットワーク(衛星、リモート、オフショアなど)で動作するクライアント/サーバービジネスシステムの最適化に少し時間を費やしてきましたが、かなり反復可能なプロセスで劇的なパフォーマンスの向上を達成できました。

  • 対策:まず、ネットワークの基礎となる容量とトポロジーを理解することから始めます。ビジネスの関連するネットワーキング担当者と話し、pingやtracerouteなどの基本的なツールを使用して、通常の運用期間中に各クライアントの場所からのネットワーク遅延を(少なくとも)確立します。次に、問題のある症状を表示する特定のエンドユーザー機能の正確な時間測定を行います。これらの測定値のすべてを、それらの場所、日付、時刻とともに記録します。エンドユーザーの「ネットワークパフォーマンステスト」機能をクライアントアプリケーションに組み込んで、パワーユーザーが改善プロセスに参加できるようにすることを検討してください。このように権限を与えることは、パフォーマンスの低いシステムに不満を抱いているユーザーに対処するときに、大きな心理的影響を与える可能性があります。

  • 分析:影響を受ける操作の実行中に送受信されるデータを正確に確立するために、利用可能なすべてのロギングメソッドを使用します。理想的には、アプリケーションはクライアントとサーバーの両方で送受信されたデータをキャプチャできます。これらにタイムスタンプも含まれている場合、さらに良いでしょう。十分なログが利用できない場合(たとえば、クローズドシステム、または変更を運用環境に展開できない場合)、ネットワークスニファを使用して、ネットワークレベルで何が起こっているのかを本当に理解していることを確認してください。

  • キャッシュ:静的なデータや頻繁に変更されないデータが繰り返し送信されるケースを探し、適切なキャッシュ戦略を検討します。典型的な例には、「選択リスト」の値やその他の「参照エンティティ」が含まれますが、一部のビジネスアプリケーションでは驚くほど大きくなる可能性があります。多くの場合、特に頻繁に使用されるユーザーインターフェイス要素の表示が大幅に短縮される可能性がある場合は、ユーザーがアプリケーションを再起動または更新して、まれに更新されるデータを更新する必要があることを受け入れることができます。既にデプロイされているキャッシング要素の実際の動作を理解していることを確認してください。多くの一般的なキャッシング方法(HTTP ETagなど)は、一貫性を確保するためにネットワークラウンドトリップを必要とします。また、ネットワーク遅延が高額な場合は、それを完全に回避できる場合があります。別のキャッシュ方法。

  • Parallelize:論理的に厳密に順次発行する必要のない順次トランザクションを探し、システムを再処理してそれらを並列に発行します。エンドツーエンドのリクエストに固有のネットワーク遅延が〜2秒である1つのケースを扱いましたが、これは単一のトランザクションでは問題ではありませんでしたが、ユーザーがクライアントアプリケーションの制御を取り戻す前に、6回の2秒のラウンドトリップが必要でした、それはフラストレーションの巨大な原因になりました。これらのトランザクションが実際には独立していることを発見すると、トランザクションを並行して実行できるようになり、エンドユーザーの遅延が1回のラウンドトリップのコストに非常に近くなります。

  • 結合:順次リクエストを順次実行する必要がある場合は、それらを1つのより包括的なリクエストに組み合わせる機会を探します。典型的な例には、新しいエンティティの作成が含まれ、それらのエンティティを他の既存のエンティティに関連付ける要求が続きます。

  • 圧縮:テキスト形式をバイナリ形式に置き換えるか、実際の圧縮技術を使用して、ペイロードの圧縮を活用する機会を探します。多くの最新(つまり10年以内)のテクノロジスタックはこれをほぼ透過的にサポートしているため、構成されていることを確認してください。私はしばしば、問題が帯域幅ではなく基本的にレイテンシであることが明らかである圧縮の大きな影響に驚かされ、トランザクションが単一のパケット内に収まることを許可するか、そうでなければパケット損失を回避し、その結果、サイズが大きくなることを発見しました。パフォーマンスへの影響。

  • 繰り返し:最初に戻って、同じ場所と時間で操作を再測定し、改善を加え、結果を記録して報告します。すべての最適化と同様に、いくつかの問題が解決され、現在支配的となっている他の問題が露呈している可能性があります。

上記の手順では、アプリケーション関連の最適化プロセスに焦点を当てていますが、もちろん、基盤となるネットワーク自体がアプリケーションをサポートするためにも最も効率的な方法で構成されていることを確認する必要があります。ビジネスのネットワーキングスペシャリストを関与させ、容量の改善、QoS、ネットワーク圧縮、またはその他の手法を適用して問題に対処できるかどうかを判断します。通常、彼らはあなたのアプリケーションのニーズを理解しないので、あなたは彼らとそれについて話し合うこと、そして彼らに発生するようにあなたに要求するつもりであるどんなコストについてもビジネスケースを作ることを(分析ステップの後に)装備することが重要です。誤ったネットワーク構成により、アプリケーションデータが陸上リンクではなく低速衛星リンクを介して送信されるケースに遭遇しました。単にネットワークスペシャリストには「よく知られていない」TCPポートを使用していたからです。このような問題を修正すると、ソフトウェアコードや構成の変更がまったく不要になるため、パフォーマンスに劇的な影響を与える可能性があります。


7

この質問に一般的な答えを出すことは非常に困難です。それは本当にあなたの問題領域と技術的な実装に依存します。かなり言語に依存しない一般的な手法:排除できないコードホットスポットを特定し、アセンブラコードを手動で最適化します。


7

最後の数%は、CPUとアプリケーションに非常に依存しています...

  • キャッシュアーキテクチャは異なります。一部のチップには、直接マップできるオンチップRAMがあります。ARMには(場合によっては)ベクトルユニットがあり、SH4は便利なマトリックスオペコードです。GPUはありますか?おそらくシェーダーが適しています。TMS320はループ内の分岐に非常に敏感です(ループを分離し、可能であれば条件を外部に移動します)。

リストは続きます...しかし、これらの種類のものは本当に最後の手段です...

x86用にビルドし、コードに対してValgrind / Cachegrind を実行して、適切なパフォーマンスプロファイリングを行います。または、Texas Instrumentsの CCStudioには、優れたプロファイラがあります。そうすれば、どこに集中すべきか本当にわかるでしょう...


7

Did you know that a CAT6 cable is capable of 10x better shielding off extrenal inteferences than a default Cat5e UTP cable?

オフライン以外のプロジェクトでは、最高のソフトウェアと最高のハードウェアがあり、スルー出力が弱い場合、その細い線はデータを圧迫し、ミリ秒単位ではありますが遅延を与えます...しかし、最後の低下について話している場合、それは獲得したいくつかのドロップであり、送信または受信したパッケージごとに24時間年中無休です。


7

以前の回答ほど詳細ではありませんが、複雑ではありませんが、次のとおりです(これらはより初心者/中級レベルです)。

  • 明白:ドライ
  • ループを逆方向に実行して、変数ではなく常に0と比較する
  • できる限りビット演算子を使用する
  • 反復的なコードをモジュール/関数に分割する
  • キャッシュオブジェクト
  • ローカル変数にはパフォーマンス上の利点があります
  • 文字列操作をできるだけ制限する

4
逆方向のループについて:はい、ループ終了の比較はより速くなります。通常、変数を使用してメモリにインデックスを付けますが、キャッシュミスが頻繁に発生するため(プリフェッチなし)、逆にアクセスすると逆効果になる場合があります。
Andreas Reiff 2013

1
私の知る限り、ほとんどの場合、合理的なオプティマイザはループでうまく機能し、プログラマが明示的に逆に実行する必要はありません。オプティマイザがループ自体を逆にするか、同等に良い別の方法があります。私は昇順vs 最大と降順vs 0の両方で書かれた(確かに比較的単純な)ループの同一のASM出力に気付きました。確かに、私のZ80日は、逆方向ループを再帰的に書く習慣がありますが、初心者にそれを言及することは通常赤ニシン/時期尚早の最適化、コードを読みやすくすること、そしてより重要な実践を学ぶことを優先すべきです
underscore_d

逆に、ゼロとの比較と追加の減算と単一の整数の比較との比較では、単一の整数の比較の方が速いため、下位レベルの言語では逆方向のループの実行が遅くなります。デクリメントする代わりに、メモリの開始アドレスへのポインタとメモリの終了アドレスへのポインタを持つことができます。次に、開始ポインターを、終了ポインターと等しくなるまで増分します。これにより、アセンブリコードの余分なメモリオフセット操作がなくなり、パフォーマンスが大幅に向上します。
ジャックギフィン2018

5

言うことは不可能です。それはコードがどのように見えるかに依存します。コードがすでに存在すると想定できる場合は、コードを見て、そこから最適化する方法を理解するだけです。

キャッシュの局所性の向上、ループの展開、長い依存関係の連鎖を排除して、命令レベルの並列性を向上させます。可能な場合は、ブランチより条件付きの移動を優先します。可能な場合はSIMD命令を活用します。

コードの動作を理解し、コードが実行されているハードウェアを理解します。その後、コードのパフォーマンスを改善するために何をする必要があるかを決定するのはかなり簡単になります。それは本当に私が考えることができる唯一の本当に一般的なアドバイスです。

さて、それと、「SOでコードを表示して、その特定のコードの最適化に関するアドバイスを求める」。


5

より優れたハードウェアが選択肢である場合は、間違いなくそれを選択してください。さもないと

  • 最適なコンパイラとリンカオプションを使用していることを確認してください。
  • 頻繁な呼び出し元とは異なるライブラリのホットスポットルーチンの場合は、それを呼び出し元モジュールに移動または複製することを検討してください。呼び出しオーバーヘッドの一部を除去し、キャッシュヒットを改善する可能性があります(AIXがstrcpy()を個別にリンクされた共有オブジェクトに静的にリンクする方法を参照)。もちろん、これによりキャッシュヒットも減少する可能性があります。
  • ホットスポットルーチンの特殊なバージョンを使用する可能性があるかどうかを確認します。欠点は、維持する複数のバージョンです。
  • アセンブラーを見てください。改善の余地があると思われる場合は、コンパイラがこれを理解できなかった理由と、コンパイラを支援する方法を検討してください。
  • 検討してください:あなたは本当に最高のアルゴリズムを使用していますか?入力サイズに最適なアルゴリズムですか?

最初の部分に追加します。コンパイラオプションですべてのデバッグ情報をオフにすることを忘れないでください
varnie

5

グーグルの方法は、「キャッシュする..可能な限りディスクに触れない」という1つのオプションです。


5

ここで私が使用するいくつかの迅速で汚い最適化手法があります。私はこれを「最初のパス」の最適化と考えています。

時間を費やす場所を学ぶれているているものを正確に見つけます。ファイルIOですか?CPU時間ですか?ネットワークですか?データベースですか?それがボトルネックでない場合は、IOを最適化しても意味がありません。

あなたの環境を知る 知る最適化する場所を知ることは、通常、開発環境によって異なります。たとえば、VB6では、参照による受け渡しは値による受け渡しよりも低速ですが、CおよびC ++では、参照による受け渡しは非常に高速です。Cでは、戻りコードが失敗を示している場合、何かを試して別のことを行うのが妥当ですが、Dot Netでは、例外をキャッチする前に、有効な条件を確認するよりもはるかに遅くなります。

インデックス頻繁に照会されるデータベースフィールドにインデックスを作成します。ほとんどの場合、速度とスペースを交換できます。

ルックアップ を回避する最適化されるループの内側では、ルックアップを実行する必要はありません。ループの外でオフセットやインデックスを検索し、内部でデータを再利用します。

IOを最小化特にネットワーク接続での読み取りまたは書き込みの回数を減らす方法で試行をて設計しよう

抽象化削減コードが処理しなければならない抽象化の層が多いほど、処理は遅くなります。重要なループの内側で、抽象化を減らします(たとえば、余分なコードを回避する低レベルのメソッドを明らかにします)

ユーザーインターフェイスを備えたプロジェクトのスレッドを生成し、新しいスレッドを生成してより遅いタスクを実行すると、アプリケーションの応答性は高くなります、そうではありません。

前処理一般に、速度と引き換えにスペースを交換できます。計算やその他の激しい操作がある場合は、クリティカルループに入る前に、情報の一部を事前計算できるかどうかを確認してください。


5

OpenCLまたは(NVidiaチップの場合は)CUDAを使用して、高度に並列化された浮動小数点演算(特に単精度)がたくさんある場合は、グラフィックプロセッサ(存在する場合)にオフロードしてみてください。GPUは、シェーダーに非常に大きな浮動小数点演算能力を備えています。これは、CPUよりもはるかに優れています。


5

他のすべてに含まれていないので、この回答を追加しました。

型と符号の間の暗黙の変換を最小化します。

これは少なくともC / C ++に適用されます。ます。変換は不要ていても、パフォーマンスを必要とする関数、特にループ内の変換に注意する必要がある関数の周囲にコンパイラ警告を追加してテストするのは良いことです。

GCC固有:コードの周りに詳細なプラグマを追加することで、これをテストできます。

#ifdef __GNUC__
#  pragma GCC diagnostic push
#  pragma GCC diagnostic error "-Wsign-conversion"
#  pragma GCC diagnostic error "-Wdouble-promotion"
#  pragma GCC diagnostic error "-Wsign-compare"
#  pragma GCC diagnostic error "-Wconversion"
#endif

/* your code */

#ifdef __GNUC__
#  pragma GCC diagnostic pop
#endif

このような警告によって発生するコンバージョンを減らすことで、数パーセントの速度向上が見られるケースを見てきました。

場合によっては、偶発的な変換を防ぐために含めた厳格な警告のあるヘッダーがありますが、静かな意図的な変換に多くのキャストを追加することになり、コードを最小限に乱雑にする可能性があります。利益。


OCamlでこれが好きなのはこのためです。数値型間のキャストはxplicitでなければなりません。
Gaius 14

@Gaiusの公平な点-しかし、多くの場合、言語の変更は現実的な選択ではありません。C / C ++は非常に広く使用されているため、コンパイラー固有であっても、C / C ++をより厳密にすることができると便利です。
ideasman42 14

4

データのレイアウトを変更すると役立つ場合があります。Cでは、配列または構造体から配列の構造体に、またはその逆に切り替えることができます。


4

OSとフレームワークを微調整します。

やり過ぎに聞こえるかもしれませんが、次のように考えてください。オペレーティングシステムとフレームワークは、多くのことを実行するように設計されています。アプリケーションは非常に特定のことだけを行います。アプリケーションが必要とするものをOSに正確に実行させ、フレームワーク(php、.net、java)がどのように機能するかをアプリケーションに理解させることができれば、ハードウェアの性能を大幅に向上させることができます。

たとえば、Facebook はLinuxのカーネルレベルのものを変更し、memcachedの動作を変更しました(たとえば、memcachedプロキシを作成し、tcpではなくudp使用しました)。

これのもう1つの例はWindow2008です。Win2K8には、Xアプリケーション(Webアプリ、サーバーアプリなど)を実行するのに必要な基本的なOSだけをインストールできるバージョンがあります。これにより、OSが実行中のプロセスに与えるオーバーヘッドの多くが削減され、パフォーマンスが向上します。

もちろん、最初のステップとして、常により多くのハードウェアを投入する必要があります...


2
他のすべてのアプローチが失敗した後、または特定のOSまたはフレームワーク機能がパフォーマンスの大幅な低下の原因であった場合、それは有効なアプローチですが、それを実現するために必要な専門知識と制御のレベルは、すべてのプロジェクトで利用できるとは限りません。
Andrew Neely、2011
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.