パフォーマンスを改善するためにどのような簡単なテクニックを使用しますか?


21

コードを読みにくくすることなくパフォーマンスを向上させるために簡単なルーチンを記述する方法について話している...たとえば、これは私たちが学んだ典型的なものです:

for(int i = 0; i < collection.length(); i++ ){
   // stuff here
}

しかし、私は通常、a foreachが該当しない場合にこれを行います。

for(int i = 0, j = collection.length(); i < j; i++ ){
   // stuff here
}

lengthメソッドを一度だけ呼び出すので、これはより良いアプローチだと思います...私のガールフレンドはそれが不可解だと言います。自分の開発で使用する他の簡単なトリックはありますか?


34
+1は、コードが明確でない場合に通知してくれるガールフレンドがいるためです。
クリスト

76
あなたはこれを投稿して、あなたにガールフレンドがいると言っています。
ジョシュK

11
@Christian:これを行うコンパイラーの最適化があることを忘れないでください。そうすれば、読みやすさにのみ影響を与え、パフォーマンスにはまったく影響を与えないかもしれません。早すぎる最適化はすべての悪の根源です...同じ行で複数の宣言または割り当てを避け、人々にそれを2回読み取らせないでください...通常の方法(最初の例)を使用するか、 forループの外側の2番目の宣言(ただし、jの意味を確認するために読み返す必要があるため、読みやすさも低下します)。
タマラWijsman

5
@TomWij:正しい(そして完全な)引用: 「小さな効率を忘れて、約97%の時間を言うべきです。時期尚早な最適化がすべての悪の根源です。 」
ロバートハーベイ

3
@tomwij:3%を費やしている場合、定義上、タイムクリティカルなコードでそれを行うべきであり、他の97%に時間を無駄にしないでください。
ロバートハーベイ

回答:


28

早すぎる議論が根源的な悪の講義を挿入する

そうは言っても、不必要な効率を避けるために私が習得したいくつかの習慣があり、場合によってはコードをよりシンプルで正確にすることもできます。

これは一般的な原則の議論ではありませんが、コードに不必要な非効率性を持ち込むことを避けるために知っておくべきいくつかの事柄の議論です。

あなたのビッグオーを知る

これはおそらく上記の長い議論に統合されるべきです。内側のループが計算を繰り返すループ内のループが遅くなるのは、かなり常識です。例えば:

for (i = 0; i < strlen(str); i++) {
    ...
}

文字列が本当に長い場合、ループの各反復で長さが再計算されるため、これには非常に長い時間がかかります。GCCstrlen()は純粋な関数としてマークされているため、実際にこのケースを最適化することに注意してください。

100万個の32ビット整数をソートする場合、バブルソートは間違った方法です。一般的に、ソートはO(n * log n)時間(または基数ソートの場合は良い)で実行できるため、データが小さくなることがわかっていない限り、少なくともO(n *ログn)。

同様に、データベースを扱うときは、インデックスに注意してください。あなたならばSELECT * FROM people WHERE age = 20、あなたは人(年齢)にインデックスを持っていない、と、それは、シーケンシャルスキャンではなくはるかに高速O(Nログ)インデックス・スキャンよりもO(n)を必要とします。

整数算術階層

Cでプログラミングする場合、算術演算の中には他の演算よりも高価なものがあることに留意してください。整数の場合、階層は次のようになります(最も高価ではない)。

  • + - ~ & | ^
  • << >>
  • *
  • /

確かに、コンパイラは通常、主流のコンピューターをターゲットにしている場合n / 2n >> 1自動的に最適化するようなものですが、組み込みデバイスをターゲットにしている場合、その贅沢は得られないかもしれません。

また、% 2および& 1異なる意味を持っています。除算とモジュラスは通常ゼロに丸められますが、実装が定義されています。良いオール>>&常に負の無限大に向かって丸めます(私の意見では)これははるかに理にかなっています。たとえば、私のコンピューターでは:

printf("%d\n", -1 % 2); // -1 (maybe)
printf("%d\n", -1 & 1); // 1

したがって、意味のあるものを使用してください。% 2あなたがもともと書くつもりだったときに使うことによってあなたが良い男の子であると思わないでください& 1

高価な浮動小数点演算

以下のような浮動小数点演算を避ける重いpow()log()整数を扱う場合は特に、本当に、それらを必要としないコードに。たとえば、数字を読んでみましょう:

int parseInt(const char *str)
{
    const char *p;
    int         digits;
    int         number;
    int         position;

    // Count the number of digits
    for (p = str; isdigit(*p); p++)
        {}
    digits = p - str;

    // Sum the digits, multiplying them by their respective power of 10.
    number = 0;
    position = digits - 1;
    for (p = str; isdigit(*p); p++, position--)
        number += (*p - '0') * pow(10, position);

    return number;
}

この使用pow()(およびそれを使用するために必要なint<-> double変換)がかなり高価であるだけでなく、精度が失われる可能性があります(偶然、上記のコードには精度の問題はありません)。そのため、このタイプの関数が数学以外のコンテキストで使用されているのを見ると、私はひるむ。

また、各反復で10倍になる以下の「賢い」アルゴリズムは、実際には上記のコードよりも簡潔であることに注意してください。

int parseInt(const char *str)
{
    const char *p;
    int         number;

    number = 0;
    for (p = str; isdigit(*p); p++) {
        number *= 10;
        number += *p - '0';
    }

    return number;
}

非常に徹底的な答え。
Paddyslacker

1
早期最適化の説明は、ガベージコードには適用されないことに注意してください。そもそも、うまく機能する実装を常に使用する必要があります。

strlen()は純粋な関数としてマークされているため、GCCは実際にこのケースを最適化することに注意してください。 あなたはそれがconst関数であり、純粋ではないことを意味すると思います。
アンディレスター

@アンディレスター:実際には、私は純粋に意味した。 GCCのドキュメントでは、const関数はグローバルメモリを読み取れないという点で、constはpureよりもわずかに厳密であると記載されています。 strlen()ポインター引数が指す文字列を調べます。つまり、constにできないことを意味します。また、strlen()実際にはglibcの中で純粋としてマークされているstring.h
ジョーイ・アダムス

あなたは正しい、私の間違いです、そして私は再確認すべきです。私はParrotプロジェクトに取り組んでおり、関数のどちらか、pureまたはconst2つに微妙な違いがあるため、ヘッダーファイルにそれを文書化しています。docs.parrot.org/parrot/1.3.0/html/docs/dev/c_functions.pod.html
アンディレスター

13

あなたの質問とコメントスレッドから、このコードの変更がパフォーマンスを向上させると「考える」ように聞こえますが、実際にそうなるかどうかはわかりません。

私はケントベックの哲学のファンです。

「機能させ、正しく機能させ、高速化します。」

コードのパフォーマンスを向上させる私のテクニックは、最初にユニットテストに合格して適切にファクタリングされたコードを取得し、次に(特にループ操作の場合)パフォーマンスをチェックするユニットテストを記述してから、コードをリファクタリングするか、別のアルゴリズムを考えます選択したveが期待どおりに機能していません。

たとえば、.NETコードで速度をテストするには、NUnitのタイムアウト属性を使用して、特定のメソッドの呼び出しが特定の時間内に実行されるというアサーションを記述します

NUnitのタイムアウト属性のようなものを指定したコード例(およびループの多数の反復)を使用して、コードの「改善」が実際にそのループのパフォーマンスに役立つかどうかを実際に証明できます。

免責事項:これは「ミクロ」レベルでは効果的ですが、パフォーマンスをテストする唯一の方法ではなく、「マクロ」レベルで発生する可能性のある問題を考慮に入れていません-良いスタートです。


2
私はプロファイリングを大いに信じていますが、クリスティアンが探しているヒントを心に留めておくのも賢明だと思います。常に読みやすい2つの方法のうち、速い方を選択します。成熟後の最適化を余儀なくされるのは楽しいことではありません。
AShelly

単体テストは必ずしも必要ではありませんが、パフォーマンスの神話が正しいかどうかを確認するためにこの20分を費やすことは常に価値があります。特に、回答はコンパイラと-Oおよび-gフラグ(またはDebug / VSの場合はリリースします。
mbq

+1この回答は、質問自体に関連するコメントを補足するものです。
タマラWijsman

1
@AShelly:ループ構文の単純な再定式化について話している場合、事実の後にそれを変更することは非常に簡単です。また、他のプログラマーにとっても、等しく読みやすいとは言えません。可能な限り「標準」構文を使用し、必要であることが証明された場合にのみ変更するのが最善です。
ジョーリSebrechts

@AShelly間違いなく、等しく読みやすい2つの方法を考えて、自分の仕事をしていないだけの効率の悪い方法を選択したらどうでしょうか。誰も実際にそれを行うだろうか?
グレナトロン

11

コンパイラが次のように変わる可能性があることに注意してください。

for(int i = 0; i < collection.length(); i++ ){
   // stuff here
}

に:

int j = collection.length();
for(int i = 0; i < j; i++ ){
   // stuff here
}

collectionループ全体で変更されていない場合、または同様の何か。

このコードがアプリケーションのタイムクリティカルセクションにある場合、これが該当するかどうか、または実際にこれを行うためにコンパイラオプションを変更できるかどうかを調べる価値があります。

これにより、コードの可読性が維持されます(前者はほとんどの人が期待するものであるため)が、これらのいくつかの余分なマシンサイクルが得られます。その後、コンパイラがあなたを助けられない他の領域に集中することができます。

サイドノート:collection要素を追加または削除してループ内で変更する場合(はい、それは悪い考えですが、実際に起こります)、2番目の例はすべての要素をループしないか、過去にアクセスしようとします配列の終わり。


1
なぜ明示的にしないのですか?

3
バウンドチェックを行う一部の言語では、明示的に行うとコードが遅くなります。collection.lengthへのループにより、コンパイラはそれを自動的に移動し、境界チェックを省略します。アプリの他の場所からの定数へのループを使用すると、反復ごとに境界チェックが行われます。それが測定することが重要である理由です-パフォーマンスに関する直感はほとんど正しくありません。
ケイトグレゴリー

1
だからこそ、「調べる価値があるだろう」と言ったのです。
ChrisF

stack.pop()のように、collection.length()がコレクションを変更しないことをC#コンパイラはどのように知ることができますか?コンパイラがこれを最適化すると仮定するのではなく、ILをチェックするのが最善だと思います。C ++では、メソッドをconst(「オブジェクトを変更しない」)としてマークできるため、コンパイラはこの最適化を安全に行うことができます。
JBRウィルキンソン

1
これを行う@JBRWオプティマイザーは、コレクションのメソッドのok-let's-call-it-constness-even-this-is-not-C ++ではないことも認識しています。結局のところ、何かがコレクションであることに気付き、その長さを取得する方法を知っている場合にのみ、境界チェックすることができます。
ケイトグレゴリー

9

通常、この種の最適化は推奨されません。この最適化はコンパイラによって簡単に実行できます。アセンブリではなく、より高いレベルのプログラミング言語で作業しているので、同じレベルで考えてください。


1
彼女にプログラミングに関する本を渡してください;)
ジョーリ・セブレヒト

1
+1。私たちのガールフレンドのほとんどは、コードの明快さよりもレディー・ガガに興味があるようです。
ハプロイド

推奨されない理由を説明してもらえますか?
マクニール

@macneilよく...そのトリックはコードをそれほど一般的ではなく、完全に機能しません。その最適化の一部はコンパイラーによって行われることになっています。
タクト

@macneilは、より高いレベルの言語で作業している場合、同じレベルで考えてください。
タクト

3

これは汎用コーディングにはあまり当てはまらないかもしれませんが、私は最近組み込み開発をほとんどしています。特定のターゲットプロセッサ(より速くなることはありません。20年以上後にシステムを廃止するまでには古くさい時代遅れに見えます)と、コードの大部分の非常に制限的なタイミングの期限があります。プロセッサは、すべてのプロセッサと同様に、どの操作が高速か低速かに関して特定の癖があります。

チーム全体の可読性を維持しながら、最も効率的なコードを生成するためのテクニックを使用しています。最も自然な言語構成では最も効率的なコードが生成されない場所では、最適なコードが使用されることを保証するマクロを作成しました。別のプロセッサで後続プロジェクトを行う場合、そのプロセッサで最適な方法に合わせてマクロを更新できます。

具体的な例として、現在のプロセッサでは、分岐がパイプラインを空にし、プロセッサを8サイクル停止させます。コンパイラは次のコードを受け取ります。

 bool isReady = (value > TriggerLevel);

と同等のアセンブリに変換します

isReady = 0
if (value > TriggerLevel)
{
  isReady = 1;
}

これには3サイクルかかるか、飛び越すと10サイクルかかりisReady=1;ます。しかし、プロセッサにはシングルサイクルのmax命令があるため、常に3サイクルかかることが保証されているこのシーケンスを生成するコードを記述する方がはるかに優れています。

diff = value-TriggerLevel;
diff = max(diff, 0);
isReady = min(1,diff);

明らかに、ここでの意図はオリジナルよりも明確ではありません。したがって、ブール値のより大きい比較が必要な場合に使用するマクロを作成しました。

#define BOOL_GT(a,b) min(max((a)-(b),0),1)

//isReady = value > TriggerLevel;
isReady = BOOL_GT(value, TriggerLevel);

他の比較でも同様のことができます。部外者にとっては、自然な構造のみを使用した場合よりもコードが少し読みにくくなります。ただし、コードの操作に少し時間を費やすとすぐに明らかになり、すべてのプログラマーが独自の最適化手法を試すよりもはるかに優れています。


3

まあ、最初のアドバイスは、コードに何が起こっているかを正確に知るまで、そのような時期尚早な最適化を回避することです。

たとえば、C#では、配列にアクセスするときにインデックスを範囲チェックする必要がないことがわかっているため、配列の長さをループしている場合、コンパイラはコードを最適化します。配列の長さを変数に入れて最適化しようとすると、ループと配列の間の接続が切断され、実際にコードがかなり遅くなります。

最適化を行う場合は、多くのリソースを使用することがわかっているものに限定する必要があります。パフォーマンスがわずかに向上する場合は、代わりに最も読みやすく保守可能なコードを使用する必要があります。コンピューターの動作は時間の経過とともに変化するので、あなたが見つけたものは今やや高速になりますが、そのままではないかもしれません。


3

私は非常にシンプルなテクニックを持っています。

  1. コードを機能させます。
  2. 速度をテストします。
  3. 速い場合は、他の機能について手順1に戻ります。遅い場合は、プロファイルを作成してボトルネックを見つけます。
  4. ボトルネックを修正します。手順1に戻ります。

このプロセスを回避するために時間を節約できる場合がたくさんありますが、一般的にはそうであるかどうかを知っています。疑問がある場合は、デフォルトでこれに固執します。


2

短絡を利用する:

if(someVar || SomeMethod())

コーディングには同じくらい時間がかかり、次のように読みやすくなります。

if(someMethod() || someVar)

それでも、時間の経過とともにより迅速に評価されます。


1

6か月待って、上司に新しいコンピューターを全員購入してもらいます。真剣に。プログラマーの時間は、長い目で見ればハードウェアよりもはるかに高価です。高性能コンピューターを使用すると、コード作成者は速度を気にすることなく、簡単な方法でコードを書くことができます。


6
えー...顧客が見るパフォーマンスはどうですか?あなたも彼らのために新しいコンピューターを買うのに十分な裕福ですか?
ロバートハーベイ

2
そして、私たちはパフォーマンスの壁をほぼ打ち破りました。マルチコア計算が唯一の方法ですが、待機してもプログラムで使用されません。
mbq

+1この回答は、質問自体に関連するコメントを補足するものです。
タマラWijsman

3
何千人または何百万人ものユーザーがいる場合、プログラミング時間はハードウェアほど高くありません。プログラマーの時間はユーザーの時間よりも重要ではありません。できるだけ早く頭に入れてください。
HLGEM

1
良い習慣を身に付けると、プログラマーの時間はかかりません。いつもやっていることです。
ドミニクマクドネル

1

事前に最適化しすぎないようにしてください。最適化するときは、読みやすさについて少し心配する必要はありません。

不必要な複雑さよりも嫌いなことはほとんどありませんが、複雑な状況に陥った場合、複雑なソリューションが必要になることがよくあります。

最も明白な方法でコードを記述する場合は、複雑な変更を加えたときにコードが変更された理由を説明するコメントを作成します。

ただし、具体的には、デフォルトのアプローチとは反対のブール値を使用すると、多くの場合に役立つことがわかります。

for(int i = 0, j = collection.length(); i < j; i++ ){
// stuff here
}

になることができる

for(int i = collection.length(); i > 0; i-=1 ){
// stuff here
}

多くの言語では、「スタッフ」部分に適切な調整を加え、それがまだ読み取り可能である限り。それは逆に数えられるので、ほとんどの人が最初にそれをすることを考える方法で問題にアプローチしません。

C#の例:

        string[] collection = {"a","b"};

        string result = "";

        for (int i = 0, j = collection.Count() - 1; i < j; i++)
        {
            result += collection[i] + "~";
        }

次のように書くこともできます。

        for (int i = collection.Count() - 1; i > 0; i -= 1)
        {
            result = collection[i] + "~" + result;
        }

(そして、はい、結合または文字列ビルダーでそれを行う必要がありますが、簡単な例を作成しようとしています)

使用するのが難しい他の多くのトリックがありますが、それらの多くは、文字列の再割り当てのペナルティを回避するため、またはバイナリモードでテキストファイルを読み取るために、古いvbの割り当ての左側でmidを使用するなど、すべての言語に適用されませんファイルがreadtoendには大きすぎる場合、.netでバッファリングのペナルティを回避します。

私が考えることができる唯一の他の本当に一般的なケースは、ブール代数を複雑な条件に適用して、方程式を短絡条件を活用する可能性が高い何かに変換しようとするか、または複合体を回すことですネストされたif-thenまたはcaseステートメントのセットから完全に方程式になります。これらのいずれもすべての場合に機能するわけではありませんが、大幅に時間を節約できます。


それは解決策ですが、ほとんどの一般的なクラスについてlength()が符号なしの型を返すため、コンパイラはおそらく警告を吐き出します
です-stijn

しかし、インデックスを逆にすると、反復自体がより複雑になる可能性があります。
タマラWijsman

@stijn私はそれを書いたときにc#を考えていましたが、おそらくこの提案はその理由のために言語固有のカテゴリに分類されます-編集を参照してください... @ToWijは確かに、この性質の提案があったとしても多くはないと思いますそのリスクはありません。あなたの// stuffが何らかのスタック操作である場合、ロジックを正しく反転することさえ不可能かもしれませんが、多くの場合、それらのほとんどのケースで慎重に行われれば、混乱しすぎません。
ビル

あなたが正しい; C ++では、 'normal'ループを引き続き使用しますが、length()呼び出しを反復から除外します(const size_t len = collection.length(); for(size_t i = 0; i <len; ++) i){})には2つの理由があります。「通常の」フォワードカウントループは読みやすく、理解しやすいと思います(しかし、それはおそらくそれがより一般的だからです)。
stijn

1
  1. プロフィール。問題さえありますか?どこ?
  2. 何らかの理由でIOに関連する90%のケースでは、キャッシュを適用します(そして、より多くのメモリを取得します)
  3. CPU関連の場合は、キャッシュを適用します
  4. それでもパフォーマンスが問題になる場合は、単純なテクニックの領域を残して、計算を行います。

1

優れたコンパイラ、優れたプロファイラ、優れたライブラリ- 見つけることができる最高のツールを使用してください。アルゴリズムを適切に、またはそれ以上に改善します-適切なライブラリを使用してそれを行います。些細なループ最適化は小さなポテトです。さらに、最適化コンパイラほど賢くはありません。


1

私にとって最も簡単なのは、一般的な使用パターンがたとえば[0、64)の範囲に収まるときはいつでもスタックを使用することですが、小さな上限のないまれなケースがあります。

単純なCの例(前):

void some_hotspot_called_in_big_loops(int n, ...)
{
    // 'n' is, 99% of the time, <= 64.
    int* values = calloc(n, sizeof(int));

    // do stuff with values
    ...
    free(values);
}

以降:

void some_hotspot_called_in_big_loops(int n, ...)
{
    // 'n' is, 99% of the time, <= 64.
    int values_mem[64] = {0}
    int* values = (n <= 64) ? values_mem: calloc(n, sizeof(int));

    // do stuff with values
    ...
    if (values != values_mem)
        free(values);
}

これらの種類のホットスポットがプロファイリングで多く現れるので、私はこれをそのように一般化しました:

void some_hotspot_called_in_big_loops(int n, ...)
{
    // 'n' is, 99% of the time, <= 64.
    MemFast values_mem;
    int* values = mf_calloc(&values_mem, n, sizeof(int));

    // do stuff with values
    ...

    mf_free(&values_mem);
}

上記では、99.9%のケースで割り当てられているデータが十分に小さい場合にスタックを使用し、そうでない場合はヒープを使用します。

C ++ではSmallVector、同じ概念を中心に展開する標準準拠の小さなシーケンス(既存の実装に類似)でこれを一般化しました。

それは壮大な最適化ではありません(たとえば、操作が1.8秒に完了するまでの時間を3秒から短縮しました)が、適用するにはこのような些細な努力が必要です。1行のコードを導入して2つを変更するだけで3秒から1.8秒に値を下げることができる場合、このような小さな金額にはかなり良い価値があります。


0

さて、アプリケーションに大きな影響を与えるデータにアクセスするときに行えるパフォーマンスの変更はたくさんあります。クエリを作成するか、ORMを使用してデータベースにアクセスする場合は、使用するデータベースバックエンドのパフォーマンスチューニングブックを読む必要があります。パフォーマンスの低い既知の技術を使用している可能性があります。無知を除いてこれを行う理由はありません。これは時期尚早な最適化ではありません(パフォーマンスを気にしないほど広く絡み合っているため、私はこれを言った男を呪います)。これは良い設計です。

SQL Serverのパフォーマンスエンハンサーの簡単なサンプル:適切なインデックスを使用し、カーソルを避けます-セットベースのロジックを使用し、sargable where句を使用し、ビューの上にビューを重ねないで、必要以上のデータを返さないでください必要な列以外は、相関サブクエリを使用しないでください。


0

これがC ++の場合、の習慣で++iはなくを取得する必要がありますi++++i決して悪化することはありません。これはスタンドアロンステートメントとまったく同じことを意味し、場合によってはパフォーマンスの向上になる可能性があります。

偶然に既存のコードを変更するだけの価値はありませんが、入るのは良い習慣です。


0

私はそれについて少し異なる見解を持っています。ここで得たアドバイスに従うだけでは、大きな違いはありません。ミスがいくつかあり、修正する必要があり、それから学習する必要があるからです。

あなたがしなければならない間違いは、誰もがするようにデータ構造を設計することです。つまり、冗長データと多くの抽象化レイヤー、および構造の一貫性を維持しようとする構造全体に伝播するプロパティと通知を使用します。

次に、パフォーマンスチューニング(プロファイリング)を行う必要があり、多くの方法で多くのサイクルを犠牲にしているのは、構造の一貫性を維持しようとして構造全体に伝播するプロパティと通知を含む、多くの抽象化のレイヤーである方法を示す必要があります。

コードに大きな変更を加えることなく、これらの問題をいくらか修正できる場合があります。

その後、運が良ければ、データ構造が少ないほど良いこと、そして多くのことをメッセージの波にしっかりと合わせようとするよりも、一時的な矛盾に耐えることができることを学ぶことができます。

ループの記述方法は、実際には関係ありません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.