最後の手段としてのパフォーマンス最適化戦略 [終了] 質問する

最後の手段としてのパフォーマンス最適化戦略 [終了] 質問する

このサイトには既にパフォーマンスに関する質問が多数ありますが、そのほとんどが問題に非常に特化しており、かなり範囲が狭いように思います。また、ほとんどすべてが、時期尚早な最適化を避けるようにというアドバイスを繰り返しています。

仮定してみましょう:

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

私がここで探しているのは、他に何もすることがなく、何でもやるしかないときに、重要なアルゴリズムで最後の数パーセントまで絞り出すための戦略とコツです。

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

私は自分の最初の提案を添えて返信し、Stack Overflow コミュニティが他に何を思いつくかを楽しみにしています。

ベストアンサー1

わかりました。あなたは、改善の余地がほとんどないと思われるような問題を定義しつつあります。私の経験では、それはかなりまれなことです。私は、1993 年 11 月の Dr. Dobbs の記事で、従来どおり適切に設計された、明らかな無駄のない非自明なプログラムから始めて、一連の最適化を行い、ウォールクロック時間が 48 秒から 1.1 秒に短縮され、ソース コードのサイズが 4 分の 1 に縮小されるまで、これを説明しようとしました。私の診断ツールこれは変更の順序は次のとおりです。

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

  • 現在、最も時間がかかるのはリスト作成です。割合で見ると、以前はそれほど大きくありませんでしたが、大きな問題が取り除かれたため、割合は大きくなっています。これを高速化する方法を見つけたので、時間は 17 秒に短縮されました。

  • 今では明らかな原因を見つけるのは難しくなりましたが、対処できる小さな原因がいくつかあり、時間は 13 秒に短縮されました。

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

その後、私は再設計を思いつきました。再設計では、プログラム コードは実際には (プリプロセッサ マクロを介して) より小さなソース セットから生成され、プログラマーがかなり予測可能であると知っている事柄をプログラムが常に把握することはありません。言い換えると、実行すべき一連の事柄を「解釈」するのではなく、「コンパイル」するのです。

  • 再設計が完了すると、ソース コードは 4 分の 1 に縮小され、時間は 10 秒に短縮されます。

さて、作業が速すぎてサンプルを取るのが難しいため、作業量を 10 倍に増やしましたが、以下の時間は元の作業量に基づいています。

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

  • 今、私が行っていた診断印刷に非常に時間がかかっています。フラッシュ - 4 秒。

  • 現在、最も時間がかかるのはmallocfreeの呼び出しです。オブジェクトのリサイクル - 2.6 秒。

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

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

2 つのプログラムが同じということはありません。しかし、おもちゃ以外のソフトウェアでは、常にこのような進行が見られます。最初は簡単なものから始めて、だんだん難しくなっていき、収穫逓減の段階に達します。次に、得られた洞察によって再設計が行われ、新たなスピードアップ ラウンドが始まりますが、再び収穫逓減の段階に達します。この時点で、または++iまたはi++またはfor(;;)のほうwhile(1)が速いかどうか疑問に思うのが妥当かもしれません。これは、Stack Overflow でよく見かける種類の質問です。

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 になり、今では「より大きな問題」になっています。一般的には、このような流れになります。

このプロジェクトは、私が実際に手伝ったプロジェクトから生まれたものであることも付け加えておきます。そのプロジェクトでは、タスクが完了したかどうかを確認するために内部ループ内でデータベース アクセス ルーチンを呼び出すなど、パフォーマンスの問題ははるかに深刻でした (スピードアップも同様でした)。

参照追加: オリジナルと再設計されたソースコードは、以下にあります。ホームページ、1993 年の場合は、ファイル 9311.zip、ファイル slug.asc、および slug.zip。

編集2011/11/26: 現在、SourceForge プロジェクトVisual C++ のソース コードと、チューニング方法の詳細な説明が含まれています。上記のシナリオの前半のみを取り上げており、まったく同じシーケンスに従うわけではありませんが、それでも 2 ~ 3 桁の高速化が実現されています。

おすすめ記事