コードベースが均一に遅くなることに対するアプローチ


11

適度なサイズのC ++コードベース(10Mloc)に取り組んでいますが、最適化の取り組みにより均一に遅くなっています。

このコードベースは、機能させるために組み合わせるライブラリのセットです。これらのライブラリが通信する方法の一般的なフレームワークが開発されたとき、パフォーマンスに重点が置かれ、後で追加された部分が増えても、一般的なフレームワークはあまり変更されませんでした。最適化は、必要なときに、ハードウェアの進化とともに行われました。これにより、高価な早期決定が明らかになったのはかなり後のことです。コードベースの大部分を書き直す必要があるため、さらなる最適化ははるかに高価になります。原則として、コードははるかに高速に実行できるはずであることがわかっているため、望ましくないローカルミニマムに近づいています。

簡単な最適化の機会によって簡単に混同されない、グローバルに最適なパフォーマンスのソリューションへのコードベースの進化を引き継ぐために何が変わるかを決定するのに役立つ成功した方法論はありますか?

編集

現在のプロファイリング方法に関する質問に答えるには:

実際、このコードを使用する方法は2つだけあり、どちらも恥ずかしいほど並行しています。プロファイリングは、大量の入力サンプルで平均化されたウォールクロック時間と、より詳細な実行(命令コスト、分岐予測ミス、キャッシュの問題)の両方で行われます。これは非常に均質なマシン(数千の同一マシンのクラスター)でのみ実行されるため、うまく機能します。私たちは通常、すべてのマシンをほとんどの時間、高速で稼働させているため、追加の新しいものを見ることができます。もちろん、問題は、新しい入力バリエーションが現れたときに、他のユースケースで最も明らかな非効率性を取り除き、「最適に実行される」シナリオの数を絞り込む可能性があるため、後期のペナルティが発生する可能性があることです。


10
10Mlocは実際には巨大なプロジェクトです
BЈовић

1
でカウントした場合、1000万loc(SIプレフィックス)slocです。ここで「大きな」と見なされるものがわからないため、「適度なサイズ」と呼びました。
ベンジャミンバニエ

5
1000万個は少なくともどこでも大きく、おそらくほとんどの場所で巨大であると確信しています。
リャサル

1
すごい、@ honkに感謝します。10MLOCでは、ほとんどハードウェアレベルで、非常に低いレベルで最適化を行っているようですね。従来のOOP(AOS「構造の配列」)はキャッシュ上でひどく非効率的です。クラスをSOA(配列の構造)に再配置して、コードで作業しているデータポイントがメモリ内で一貫性を保とうとしましたか?そんなに多くのマシンで、通信障害や同期を実行して時間を浪費していますか?最後の質問は、大量のストリーミングデータを処理しているのですか、それともデータセットに対する複雑な操作の問題ですか?
パトリックヒューズ

1
たくさんのコードを手に入れたとき、私が述べた非ローカルな種類の大きな潜在的な高速化が存在する可能性があります。数千のスレッド/プロセスが存在しても違いはありません。いくつかのランダムな一時停止は、あなたのためにそれらを指で触れるか、私が間違っていることを証明します。
マイクダンラベイ

回答:


9

この問題に対する汎用的なアプローチはわかりませんが、過去には多少関連する2つのアプローチがうまく機能しました。より良い用語がないため、それらをバンチング水平最適化と呼びました。

バンチングアプローチは、多数の短く高速な操作を、最終的には同じ結果が得られる、実行速度が遅く、高度に特殊化された単一の操作に置き換える試みです。

:ビジュアルルールエディターの特に遅い操作をプロファイリングした後、「低垂れ下がりの果物」は見つかりませんでした。実行時間の2%を超える操作は1つもありませんでしたが、操作全体が遅く感じました。しかし、エディターがサーバーに多数の小さなリクエストを送信していることがわかりました。エディターが個々の返信をすばやく処理していても、リクエスト/レスポンスのやり取りの数は相乗効果をもたらしたため、操作にかかった全体の時間は数秒でした。その長時間実行中のエディターの対話を注意深くカタログ化した後、サーバーインターフェイスに新しいコマンドを追加しました。追加のコマンドは、短い操作のサブセットを実行するために必要なデータを受け入れたため、より専門化されました。データの依存関係を調べて、返されるデータの最終セットを見つけ、サーバーへの1回の旅行で個々の小さな操作をすべて完了するために必要な情報を含む応答を提供しました。これにより、コードの処理時間は短縮されませんでしたが、複数の高価なクライアントサーバーラウンドトリップが削除されるため、レイテンシが大幅に削減されました。

水平最適化は、実行環境の特定の機能を使用して、システムの複数のコンポーネント間で薄く分散される「遅さ」を排除する場合の関連技術です。

:実行時間の長い操作をプロファイリングした後、アプリケーションドメインの境界を越えて多くの呼び出しを行うことがわかりました(これは.NETに非常に固有です)。呼び出しを削除することもできず、それらをまとめることもできませんでした。それらはシステムのさまざまなセクションからさまざまな時間に来ていて、要求したことは以前の要求から返された結果に依存していました。各呼び出しには、比較的少量のデータのシリアル化と逆シリアル化が必要でした。繰り返しになりますが、個々の通話は短い時間でしたが、数が非常に多くなりました。最終的には、シリアル化をほぼ完全に回避するスキームを設計し、アプリドメインの境界を越えてポインターを渡すことに置き換えました。完全に無関係なクラスからの多くのリクエストが、単一の水平ソリューション。


あなたの経験を共有してくれてありがとう、これらは覚えておくと便利な最適化です。また、問題のある部品を別の場所に持ち上げるので、将来制御するのがはるかに良いでしょう。ある意味では、彼らはそもそも何が起きたはずだったかを、現在はハードデータによってのみ元に戻しています。
ベンジャミンバニエ

3

これにより、高価な早期決定が明らかになったのはかなり後のことです。コードベースの大部分を書き直す必要があるため、さらなる最適化ははるかに高価になります。

この書き換えを開始すると、いくつかのことを異なる方法で行う必要があります。

最初。そして最も重要です。「最適化」を停止します。「最適化」はそれほど重要ではありません。これまで見てきたように、大規模な書き換えのみが重要です。

したがって。

第二に。すべてのデータ構造とアルゴリズムの選択の意味を理解します。

三番。データ構造とアルゴリズムの実際の選択は、「遅延バインディング」の問題にします。インターフェイスの背後で使用されるいくつかの実装のいずれか1つを持つことができるインターフェイスを設計します。

データ構造またはアルゴリズムに大規模な変更を加えることができる一連のインターフェイスが定義されていれば、今やっていること(書き換え)は非常に簡単です。


1
ご回答有難うございます。まだ最適化する必要がありますが(私にとっては1.と2.に該当します)、3の背後にある組織化された考え方が本当に好きです。私たちが直面している問題。一貫した言語に翻訳してくれてありがとう。
ベンジャミンバニエ

実際に最適化する必要はありません。正しいデータ構造を取得すると、最適化は労力の無駄になることが示されます。プロファイリングにより、間違ったデータ構造と間違ったアルゴリズムがある場所が表示されます。間の性能差が浮気+++=1は無関係とほとんど測定不能になります。それはあなたが長持ちすることです。
-S.ロット

1
すべての悪いアルゴリズムが純粋な推論で見つかるわけではありません。たまに座ってプロファイルを作成する必要があります。これは、最初の推測が正しいかどうかを確認する唯一の方法です。これが実際のコスト(BigO + const)を推定する唯一の方法です。
ベンジャミンバニエ

プロファイリングにより、悪いアルゴリズムが明らかになります。完全に正しい。それはまだ「最適化」ではありません。それは、設計変更を行う際の基本的な設計上の欠陥の修正です。最適化(微調整、微調整など)がプロファイリングで認識されることはほとんどありません。
S.Lott

3

実用的なトリックは、単体テストスイートをパフォーマンステストスイートとして使用することです。

次のアプローチは、私のコードベースでうまく機能しました。

  1. ユニットテストのカバレッジが良好であることを確認します(これを既に実行しましたか?)
  2. テスト実行フレームワークが個々のテストごとにランタイムを報告することを確認してください。パフォーマンスが低下している場所を見つけたいため、これは重要です。
  3. テストの実行が遅い場合は、これを使用して、この領域で最適化の対象を絞り込みます。パフォーマンスの問題を特定する前に最適化することは時期尚早と見なされる可能性があるため、このアプローチの素晴らしい点は、まずパフォーマンスの低下の具体的な証拠を得ることです。必要に応じて、テストをさまざまな側面のベンチマークとなる小さなテストに分割し、根本的な問題の場所を特定できるようにします。
  4. テストが非常に高速に実行される場合、通常は問題ありませんが、異なるパラメーターを使用してテストをループで実行することを検討することもできます。これにより、より優れたパフォーマンステストになり、パラメーター空間のテスト範囲も広がります。
  5. エンドツーエンドのトランザクション時間や1,000のルールアプリケーションを完了するまでの時間など、特にパフォーマンスを対象とするいくつかの追加テストを作成します。特定の機能以外のパフォーマンス要件がある場合(応答時間が300ミリ秒未満など)、テストに時間がかかりすぎる場合はテストを失敗させます。

これをすべて続ければ、時間の経過とともにコードベースの平均パフォーマンスは有機的に改善されるはずです。

また、過去のテスト時間を追跡し、パフォーマンスチャートを作成して、平均パフォーマンスの経時的な回帰を見つけることもできます。主に変更して新しいテストを追加するのと同じように比較するのは少し難しいので、これを気にすることはありませんでしたが、パフォーマンスが十分に重要な場合は興味深い練習になる可能性があります。


私はテクニックが好きです-賢い-しかし、これはマイクロ最適化であり、ローカルの最小値に改善されます
到達

@jasonk-あなたは絶対に正しい。特定のアーキテクチャの変更が正当化される理由を示すために必要な証拠を時々提供できると付け加えますが
...-mikera

1

@dasblinkenlightの回答は、特に私のコードベースでの非常に一般的な問題を指摘しています。深刻なパフォーマンスの問題があるかもしれませんが、それらはローカライズされいません。プロファイラーを実行する場合、注意を引くのに十分な時間を費やしているルーチンはありません。(呼び出し先を含む包括的時間の割合を見ると仮定します。「自己時間」を気にしないでください。)

実際、その場合、実際の問題はプロファイリングではなく、幸運な洞察によって発見されました。

ケーススタディがあります。簡単なPDFスライドショーがあり、この問題の詳細と対処方法を説明しています。基本的なポイントは、コードが考えられるよりもはるかに遅いため、それは(定義により)削除できることをするために余分な時間が費やされることを意味します。

プログラムの状態のランダムな時間のサンプルを見ると、時間の割合が原因で、リムーバブルアクティビティを実行しているのがわかります。リムーバブルアクティビティが1つの機能、または多くの機能に限定されることはありません。そのようにローカライズされていません。

「ホットスポット」ではありません。

それはあなたが見たものの説明であり、それはたまたま本当のことです。これにより発見が簡単になりますが、修正が簡単かどうかは、書き直しに必要な量に依存します。

(多くの場合、このアプローチは、サンプル数が統計的妥当性に対して少なすぎるという批判があります。これはPDFのスライド13で回答されています。簡潔に-はい、潜在的な節約の「測定」には高い不確実性がありますが、 1)その潜在的な節約の期待値は本質的に影響を受けず、2)潜在的な節約$ x $が$ 1 /(1-x)$で高速化率に変換されると、それは高い(有益な)要因に強く偏ります。)


ご回答有難うございます。統計的なサンプリングを信じておらず、valgrindでインストルメンテーションを使用しています。これにより、私たちが行うほとんどの作業の「自己」と「包括的」なコストの両方の適切な見積もりが得られます。
ベンジャミンバニエ

@honk:そうです。しかし、残念ながら、インストルメンテーションにはパフォーマンスの問題がローカライズされているという考えが残っているため、ルーチンなどで費やした時間の一部を測定することで見つけることができます。 。
マイクダンラベイ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.