コーディングの際、パフォーマンスに関して留意すべき経験則は何ですか? 特定のプラットフォームとコンパイラー向けに最適化する方法は無限にありますが、コンパイラーとプラットフォーム全体に等しく (またはほぼ) 適用できる答えを探しています。
26 に答える
有名な引用が思い浮かびます:
「私たちは小さな効率を忘れるべきです。たとえば、97%の確率で、時期尚早の最適化がすべての悪の根源です。」(Knuth、Donald。ステートメントに移動する構造化プログラミング、ACM Journal Computing Surveys、Vol 6、No。4、1974年12月。p.268。)
しかし、とにかく大きなデータ構造を値で渡すべきではないかもしれません... :-)
編集:そして多分またO(N ^ 2)またはより複雑なアルゴリズムを避けてください...
パフォーマンスの最大のヒントは、コードを早期に頻繁にプロファイリングすることです。一般的な「これを行わない」ためのヒントはたくさんありますが、これがアプリケーションのパフォーマンスに影響を与えることを保証するのは非常に困難です。なんで?すべてのアプリケーションは異なります。要素がたくさんある場合、値でベクトルを渡すのは悪いことだと言うのは簡単ですが、プログラムはベクトルを使用することさえありますか(おそらくそうすべきですが...)?
プロファイリングは、アプリケーションのパフォーマンスを理解する唯一の方法です。私は、人々がコードを「最適化」したが、プロファイルを作成しなかったという状況が多すぎました。「最適化」は多くのバグをもたらし、コードパスのホットスポットでさえないことが判明しました。みんなの時間の無駄。
編集:
数人の人が私の答えの「初期」の部分についてコメントしました。1日目からプロファイリングするべきではないと思います。ただし、船から1か月も待たないでください。
私は通常、いくつかの決定的なエンドツーエンドのシナリオができたら、または大規模なプロジェクトではほとんど機能するコンポーネントを作成したら、最初にプロファイルを作成します。私は1日か2日(通常はQAで作業します)、いくつかの大きなシナリオをまとめてコードに投げ込みます。これは、明らかなパフォーマンスの問題を早期に発見するための優れたスポットチェックです。この時点でそれらを修正するのは少し簡単です。
典型的なプロジェクトでは、プロジェクトの30%〜40%でこの基準を満たすコードがあります(100%は顧客の手に渡っています)。今回は大まかに早めに分類します。
- 可能な場合は、関数ポインタを介した呼び出しの代わりに使用
if
しswitch
てください。明確化:void doit(int m) { switch(m) { case 1: f1(); break; case 2: f2(); break; } }
代わりにvoid doit(void(*m)()) { m(); }
、呼び出しをインライン化できます。 - 可能で害を及ぼさない場合は、仮想関数よりもCRTPを優先します
- 可能であれば、C文字列を避け、Stringクラスを使用してください。ほとんどの場合、より高速になります。(一定時間の長さの「測定」、償却された一定時間を追加、...)
- 値をコピーするのではなく、常にconst(T const&)を参照して、ユーザー定義の型付き値(イテレーターなど、意味をなさない場合を除く)を渡します。
- ユーザー定義タイプの場合、常にでは
++t
なくを優先しますt++
const
早く、頻繁に使用してください。読みやすさを向上させるために最も重要です。new
最小限に抑えるようにしてください。可能であれば、常に(スタック上の)自動変数を優先しますT t[N] = { };
配列を自分で埋める代わりに、ゼロが必要な場合のように、空の初期化子リストで初期化することをお勧めします。- 特にユーザー定義の型付きメンバーを初期化する場合は、コンストラクター初期化子リストをできるだけ頻繁に使用してください。
- ファンクター(
operator()
オーバーロードされたタイプ)を利用します。これらは、関数ポインタを介した呼び出しよりもインライン化されています。 - のようなクラスを使用しないでください。
std::vector
またはstd::string
、固定サイズの数量が増えていない場合は使用しないでください。またはネイキッドアレイを使用boost::array<T, Size>
して、適切に使用してください。
そして確かに、私はそれをほとんど忘れていました:
時期尚早の最適化はすべての悪の根源です
誰かが関数ポインタについて言及しました (そしてなぜ を使うべきなのかif
)。さらに良いことに、代わりにファンクターを使用すると、インライン化され、通常はオーバーヘッドがゼロになります。ファンクターは、演算子をオーバーロードする構造体 (またはクラスですが、通常は前者) で()
あり、そのインスタンスは通常の関数のように使用できます。
template <typename T>
struct add {
operator T ()(T const& a, T const& b) const { return a + b; }
};
int result = add<int>()(1, 2);
これらは、通常の関数または関数ポインタを使用できるほぼすべてのコンテキストで使用できます。std::unary_function
これらは通常、 or のいずれかから派生しますstd::binary_function
が、多くの場合、それは必要ではありません (実際には、いくつかの有用なtypedef
s を継承するためだけに行われます)。
EDIT上記のコードでは、明示的な<int>
型修飾が必要です。型推論は、インスタンスの作成ではなく、関数呼び出しに対してのみ機能します。ただし、make
ヘルパー関数を使用することで省略できることがよくあります。これはpair
s の STL で行われます。
template <typename T1, typename T2>
pair<T1, T2> make_pair(T1 const& first, T2 const& second) {
return pair<T1, T2>(first, second);
}
// Implied types:
pair<int, float> pif = make_pair(1, 1.0f);
誰かがコメントで、ファンクターは「ファンクションイド」と呼ばれることがあると述べました。はい、そうですが、完全ではありません。実際、「ファンクター」は「関数オブジェクト」の (やや奇妙な) 省略形です。Functionoid は概念的には似ていますが、仮想関数を使用することによって実現されます (ただし、同義語として使用されることもあります)。たとえば、Functionoid は次のようになります (必要なインターフェイス定義と共に)。
template <typename T, typename R>
struct UnaryFunctionoid {
virtual R invoke(T const& value) const = 0;
};
struct IsEvenFunction : UnaryFunctionoid<int, bool> {
bool invoke(int const& value) const { return value % 2 == 0; }
};
// call it, somewhat clumsily:
UnaryFunctionoid const& f = IsEvenFunction();
f.invoke(4); // true
もちろん、これはファンクターが仮想関数呼び出しのために持っているパフォーマンス上の利点を失います。したがって、実際にはポリモーフィック (ステートフル) ランタイム関数を必要とする別のコンテキストで使用されます。
C++ FAQには、この件に関してさらに多くのことが書かれています。
必要になるまでわざわざ最適化しないでください。必要かどうかを確認するには、プロファイルを作成します。推測しないでください。証拠があります。
また、アルゴリズムの最適化は通常、マイクロの最適化よりも大きな影響を及ぼします。ブルートフォースパスファインディングの代わりにAスターを使用すると、ブレゼンハムのサークルがsin / cosを使用するよりも優れているのと同じように、より高速になります。もちろんこれらには例外がありますが、それらは非常に(非常に)まれです(<0.1%)。優れた設計がある場合、アルゴリズムを変更すると、コード内の1つのモジュールのみが変更されます。簡単。
使用および再利用された、レビュー済みの既存のコードを使用します。(例: STL、ブースト vs 独自のコンテナーとアルゴリズムのローリング)
コメントによる更新: 使用および再利用された既存のレビュー済みコードを正しく使用してください。
パフォーマンスに関する限り、できる最善のことは、堅固なアーキテクチャとスレッドモデルから始めることです。他のすべてはこれに基づいて構築されるので、基礎が不器用な場合、完成品はそれと同じくらい良いものになります。プロファイリングは少し遅れて、さらに遅くなってマイクロ最適化が行われます(一般に、これらは重要ではなく、何よりもコードを複雑にします)。
話の教訓は次のとおりです。効率的な基盤から始めて、まったく愚かで遅いことをしないという認識の上に構築してください。そうすれば、大丈夫なはずです。
もう1つのポイント:最速のコードは存在しないコードです。つまり、プロジェクトがより堅牢で機能が充実している必要があるほど、プロジェクトは遅くなります。結論:要件を満たしていることを確認しながら、可能な限り綿毛をスキップします。
C ++の2つの最良のヒント:
ScottMeyersによるEffectiveC++を購入します。
次に、ScottMeyersによるMoreEffectiveC++を購入します。
コードをできるだけクリーンに保ちます。コンパイラは最近素晴らしいです。次に、パフォーマンスに問題がある場合は、プロファイルを作成します。
これはすべて、問題に利用できる最良のアルゴリズムを選択した後です。
行うべき良いことは、使用しているものの効率を知ることです。乗算に対する足し算の速さ、ベクトルが通常の配列と比較される速さ、または特定のアルゴリズムが比較されるより高いスケールと比較される速さ。これにより、タスクに最も効率的なツールを選択できます
一般的なアルゴリズムを使用することは、最適化の優れたヒントです。実行時間ではなく、コーディング時間に関してです。並べ替え (開始、終了) が可能であり、範囲 (データベースへの 2 つのポインターまたは反復子) を期待できることを知っていれば、並べ替えが行われます (さらに、使用されるアルゴリズムも実行時に効率的になります)。ジェネリック プログラミングは、C++ を独特で強力なものにしている要素であり、常に心に留めておく必要があります。バージョンが既に存在するため、多くのアルゴリズムを作成する必要はありません (そして、作成するものと同じかそれよりも高速である可能性があります)。他の考慮事項がある場合は、アルゴリズムを特化できます。
簡単な推測の1つは、i++ではなく++iを実行する習慣を身に付けることです。i ++はコピーを作成しますが、これは高額になる可能性があります。
ここにいくつかあります:
インライン化を効果的に活用します(プラットフォームによって異なります)。
一時的なものの使用はできるだけ避けてください(そしてそれらが何であるかを知ってください)
x = y + z;
次のように記述した場合、より適切に最適化されます。
x = y;
x + = z;
また、仮想関数を避け、使用する必要がある場合にのみオブジェクトを作成してください。
気分が良ければ、EfficientC++をチェックしてください。学校に通っていた頃、家にコピーを持っていました。
メモリプールの使用を検討してください。
基本的に、最大のパフォーマンスの向上は、アルゴリズムの改善によってもたらされます。これは、最も効率的なアルゴリズムを使用し、次にデータ項目に最も効率的なコンテナを使用することを意味します。
最良のトレードオフが何であるかを知るのは難しい場合がありますが、幸いなことに、STLの設計者はまさにこのユースケースを念頭に置いていたため、STLコンテナーは一般に、要求に応じてコンテナーを混合および一致させるのに十分な柔軟性を備えています。アプリケーションの。
ただし、この利点を完全に実現するには、クラス/モジュールなどのインターフェイスの一部として内部設計の選択を公開しないようにする必要があります。の使用に依存するクライアントはありませんstd::vector
。少なくとも、彼ら(およびあなた)が使用できるtypedefを提供します。これにより、必要に応じてベクターをリスト(またはその他)に変更できるようになります。
同様に、デバッグされたアルゴリズムとコンテナを可能な限り幅広く選択できることを確認してください。ブーストおよび/またはTR1は最近かなり必需品です。
私が参照したC++の本の1つ(BulkaとMayhewによる効率的なC ++パフォーマンステクニック)から、C++パフォーマンスの側面について明示的に説明しました。それらの1つは;
コンストラクターを定義している間..他のコンストラクターも初期化します。何かのようなもの;
class x {
x::x(char *str):m_x(str) {} // and not as x::x(char *str) { m_str(str); }
private:
std::string m_x;
};
上記は私の注意を引き、私のコーディングスタイルを改善するのに役立ったいくつかのことです...この本は、パフォーマンスのこの興味深いトピックについてもっと共有する必要があります。
著しく非効率なアルゴリズムを使用しないでください。コンパイラで最適化をオンにし、プロファイラーがボトルネックであると示さない限り何も最適化しないでください。物事を改善しようとするときは、良いことか悪いことかをテストしてください。また、ライブラリ関数は通常、あなたよりも優れた人によって最適化されていることも覚えておいてください。
これらに比べて、他のほとんどすべてはマイナーです。
メモリがどのように見えるかを常に考えてみてください。たとえば、配列はサイズ numOfObjects X のメモリの連続行です
sizeof(object)
。2 次元配列は n X m Xsizeof(object)
であり、各オブジェクトは n + m X n のインデックスにあるなどfor(int i = 0 ; i < n ; i++){ for(int j = 0 ; j < m ; j++){ arr[i,j] = f();
それよりもはるかに優れています(単一プロセスで):
for(int i = 0 ; i < n ; i++){ for(int j = 0 ; j < m ; j++){ arr[j,i] = f();
配列は連続したチャンクでキャッシュに取り込まれるため、最初のスニペットは残りをフェッチする前にキャッシュ内のすべてのセルで実行されますが、2 番目のスニペットは新しい配列セルをセルに何度もフェッチする必要があります。
アプリケーションが遅くなり始めたら、パフォーマンス ベンチマークを使用して正確なボトルネックを見つけ、
GetTickCount
コンポーネントの実行にかかる時間を特定するために単純な呼び出しを使用できます。大規模なプロジェクトでは、最適化を開始する前に適切なプロファイラーを使用して、重要な部分で最も最適化の労力を費やすようにします。
「時期尚早の最適化はすべての悪の根源です」(クヌース、ドナルド)
それは実際にあなたが書くコードのタイプとそれの典型的な使用法に依存します。
時期尚早の最適化に関するアドバイスに同意します。ただし、後で最適化するのが難しい場合があるため、設計時に従うのが好きなガイドラインがいくつかあります。
- 起動時にすべてのオブジェクトを割り当て、
new
実行時の使用を最小限に抑えるようにしてください。 - アルゴリズムがO(1)になるようにデータ構造を設計します。
- そしていつものように、モジュール化して、後で取り除いて交換できるようにします。これは、新しいソリューションが正しいという確信を与える単体テストの包括的なスイートがあることを意味します。
- ユニットテストスイートにパフォーマンステストを追加して、誤ってO(n * n)コードが発生しないようにします:-)
- ルール 1: しない
- ルール 2: メジャー
数サイクルを節約するためにどのような行動を取るにしても、次のことを覚えておいてください。
正確な問題には対処していませんが、いくつかのアドバイスは次のとおりです。
常にインターフェイスのコードを作成して(アルゴリズムに関しては)、効率的なものにスムーズに置き換えることができるようにします(必要な手段で)。
最高のパフォーマンスを発揮するアルゴリズムを選択し、メモリの使用量を減らし、分岐の使用量を減らし、高速な操作を使用し、反復回数を減らします。