web-dev-qa-db-ja.com

コンパイラ/オプティマイザがより高速なプログラムを作成できるようにするコーディングプラクティス

何年も前、Cコンパイラは特に賢くはありませんでした。回避策として、K&Rはregisterキーワードを発明し、コンパイラーにヒントを与えるために、この変数を内部レジスターに保持することをお勧めします。彼らはまた、より良いコードを生成するために三次演算子を作成しました。

時間が経つにつれて、コンパイラは成熟しました。彼らは、フロー解析により、レジスタに保持する値について、あなたができることよりも良い決定を下せるという点で非常に賢くなった。 registerキーワードは重要ではなくなりました。

alias の問題により、ある種の操作ではFORTRANがCよりも高速になります。理論的には慎重にコーディングすれば、この制限を回避してオプティマイザーがより高速なコードを生成できるようにすることができます。

コンパイラ/オプティマイザがより高速なコードを生成できるようにする可能性のあるコーディング慣行はありますか?

  • 使用するプラットフォームとコンパイラを特定していただければ幸いです。
  • なぜこのテクニックが機能するように見えるのですか?
  • サンプルコードが推奨されます。

関連する質問

[編集]この質問は、プロファイリングおよび最適化するプロセス全体に関するものではありません。プログラムが正しく記述され、完全に最適化されてコンパイルされ、テストされ、本番環境に投入されたと仮定します。コード内に、オプティマイザーが可能な限り最高のジョブを実行することを禁止する構成体がある場合があります。これらの禁止事項を取り除き、オプティマイザーがさらに高速なコードを生成できるようにリファクタリングするにはどうすればよいですか?

[編集]オフセット関連リンク

116
EvilTeach

引数を出力せずにローカル変数に書き込みます!これは、エイリアスのスローダウンを回避するための大きな助けになります。たとえば、コードが次のように見える場合

void DoSomething(const Foo& foo1, const Foo* foo2, int numFoo, Foo& barOut)
{
    for (int i=0; i<numFoo, i++)
    {
         barOut.munge(foo1, foo2[i]);
    }
}

コンパイラはfoo1!= barOutを知らないため、ループを通るたびにfoo1をリロードする必要があります。また、barOutへの書き込みが完了するまで、foo2 [i]を読み取ることはできません。制限されたポインターをいじり始めることもできますが、これを行うのと同じくらい効果的です(はるかに明確です)。

void DoSomethingFaster(const Foo& foo1, const Foo* foo2, int numFoo, Foo& barOut)
{
    Foo barTemp = barOut;
    for (int i=0; i<numFoo, i++)
    {
         barTemp.munge(foo1, foo2[i]);
    }
    barOut = barTemp;
}

馬鹿げているように聞こえますが、コンパイラはローカル変数をよりスマートに処理できます。これは、メモリ内で引数と重複しないためです。これは、恐ろしいload-hit-store(このスレッドでFrancis Boivinが言及)を回避するのに役立ちます。

54
celion

これは、コンパイラーが高速コード(あらゆる言語、あらゆるプラットフォーム、あらゆるコンパイラー、あらゆる問題)を作成するのに役立つコーディングの実践です。

notは、コンパイラーがメモリー(キャッシュやレジスターを含む)に最適と思われるように変数をレイアウトすることを強制する、または奨励する巧妙なトリックを使用します。まず、正しく保守可能なプログラムを作成します。

次に、コードのプロファイルを作成します。

その後、そしてそのときだけ、コンパイラーにメモリーの使用方法を伝えることの効果の調査を開始することができます。一度に1つの変更を加えて、その影響を測定します。

がっかりしたり、小さなパフォーマンスの改善のために非常に一生懸命働かなければならないことを期待してください。 FortranやCなどの成熟した言語向けの最新のコンパイラは、非常に優れています。コードのパフォーマンスを向上させるために「トリック」のアカウントを読んだ場合、コンパイラー作成者もそれについて読んでおり、実行する価値がある場合はおそらく実装していることに留意してください。彼らはおそらくあなたが最初に読んだものを書いたでしょう。

73

メモリを走査する順序はパフォーマンスに大きな影響を与える可能性があり、コンパイラーはそれを把握して修正するのがあまり得意ではありません。パフォーマンスを重視する場合は、コードを記述するときにキャッシュの局所性の問題に注意する必要があります。たとえば、Cの2次元配列は、行優先形式で割り当てられます。列メジャー形式で配列を走査すると、キャッシュミスが多くなり、プログラムのプロセッサバウンドよりもメモリバウンドが大きくなる傾向があります。

#define N 1000000;
int matrix[N][N] = { ... };

//awesomely fast
long sum = 0;
for(int i = 0; i < N; i++){
  for(int j = 0; j < N; j++){
    sum += matrix[i][j];
  }
}

//painfully slow
long sum = 0;
for(int i = 0; i < N; i++){
  for(int j = 0; j < N; j++){
    sum += matrix[j][i];
  }
}
47
vicatcu

一般的な最適化

ここに私のお気に入りの最適化のいくつかとして。これらを使用することで、実際に実行時間を増やし、プログラムサイズを小さくしました。

小さい関数をinlineまたはマクロとして宣言する

関数(またはメソッド)を呼び出すたびに、変数をスタックにプッシュするなどのオーバーヘッドが発生します。一部の関数では、戻り時にオーバーヘッドが発生する場合があります。非効率的な関数またはメソッドのコンテンツには、結合オーバーヘッドよりも少ないステートメントが含まれています。これらは、#defineマクロまたはinline関数としてインライン化するのに適した候補です。 (はい、inlineは単なる提案であることがわかりますが、この場合、コンパイラーへのリマインダーと見なします。)

デッドコードと冗長コードを削除する

コードが使用されていないか、プログラムの結果に寄与しない場合は、それを取り除きます。

アルゴリズムの設計を簡素化

私はかつて、計算中の代数方程式を書き留めてプログラムから多くのアセンブリコードと実行時間を削除し、代数式を単純化しました。簡略化された代数式の実装は、元の関数よりも少ないスペースと時間しか使用しませんでした。

ループ展開

各ループには、増分および終了チェックのオーバーヘッドがあります。パフォーマンス係数の推定値を取得するには、オーバーヘッド内の命令の数をカウントし(最小3:インクリメント、チェック、ループの開始に移動)、ループ内のステートメントの数で除算します。数値が小さいほど優れています。

Edit:ループ展開の例を提供します

unsigned int sum = 0;
for (size_t i; i < BYTES_TO_CHECKSUM; ++i)
{
    sum += *buffer++;
}

展開後:

unsigned int sum = 0;
size_t i = 0;
**const size_t STATEMENTS_PER_LOOP = 8;**
for (i = 0; i < BYTES_TO_CHECKSUM; **i = i / STATEMENTS_PER_LOOP**)
{
    sum += *buffer++; // 1
    sum += *buffer++; // 2
    sum += *buffer++; // 3
    sum += *buffer++; // 4
    sum += *buffer++; // 5
    sum += *buffer++; // 6
    sum += *buffer++; // 7
    sum += *buffer++; // 8
}
// Handle the remainder:
for (; i < BYTES_TO_CHECKSUM; ++i)
{
    sum += *buffer++;
}

この利点では、プロセッサが命令キャッシュをリロードする前に、より多くのステートメントが実行されるという副次的な利点が得られます。

32個のステートメントへのループを展開すると、驚くべき結果が得られました。プログラムは2GBファイルのチェックサムを計算しなければならなかったため、これはボトルネックの1つでした。この最適化とブロック読み取りを組み合わせることで、パフォーマンスが1時間から5分に向上しました。ループの展開は、アセンブリ言語でも優れたパフォーマンスを提供しました。私のmemcpyは、コンパイラのmemcpyよりもはるかに高速でした。 -T.M。

ifステートメントの削減

プロセッサは、命令またはキューのリロードをプロセッサに強制するため、分岐またはジャンプを嫌います。

ブール演算(Edited:コードフラグメントにコードフォーマットを適用し、例を追加)

ifステートメントをブール値の割り当てに変換します。一部のプロセッサは、分岐せずに条件付きで命令を実行できます。

bool status = true;
status = status && /* first test */;
status = status && /* second test */;

論理AND演算子(&&)の短絡は、テストの実行を妨げますstatusfalseの場合。

例:

struct Reader_Interface
{
  virtual bool  write(unsigned int value) = 0;
};

struct Rectangle
{
  unsigned int Origin_x;
  unsigned int Origin_y;
  unsigned int height;
  unsigned int width;

  bool  write(Reader_Interface * p_reader)
  {
    bool status = false;
    if (p_reader)
    {
       status = p_reader->write(Origin_x);
       status = status && p_reader->write(Origin_y);
       status = status && p_reader->write(height);
       status = status && p_reader->write(width);
    }
    return status;
};

ループ外の因子変数の割り当て

変数がループ内でオンザフライで作成される場合、作成/割り当てをループの前に移動します。ほとんどの場合、各反復中に変数を割り当てる必要はありません。

ループ外の定数式の因数分解

計算値または変数値がループインデックスに依存しない場合は、ループの外側(前)に移動します。

ブロック単位のI/O

データを大きなチャンク(ブロック)で読み書きします。大きければ大きいほど良い。たとえば、一度に1つのoctectを読み取ると、1回の読み取りで1024オクテットを読み取るよりも効率が低下します。
例:

static const char  Menu_Text[] = "\n"
    "1) Print\n"
    "2) Insert new customer\n"
    "3) Destroy\n"
    "4) Launch Nasal Demons\n"
    "Enter selection:  ";
static const size_t Menu_Text_Length = sizeof(Menu_Text) - sizeof('\0');
//...
std::cout.write(Menu_Text, Menu_Text_Length);

この手法の効率は視覚的に実証できます。 :-)

定数データにはprintffamilyを使用しないでください

定数データは、ブロック書き込みを使用して出力できます。書式付きの書き込みは、文字の書式設定や書式設定コマンドの処理のためにテキストをスキャンする時間を無駄にします。上記のコード例を参照してください。

メモリにフォーマットしてから書き込む

複数のcharを使用してsprintf配列にフォーマットし、fwriteを使用します。これにより、データレイアウトを「定数セクション」と可変セクションに分割することもできます。 mail-mergeと考えてください。

定数テキスト(文字列リテラル)をstatic constとして宣言します

変数がstaticなしで宣言されると、一部のコンパイラはスタックにスペースを割り当て、ROMからデータをコピーします。これらは2つの不必要な操作です。これは、staticプレフィックスを使用して修正できます。

最後に、コンパイラのようなコードは

時には、コンパイラーは、1つの複雑なバージョンよりもいくつかの小さなステートメントを最適化できます。また、コンパイラが最適化するのに役立つコードを書くことも役立ちます。コンパイラに特別なブロック転送命令を使用させたい場合、特別な命令を使用するように見えるコードを記述します。

36
Thomas Matthews

オプティマイザーは、プログラムのパフォーマンスを実際に制御するわけではありません。適切なアルゴリズムと構造、プロファイル、プロファイル、プロファイルを使用します。

ただし、インライン化が停止されるため、別のファイルの1つのファイルから小さな関数を内部ループすることはできません。

可能であれば、変数のアドレスを取得しないでください。ポインタを要求することは、変数をメモリに保持する必要があることを意味するため、「無料」ではありません。ポインタを避ければ、配列でさえレジスタに保持できます。これはベクトル化に不可欠です。

次のポイントにつながる^#$ @マニュアルを読む!ここに___restrict___と__attribute__( __aligned__ )を振りかけると、GCCはプレーンなCコードをベクトル化できます。オプティマイザーに非常に具体的なものが必要な場合は、具体的にする必要があります。

26
Potatoswatter

最新のプロセッサでは、最大のボトルネックはメモリです。

エイリアシング:Load-Hit-Storeは、タイトなループで壊滅的です。あるメモリー位置を読み取り、別のメモリー位置に書き込み、それらが互いにばらばらであることを知っている場合、関数パラメーターにエイリアスキーワードを慎重に置くと、コンパイラーがより高速なコードを生成するのに役立ちます。ただし、メモリ領域が重複している場合に「エイリアス」を使用すると、未定義の動作の適切なデバッグセッションが開始されます。

キャッシュミス:アルゴリズムのほとんどがコンパイラであるため、どのようにコンパイラを支援できるかは確かではありませんが、メモリをプリフェッチする組み込み関数があります。

また、異なるレジスタを使用し、あるタイプから別のタイプに変換することは、実際の変換命令を呼び出し、値をメモリに書き込み、適切なレジスタセットに戻すことを意味するため、浮動小数点値をintに、またはその逆に変換しようとしないでください。

18
Francis Boivin

人々が書くコードの大部分はI/Oバウンドになります(過去30年間にお金のために書いたすべてのコードはそのようにバインドされていると思います)。したがって、ほとんどの人のオプティマイザーの活動は学術的です。

ただし、コードを最適化するには、コンパイラーに最適化するようにコンパイラーに指示する必要があることを人々に思い起こさせます-多くの人々(忘れてしまったときも含めて)は、最適化機能を有効にしないと意味のないC++ベンチマークをここに投稿します。

11
anon

コードでは可能な限りconstの正確さを使用してください。これにより、コンパイラーの最適化が大幅に向上します。

このドキュメントには、他の最適化のヒントがたくさんあります: CPPの最適化 (少し古いドキュメントですが)

ハイライト:

  • コンストラクターの初期化リストを使用する
  • プレフィックス演算子を使用する
  • 明示的なコンストラクタを使用する
  • インライン関数
  • 一時的なオブジェクトを避ける
  • 仮想機能のコストに注意する
  • 参照パラメーターを介してオブジェクトを返す
  • クラスごとの割り当てを考慮する
  • stlコンテナアロケータを検討する
  • 「空のメンバー」の最適化
11
Toad

できるだけ静的な単一の割り当てを使用してプログラミングを試みます。 SSAは、ほとんどの関数型プログラミング言語で得られるものとまったく同じです。ほとんどのコンパイラーは、作業しやすいため、最適化を行うためにコードを変換します。これを行うことにより、コンパイラが混乱する可能性のある場所が明らかになります。また、最悪のレジスタアロケータを除くすべてが最高のレジスタアロケータと同じように機能し、1つの場所しか割り当てられていないため、変数がどこから値を取得したのかをほとんど気にする必要がないため、より簡単にデバッグできます。
グローバル変数は避けてください。

参照またはポインターによってデータを操作する場合、それをローカル変数に引き込み、作業を行ってからコピーして戻します。 (そうしない正当な理由がない限り)

数学または論理演算を行うときにほとんどのプロセッサが提供する0とほぼ無料の比較を使用します。ほとんどの場合、== 0および<0のフラグを取得し、そこから3つの条件を簡単に取得できます。

x= f();
if(!x){
   a();
} else if (x<0){
   b();
} else {
   c();
}

ほとんどの場合、他の定数をテストするよりも安価です。

もう1つのトリックは、減算を使用して、範囲テストで1つの比較を排除することです。

#define FOO_MIN 8
#define FOO_MAX 199
int good_foo(int foo) {
    unsigned int bar = foo-FOO_MIN;
    int rc = ((FOO_MAX-FOO_MIN) < bar) ? 1 : 0;
    return rc;
} 

これにより、ブール式で短絡する言語のジャンプを非常に頻繁に回避でき、コンパイラーは最初の比較の結果に遅れずに対応し、2番目の比較を行ってからそれらを結合する方法を見つけようとする必要がなくなります。これは、余分なレジスタを使い果たす可能性があるように見えますが、ほとんど使いません。多くの場合、とにかくfooはもう必要ありません。あなたがrcを使用する場合は、まだ使用されていないため、そこに移動できます。

Cの文字列関数(strcpy、memcpy、...)を使用する場合、それらが返すものを覚えておいてください-宛先!多くの場合、宛先へのポインターのコピーを「忘れて」、これらの関数の戻り値から取得するだけで、より良いコードを取得できます。

最後に呼び出した関数が返したものとまったく同じものを返す機会を決して見逃さないでください。コンパイラーはそれを拾うのにそれほど優れていません:

foo_t * make_foo(int a, int b, int c) {
        foo_t * x = malloc(sizeof(foo));
        if (!x) {
             // return NULL;
             return x; // x is NULL, already in the register used for returns, so duh
        }
        x->a= a;
        x->b = b;
        x->c = c;
        return x;
}

もちろん、戻りポイントが1つしかない場合は、そのロジックを逆にすることができます。

(後で思い出したトリック)

可能な場合に関数を静的として宣言することは常に良い考えです。コンパイラーは、特定の関数のすべての呼び出し元を説明したことを証明できる場合、最適化の名前でその関数の呼び出し規約を破ることができます。コンパイラーは、パラメーターをレジスターまたはスタック関数に移動することを避けることができます。通常、呼び出される関数はパラメーターが存在することを期待します(呼び出される関数と、これを行うためにすべての呼び出し元の位置の両方で逸脱する必要があります)。また、コンパイラは、呼び出された関数が必要とするメモリとレジスタを知ることを活用し、呼び出された関数が邪魔しないレジスタまたはメモリの場所にある変数値を保持するコードの生成を回避できます。これは、関数の呼び出しがほとんどない場合に特にうまく機能します。これにより、コードをインライン化するメリットが得られますが、実際にはインライン化されません。

9
nategoose

最適化Cコンパイラーを作成しましたが、以下に考慮すべきいくつかの非常に有用なことを示します。

  1. ほとんどの関数を静的にします。これにより、手続き間定数の伝播とエイリアス分析が可能になります。そうでない場合、コンパイラは、パラメータの完全に未知の値を使用して、変換ユニットの外部から関数を呼び出すことができると想定する必要があります。よく知られているオープンソースライブラリを見ると、本当に外部である必要があるものを除いて、それらはすべて関数を静的とマークしています。

  2. グローバル変数が使用されている場合は、可能であれば静的および定数としてマークします。一度初期化された場合(読み取り専用)、static const int VAL [] = {1,2,3,4}のような初期化リストを使用することをお勧めします。そうしないと、コンパイラーは変数が実際に初期化された定数であることを発見できず、変数からの負荷を定数に置き換えられません。

  3. ループの内部にgotoを使用しないでください。ほとんどのコンパイラではループが認識されなくなり、最も重要な最適化は適用されません。

  4. 必要な場合にのみポインタパラメータを使用し、可能であれば制限をマークします。プログラマーはエイリアスがないことを保証するので、これはエイリアス分析を大いに助けます(手続き間のエイリアス分析は通常非常に原始的です)。非常に小さな構造体オブジェクトは、参照ではなく値で渡す必要があります。

  5. 特にループ内(a [i])で可能な限り、ポインターの代わりに配列を使用します。配列は通常、エイリアス分析のためにより多くの情報を提供し、いくつかの最適化の後、とにかく同じコードが生成されます(好奇心があればループ強度の低下を検索します)。これにより、ループ不変コードモーションが適用される可能性も高くなります。

  6. 大きな関数または副作用のない外部関数(現在のループの繰り返しに依存しない)へのループ呼び出しの外側で巻き上げてみてください。多くの場合、小さな関数はインライン化またはホイストしやすい組み込み関数に変換されますが、大きな関数はコンパイラーが実際にそうしない場合に副作用があるように見える場合があります。外部関数の副作用は、一部のコンパイラーによってモデル化されることがある標準ライブラリの一部の例外を除き、完全に未知であり、ループ不変のコードモーションを可能にします。

  7. 複数の条件でテストを作成する場合、最も可能性の高い条件を最初に配置します。 if(a || b || c)は、ifbが他よりも真である可能性が高い場合にif(b || a || c)でなければなりません。コンパイラは通常、条件の可能な値と、どの分岐がより多く取られるかについて何も知りません(プロファイル情報を使用して知ることができますが、それを使用するプログラマーはほとんどいません)。

  8. switchを使用すると、if(a || b || ... || z)のようなテストを実行するよりも高速です。コンパイラがこれを自動的に行うかどうかを最初に確認し、一部のコンパイラはこれを行い、ifを使用する方が読みやすくなります。

9
Gratian Lup

組み込みシステムとC/C++で記述されたコードの場合は、回避しようとします 動的メモリ割り当て できるだけ。私がこれを行う主な理由は必ずしもパフォーマンスではありませんが、この経験則はパフォーマンスに影響します。

ヒープの管理に使用されるアルゴリズムは、一部のプラットフォーム(vxworksなど)で有名です。さらに悪いことに、mallocの呼び出しから戻るのにかかる時間は、ヒープの現在の状態に大きく依存しています。したがって、mallocを呼び出す関数は、簡単には説明できないパフォーマンスヒットを被ります。ヒープがまだクリーンであるが、そのデバイスがしばらく実行された後、ヒープが断片化する可能性がある場合、そのパフォーマンスヒットは最小限になります。呼び出しにはさらに時間がかかり、時間の経過に伴うパフォーマンスの低下を簡単に計算することはできません。最悪のケースの推定値を実際に生成することはできません。この場合、オプティマイザーはヘルプを提供できません。さらに悪いことに、ヒープが過度に断片化されると、呼び出しは完全に失敗し始めます。解決策は、ヒープの代わりにメモリプールを使用することです(例: glib slices )。割り当てを正しく行うと、割り当て呼び出しははるかに高速で決定的になります。

7
figurassa

愚かな小さなヒントですが、微視的な量の速度とコードを節約するものです。

関数の引数は常に同じ順序で渡します。

F_2を呼び出すf_1(x、y、z)がある場合、f_2(x、y、z)としてf_2を宣言します。 f_2(x、z、y)として宣言しないでください。

この理由は、C/C++プラットフォームABI(別名呼び出し規約)が特定のレジスターとスタック位置に引数を渡すことを約束するためです。引数がすでに正しいレジスタにある場合、引数を移動する必要はありません。

逆アセンブルされたコードを読んでいる間、人々がこのルールに従わなかったために、ばかげたレジスターのシャッフルが見られました。

7
Zan Lynx

上記のリストでは見なかった2つのコーディングテクニック:

一意のソースとしてコードを記述することでリンカをバイパスします

個別のコンパイルはコンパイル時間には本当にいいですが、最適化について話すと非常に悪いです。基本的に、コンパイラはコンパイル単位、つまりリンカ予約ドメインを超えて最適化できません。

しかし、プログラムを適切に設計すれば、独自の共通ソースを介してコンパイルすることもできます。それは、unit1.cとunit2.cをコンパイルする代わりに、両方のオブジェクトをリンクし、#include unit1.cとunit2.cのみを含むall.cをコンパイルします。したがって、すべてのコンパイラー最適化の恩恵を受けることができます。

これは、C++でヘッダーのみのプログラムを作成するのに非常に似ています(さらに、Cで行う方が簡単です)。

この手法は、プログラムを最初から使用可能にする場合は簡単ですが、Cセマンティックの一部が変更されることにも注意する必要があり、静的変数やマクロの衝突などの問題に対処できます。ほとんどのプログラムでは、発生する小さな問題を克服するのに十分簡単です。また、独自のソースとしてコンパイルする方法は非常に遅く、膨大な量のメモリを必要とする可能性があることに注意してください(通常、最新のシステムでは問題ありません)。

この簡単なテクニックを使用して、たまたま私が書いたプログラムを10倍高速化しました!

Registerキーワードと同様に、このトリックもまもなく廃止される可能性があります。コンパイラによる最適化のサポートは、コンパイラによってサポートされ始めました gcc:Link time Optimization

ループ内の個別のアトミックタスク

これはもっとトリッキーです。アルゴリズム設計と、オプティマイザーがキャッシュとレジスターの割り当てを管理する方法との相互作用についてです。多くの場合、プログラムは何らかのデータ構造をループ処理し、各アイテムに対していくつかのアクションを実行する必要があります。実行されるアクションは、多くの場合、論理的に独立した2つのタスクに分割できます。その場合、1つのタスクを実行する同じ境界で2つのループを使用して、まったく同じプログラムを作成できます。場合によっては、このように記述すると、一意のループよりも高速になる場合があります(詳細はより複雑ですが、説明は、単純なタスクの場合、すべての変数をプロセッサレジスタに保持でき、より複雑な変数では不可能であり、レジスタはメモリに書き込まれ、後で読み戻される必要があり、追加のフロー制御よりもコストが高くなります。

Registerを使用するように、これを改善するよりもパフォーマンスが低下する可能性があるため、これに注意してください(このトリックを使用してパフォーマンスをプロファイルするかどうか)。

5
kriss

ほとんどの最新のコンパイラは、関数呼び出しを最適化することができるため、 tail recursion を高速化する優れた仕事をするはずです。

例:

int fac2(int x, int cur) {
  if (x == 1) return cur;
  return fac2(x - 1, cur * x); 
}
int fac(int x) {
  return fac2(x, 1);
}

もちろん、この例には境界チェックがありません。

後期編集

コードの直接的な知識はありませんが、 SQL ServerでCTEを使用する要件は、末尾再帰を介して最適化できるように特別に設計されていることは明らかです。

4
Hogan

同じ作業を何度も繰り返さないでください!

私が見る一般的なアンチパターンは、これらの線に沿っています:

void Function()
{
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomething();
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingElse();
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingCool();
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingReallyNeat();
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingYetAgain();
}

コンパイラは、実際には常にこれらの関数をすべて呼び出す必要があります。プログラマーは、これらすべての呼び出しの間、集約されたオブジェクトが変化しないことを知っていると仮定します。

void Function()
{
   MySingleton* s = MySingleton::GetInstance();
   AggregatedObject* ao = s->GetAggregatedObject();
   ao->DoSomething();
   ao->DoSomethingElse();
   ao->DoSomethingCool();
   ao->DoSomethingReallyNeat();
   ao->DoSomethingYetAgain();
}

シングルトンゲッターの場合、呼び出しのコストはそれほど高くないかもしれませんが、確かにコストがかかります(通常、「オブジェクトが作成されているかどうかを確認し、作成されていない場合は作成してから返す」)。このゲッターのチェーンがより複雑になると、無駄な時間が増えます。

4
dash-tom-bang

私は実際にこれをSQLiteで見ましたが、パフォーマンスが約5%向上すると主張しています:すべてのコードを1つのファイルに入れるか、プリプロセッサを使用してこれと同等のことをします。このようにして、オプティマイザーはプログラム全体にアクセスし、より多くのプロシージャー間の最適化を実行できます。

4
dsimcha
  1. すべての変数宣言に可能な限りローカルなスコープを使用してください。

  2. 可能な限りconstを使用します

  3. Dont registerを使用する場合と使用しない場合の両方のプロファイルを作成する場合を除き、registerを使用します

これらのうち最初の2つ、特に#1は、オプティマイザーがコードを分析するのに役立ちます。特に、レジスタに保持する変数について適切な選択を行うのに役立ちます。

Registerキーワードを盲目的に使用すると、最適化が損なわれる可能性があります。Assemblyの出力またはプロファイルを確認するまで、何が重要かを知るのは非常に困難です。

コードから良好なパフォーマンスを得るには、他にも重要なことがあります。たとえば、キャッシュの一貫性を最大化するようにデータ構造を設計します。しかし、問題はオプティマイザーに関するものでした。

3
John Knoeller

@ -MSaltersのコメントから学んだきちんとしたテクニック この回答 は、条件に応じて異なるオブジェクトを返す場合でもコンパイラーが省略を実行できるようにします。

// before
BigObject a, b;
if(condition)
  return a;
else
  return b;

// after
BigObject a, b;
if(condition)
  swap(a,b);
return a;
3
Xeo

一度遭遇したことを思い出しました。症状は単にメモリが不足しているという単純なものでしたが、その結果、パフォーマンスが大幅に向上しました(メモリフットプリントが大幅に削減されました)。

この場合の問題は、使用していたソフトウェアが大量の小さな割り当てを行ったことでした。同様に、ここに4バイト、そこに6バイトなどを割り当てます。多くの小さなオブジェクトも8〜12バイトの範囲で実行されます。問題は、プログラムが多くのささいなことを必要とするほどではなかった。それは、多くのささいなことを個別に割り当てたため、各割り当てが(この特定のプラットフォームで)32バイトに膨らんだ。

ソリューションの一部は、Alexandrescuスタイルの小さなオブジェクトプールをまとめることでしたが、それを拡張して、小さなオブジェクトの配列と個々のアイテムを割り当てることができました。より多くのアイテムが一度にキャッシュに収まるため、これはパフォーマンスにおいても非常に役立ちました。

ソリューションの他の部分は、手作業で管理されたchar *メンバーの横行する使用をSSO(短文字列最適化)文字列に置き換えることでした。最小の割り当ては32バイトで、char *の後ろに埋め込まれた28文字のバッファーを持つ文字列クラスを構築したため、文字列の95%は追加の割り当てを行う必要がありませんでした(その後、この新しいクラスでこのライブラリのchar *、それは楽しかったかどうか)。これにより、メモリの断片化も大幅に軽減され、他のポイント先オブジェクトの参照の局所性が向上し、同様にパフォーマンスが向上しました。

3
dash-tom-bang
3
EvilTeach

あなたが繰り返し呼び出す小さな関数を持っている場合、私は過去に「静的インライン」としてヘッダーにそれらを置くことによって大きな利益を得ました。 ix86での関数呼び出しは驚くほど高価です。

明示的なスタックを使用して非再帰的な方法で再帰関数を再実装することも多くのメリットを得ることができますが、実際には開発時間とゲインの領域にいます。

2
Remy

最適化のアドバイスの2番目の部分を次に示します。私の最初のアドバイスと同様に、これは一般的な目的であり、言語やプロセッサ固有のものではありません。

コンパイラのマニュアルをよく読み、それが何を伝えているのかを理解してください。コンパイラを最大限に使用してください。

プログラムからパフォーマンスを絞り出すために適切なアルゴリズムを選択することが重要であると判断した他の1人または2人の回答者に同意します。それを超えて、コンパイラーの使用に投資する時間でのリターン率(コード実行の改善で測定)は、コードの調整でのリターン率よりもはるかに高くなります。

はい、コンパイラライターはコーディングジャイアントの競争から生まれたものではなく、コンパイラには間違いがあり、マニュアルとコンパイラの理論によれば、物事を高速化することが、時には物事を遅くするはずです。そのため、一度に1つの手順を実行し、調整前と調整後のパフォーマンスを測定する必要があります。

そして、最終的には、コンパイラフラグの組み合わせの爆発に直面する可能性があるため、さまざまなコンパイラフラグを使用してmakeを実行し、大規模なクラスターのジョブをキューに入れて、実行時の統計を収集するスクリプトが必要です。 PCとVisual Studioだけの場合は、十分なコンパイラフラグの十分な組み合わせを試すかなり前に興味がなくなります。

よろしく

マーク

最初にコードの一部を取得するとき、通常、1.4倍から2.0倍のパフォーマンスを得ることができます(つまり、新しいバージョンのコードは、古いバージョンの1/1.4または1/2の時間で実行されます)コンパイラフラグをいじって1〜2日。確かに、それは私の優秀さの兆候ではなく、私が取り組んでいるコードの多くを生み出した科学者の間でコンパイラーに精通していないというコメントかもしれません。コンパイラフラグをmaxに設定すると(そして-O3になることはめったにありません)、1.05または1.1の別の係数を取得するのに数か月のハードワークが必要になる場合があります

DECがアルファプロセッサを発表したとき、コンパイラは常に最大6つの引数をレジスタに自動的に配置しようとするため、関数の引数の数を7未満に保つことを推奨していました。

2
EvilTeach

私がやったことの1つは、ユーザーがプログラムが少し遅れると予想する場所に高価なアクションを維持しようとすることです。全体的なパフォーマンスは応答性に関連していますが、まったく同じではなく、多くの場合、応答性がパフォーマンスのより重要な部分です。

前回全体的なパフォーマンスの改善を実際に行わなければならなかったとき、次善のアルゴリズムに目を光らせ、キャッシュの問題がある可能性が高い場所を探しました。最初にパフォーマンスのプロファイルを作成して測定し、各変更後にパフォーマンスを再測定しました。その後、会社は崩壊しましたが、とにかく面白くて有益な仕事でした。

1
David Thornley

あなたはここで良い答えを得ていますが、彼らはあなたのプログラムが最初に最適にかなり近いと仮定し、あなたは言います

プログラムが正しく作成され、完全に最適化されてコンパイルされ、テストされ、本番環境に投入されたと仮定します。

私の経験では、プログラムは正しく書かれているかもしれませんが、それは最適に近いという意味ではありません。その点に到達するには、余分な作業が必要です。

例を挙げれば、 この答え は、macro-optimizationによって完全に合理的な外観のプログラムが40倍以上高速化されたことを示しています。 すべてのプログラムで最初に書かれたように大きな高速化はできませんが、多くの(非常に小さなプログラムを除く)では、私の経験ではできます。

それが完了したら、マイクロ最適化(ホットスポットの)が良い見返りを与えます。

1
Mike Dunlavey

私はインテルのコンパイラを使用しています。 WindowsとLinuxの両方で。

多かれ少なかれ私はコードをプロファイルします。その後、ホットスポットでハングアップし、コンパイラーがより良い仕事を行えるようにコードを変更しようとします。

コードが計算コードであり、多くのループが含まれている場合-インテル®コンパイラーのベクトル化レポートは非​​常に役立ちます-ヘルプで「vec-report」を探してください。

だから主なアイデア-パフォーマンスが重要なコードを磨きます。残りに関しては-優先順位が正しく、保守可能であること-短い機能、1年後に理解できる明確なコード。

1
jf.

C++で使用した最適化の1つは、何もしないコンストラクタを作成することです。オブジェクトを作業状態にするには、手動でinit()を呼び出す必要があります。

これは、これらのクラスの大きなベクトルが必要な場合に役立ちます。

私はreserve()を呼び出してベクター用のスペースを割り当てますが、コンストラクターは実際にはオブジェクトが存在するメモリーのページには触れません。そのため、アドレススペースをいくらか使用しましたが、実際には多くの物理メモリを消費していません。関連する建設コストに関連するページフォールトを回避します。

ベクトルを満たすオブジェクトを生成するとき、init()を使用してそれらを設定します。これにより、ページフォールトの合計が制限され、ベクトルを塗りつぶすときにresize()する必要がなくなります。

1
EvilTeach

パフォーマンスのために、最初に保守可能なコード(コンポーネント化、疎結合など)の作成に焦点を当てます。したがって、書き換え、最適化、または単純なプロファイルのいずれかを行うためにパーツを分離する必要がある場合、多くの労力をかけずに実行できます。

オプティマイザーは、プログラムのパフォーマンスをわずかに助けます。

1
Ariel

私は長い間疑っていましたが、要素の数として2のべき乗を保持するように配列を宣言することで、オプティマイザが 強度削減を行うことができることを証明しませんでした 個々の要素を検索するときに、乗算をビット数のシフトで置き換える。

0
EvilTeach

80年代のcobolで私が漠然と覚えていることの1つは、関数が一緒にリンクされる順序を変更できるリンカーオプションがあったことです。これにより、(おそらく)コードの局所性を高めることができました。

同じ考えに沿って。パターンを使用して可能な最適化を達成できるかどうか疑問に思った場合

for (some silly loop)
if (something)
    if (somthing else)
        if (somthing else)
            if (somthing else)
                /* This is the normal expected case */ 
            else error 4
        else error 3
    else error 2
else error 1

Forヘッドとifsはキャッシュブロックに収まる可能性があり、理論的にはループの実行が高速になる可能性があります。

似ている他のものはある程度最適化できると思います。

コメント?私夢見てるの?

0
EvilTeach

ソースファイルの先頭に小さい関数や頻繁に呼び出される関数を配置します。これにより、コンパイラーはインライン化の機会を見つけやすくなります。

0
Mark Ransom