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


116

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

時間の経過とともに、コンパイラは成熟しました。フロー分析により、レジスタにどの値を保持するかを、ユーザーが実行できるよりも適切に決定できるという点で、非常に賢くなりました。registerキーワードは重要ではなくなりました。

FORTRANは、エイリアスの問題により、ある種の操作ではCよりも高速になる場合があります。理論的には、注意深くコーディングすれば、オプティマイザがより高速なコードを生成できるように、この制限を回避できます。

コンパイラ/オプティマイザがより高速なコードを生成できるようにするために、どのようなコーディングプラクティスを利用できますか?

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

ここに関連する質問があります

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

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


7
この(興味深い)質問に対する「単一」の明確な回答がないため、コミュニティWiki imhoの良い候補になる可能性があります...
ChristopheD

いつも会いたいです。ご指摘ありがとうございます。
EvilTeach 2010年

「より良い」とは、単に「より速い」という意味ですか、それとも他の卓越性の基準を念頭に置いていますか?
ハイパフォーマンスマーク

1
特に移植性の高い優れたレジスタアロケータを作成するのはかなり難しく、レジスタの割り当てはパフォーマンスとコードサイズにとって絶対に不可欠です。register貧弱なコンパイラと闘うことで、パフォーマンスに敏感なコードを実際に移植しやすくしました。
Potatoswatter 2010年

1
@EvilTeach:コミュニティーWikiは「決定的な答えがない」という意味ではなく、主観的なタグと同義ではありません。コミュニティウィキとは、投稿をコミュニティに投下して、他の人が編集できるようにすることです。気に入らなければ、質問をwikiするようにプレッシャーを感じないでください。
ジュリエット

回答:


54

出力引数ではなくローカル変数に書き込みます!これは、エイリアシングの速度低下を回避するのに非常に役立ちます。たとえば、コードが次のようになっている場合

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;
}

ばかげているように聞こえますが、メモリ内で引数と重複する可能性がないため、コンパイラはローカル変数の処理をはるかに賢くすることができます。これは、恐ろしいロードヒットストア(このスレッドでFrancis Boivinが言及)を回避するのに役立ちます。


7
これには、プログラマーにとっても明らかでない副作用の可能性について心配する必要がないため、プログラマにとっても読みやすく/理解しやすくなるという追加の利点があります。
Michael Burr、2010年

ほとんどのIDEはデフォルトでローカル変数を表示するため、入力が少なくなります
EvilTeach

9
制限付きポインターを使用して、その最適化を有効にすることもできます
Ben Voigt

4
@ベン-それは本当ですが、私はこの方法がより明確だと思います。また、入力と出力がオーバーラップした場合、制限されたポインターでは結果が特定されていないと思います(おそらくデバッグとリリースで異なる動作が発生します)が、この方法は少なくとも一貫しています。誤解しないでください。制限を使用するのが好きですが、それ以上必要ないのが好きです。
セリオン

Fooに、数メガのデータをコピーするコピー操作が定義されていないことを期待しているだけです;-)
Skizz

76

以下は、コンパイラーが高速なコード(あらゆる言語、あらゆるプラットフォーム、あらゆるコンパイラー、あらゆる問題)を作成できるようにするためのコーディング方法です。

コンパイラがメモリ(キャッシュとレジスタを含む)に変数を配置することを強制または推奨する巧妙なトリックを使用しないでください。まず、正確で保守可能なプログラムを作成します。

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

次に、そのときだけ、コンパイラーにメモリーの使用方法を伝えることの影響を調査することをお勧めします。一度に1つの変更を行い、その影響を測定します。

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


20
Compiierの開発者は、他の人と同じように有限の時間を持っています。すべての最適化がコンパイラーに組み込まれるわけではありません。同様に&%2のべき乗のための(ほとんど、これまでならば、最適化されませんが、パフォーマンスが大幅に影響を与えることができます)。パフォーマンスのトリックを読んだ場合、それが機能するかどうかを知る唯一の方法は、変更を加えて影響を測定することです。コンパイラが何かを最適化するとは決して想定しないでください。
Dave Jarvis

22
&と%は、他のほとんどの安価な算術トリックとともに、ほとんど常に最適化されています。最適化されないのは、右側のオペランドが常に2のべき乗である変数である場合です。
Potatoswatter 2010年

8
明確にするために、私は一部の読者を混乱させたようです。私が提案するコーディングの実践におけるアドバイスは、パフォーマンスのベースラインを確立するためにメモリレイアウト命令を利用しない簡単なコードを最初に開発することです。次に、物事を1つずつ試して、その影響を測定します。運用実績についてのアドバイスはありません。
ハイパフォーマンスマーク

17
一定の2の累乗の場合n、gccは、最適化が無効になっている場合でも置き換え% nられ& (n-1) ます。それは正確には「めったに、滅多にない」というわけではありません...
Porculus

12
負の整数除算に関するCのばかげた規則により、%は&として符号化されたときに&として最適化できません(切り捨てて常に正の余りを持つのではなく、0に向かって丸め、負の余りを持つ)。そして、ほとんどの場合、無知なコーダーは符号付きの型を使用します...
R .. GitHub ICE HELPING ICE STOP 10/07/21

47

メモリをトラバースする順序はパフォーマンスに大きな影響を与える可能性があり、コンパイラはそれを理解して修正するのがあまり得意ではありません。パフォーマンスを重視する場合は、コードを作成するときにキャッシュの局所性の問題に注意する必要があります。たとえば、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];
  }
}

厳密に言えば、これはオプティマイザの問題ではなく、最適化の問題です。
EvilTeach 2010年

10
もちろんオプティマイザの問題です。人々は何十年もの間、自動ループ交換最適化に関する論文を書いてきました。
Phil Miller

20
@Potatoswatter何のことをいっているのですか。Cコンパイラーは、同じ最終結果が観察される限り、やりたいことは何でもできます。実際、GCC 4.4は-floop-interchange、オプティマイザーが有益であると判断した場合に内部ループと外部ループを反転させます。
エフェミエント

2
ええと、そこに行きます。Cセマンティクスは、エイリアスの問題によって損なわれることがよくあります。ここでの本当のアドバイスは、そのフラグを渡すことだと思います!
Potatoswatter、2010年

36

一般的な最適化

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

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

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

不要な冗長コードを削除する

コードが使用されていないか、プログラムの結果に貢献していない場合は、コードを削除してください。

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

かつて、プログラムが計算していた代数方程式を書き留めることにより、プログラムから多くのアセンブリコードと実行時間を削除し、代数式を簡略化しました。単純化された代数表現の実装は、元の関数よりも場所と時間が少なくて済みました。

ループ展開

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

編集: ループ展開の例を提供する前:

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。-TM

の削減 ifステートメントの

プロセッサは、命令のキューを再ロードする必要があるため、分岐またはジャンプを嫌います。

ブール演算(編集: コードフラグメントにコード形式を適用、例を追加)

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

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

短絡論理AND演算子は、(&&あれば)テストの実行を防ぐことstatusですfalse

例:

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点のリード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);

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

家族を使用しないprintf 定数データにを

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

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

char複数を使用して配列にフォーマットしてからsprintf、を使用しますfwrite。これにより、データレイアウトを「定数セクション」と可変セクションに分割することもできます。差し込み印刷について考える

定数テキスト(文字列リテラル)を次のように宣言する static const

なしで変数を宣言するとstatic、一部のコンパイラはスタックにスペースを割り当て、ROMからデータをコピーする場合があります。これらは2つの不要な操作です。これは、static接頭辞。

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

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


2
興味深いのは、より大きなコードではなく、いくつかの小さなステートメントでより良いコードを得た例を提供できますか。ブール値を使用して、ifを書き換える例を示してください。おそらく、キャッシュサイズの方が良いと思われるため、ループはコンパイラに展開しておきます。私はスプリントフィッシング、それからフライティングのアイデアに少し驚いています。私はfprintfが実際にそれを内部で行うと思います。ここでもう少し詳しく教えてもらえますか?
EvilTeach 2010年

1
fprintf別のバッファーにフォーマットしてからバッファーを出力するという保証はありません。合理化された(メモリの使用のため)fprintfは、すべての未フォーマットのテキストを出力し、次にフォーマットして出力し、フォーマット文字列全体が処理されるまで繰り返します。これにより、各タイプの出力(フォーマット済みと未フォーマット)に対して1つの出力呼び出しが行われます。他の実装では、新しい文字列全体を保持するために、呼び出しごとにメモリを動的に割り当てる必要があります(これは組み込みシステム環境では問題です)。私の提案は、出力の数を減らします。
Thomas Matthews

3
ループをロールアップすることで、パフォーマンスが大幅に向上したことがあります。次に、いくつかの間接参照を使用してそれをより厳密にロールアップする方法を見つけ、プログラムが著しく速くなりました。(プロファイリングでは、この特定の関数がランタイムの60〜80%であることを示し、パフォーマンスを前後に注意深くテストしました。)改善は局所性の向上によるものだと思いますが、それについては完全にはわかりません。
David Thornley、2010

16
これらの多くは、プログラマーがコンパイラーの最適化を支援する方法ではなく、プログラマーの最適化であり、これが元の質問の主旨でした。たとえば、ループの展開。はい、あなたはあなた自身の展開を行うことができますが、コンパイラーがあなたのために展開を解除し、それらを削除するのにどんな障害があるかを理解することはより興味深いと思います。
エイドリアン・マッカーシー、

26

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

とはいえ、あるファイルから別のファイルの小さな関数を内部ループすると、インライン化されなくなるため、内部ループしないでください。

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

これが次のポイントにつながります。^#$ @マニュアルを読んでください!GCCは、__restrict__あちこちに散りばめれば、プレーンなCコードをベクトル化できます__attribute__( __aligned__ )。オプティマイザから非常に具体的なものが必要な場合は、具体的にする必要があります。


14
これは良い答えですが、プログラム全体の最適化が一般的になりつつあり、実際には翻訳単位全体で関数をインライン化できることに注意してください。
Phil Miller

1
@Novelocratはい、言うまでもなく、A.cインライン化されたものから初めて何かを見たとき、私は非常に驚きましたB.c
Jonathon Reinhart

18

最近のほとんどのプロセッサでは、最大のボトルネックはメモリです。

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

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

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


4
ロードヒットストアとさまざまなレジスタタイプの場合は+1。x86での取引の規模がどれほどかはわかりませんが、PowerPC(Xbox360やPlaystation3など)に投資しているのです。
セリオン2010年

コンパイラループ最適化手法に関するほとんどの論文では、完全な入れ子を前提としています。つまり、最も内側を除く各ループの本体は、別のループにすぎません。これらのペーパーは、それが可能であることが非常に明確であるとしても、そのようなものを一般化するために必要な手順については説明していません。したがって、余計な手間がかかるため、多くの実装では実際にはそれらの一般化をサポートしないと思います。したがって、ループでのキャッシュの使用を最適化するための多くのアルゴリズムは、不完全なネストよりも完全なネストではるかにうまく機能する可能性があります。
Phil Miller

11

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

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


7
私は奇抜であることを告白します-私は、メモリ帯域幅が制限されている大きな科学的数値処理コードに取り組んでいます。プログラムの一般的な人々にとって、私はニールに同意します。
ハイパフォーマンスマーク

6
真; しかし、最近のI / Oにバインドされたコードの非常に多くは、実際にはペシマイザーである言語で作成されています。コンパイラーさえもない言語です。CとC ++がまだ使用されている領域は、何かを最適化することがより重要な領域になる傾向があると
思い

3
過去30年間のほとんどを、I / Oがほとんどないコードの開発に費やしてきました。データベースを使用して2年間保存します。グラフィックス、制御システム、シミュレーション-I / O制限なし。I / Oがほとんどの人のボトルネックである場合、インテルとAMDにそれほど注意を払うことはありません。
phkahler、

2
ええ、私はこの引数を実際に購入するわけではありません。そうでなければ、私(私の仕事)は、I / Oを行うためにコンピューティング時間をより多く費やす方法を探していません。また、私が遭遇したI / Oバウンドソフトウェアの多くは、I / Oが不安定に行われたため、I / Oバウンドになっています。(メモリと同様に)アクセスパターンを最適化すると、パフォーマンスが大幅に向上します。
ダッシュトムバン

3
最近、C ++言語で記述されたコードのほとんどがI / Oバウンドではないことを発見しました。確かに、ディスクの一括転送のためにOS関数を呼び出している場合、スレッドはI / O待機状態になる可能性があります(ただし、キャッシングを使用すると、問題があります)。しかし、通常のI / Oライブラリ関数は、標準的で移植性があるため、誰もが推奨するものですが、実際には最近のディスクテクノロジー(手頃な価格のものでも)に比べて非常に遅いです。ほとんどの場合、I / Oがボトルネックになるのは、ほんの数バイトを書き込んだ後でディスクに完全にフラッシュする場合だけです。OTOH、UIは別の問題です。人間は遅いです。
Ben Voigt 2010

11

コードではできるだけconstの正確さを使用してください。これにより、コンパイラーはより適切に最適化できます。

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

ハイライト:

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

8
それほどではありませんが、まれです。ただし、実際の正確性は向上します。
Potatoswatter 2010年

5
CおよびC ++では、キャストして明確に定義された動作であるため、コンパイラーはconstを使用して最適化できません。
dsimcha '15年

+1:constは、コンパイルされたコードに直接影響を与える良い例です。@dsimchaのコメント-優れたコンパイラーは、これが発生するかどうかをテストします。もちろん、優れたコンパイラーは、そのように宣言されていないconst要素を「検索」します...
Hogan

@dsimcha:変更const 及び restrict修飾ポインタが、しかし、定義されていません。そのため、このような場合、コンパイラーは異なる方法で最適化できます。
ディートリッヒエップ2011年

6
@dsimcha離れ鋳造constconst参照またはconst非に対するポインタconstオブジェクトは、明確に定義されています。実際のconstオブジェクト(つまり、const最初に宣言されたオブジェクト)を変更することはできません。
スティーブンリン

9

可能な限り静的な単一割り当てを使用してプログラミングを試みます。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つしかない場合は、そのロジックを逆にすることができます。

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

できるときに関数をstaticとして宣言することは常に良い考えです。コンパイラーが特定の関数のすべての呼び出し元を考慮したことをコンパイラーが証明できる場合、最適化の名前でその関数の呼び出し規則を破ることができます。コンパイラーは、パラメーターをレジスターまたはスタック位置に移動することを回避できることがよくあります。これは、呼び出された関数が通常パラメーターを期待する位置にあります(これを行うには、呼び出された関数とすべての呼び出し元の場所の両方で逸脱する必要があります)。コンパイラーは、呼び出された関数が必要とするメモリーとレジスターを把握し、呼び出された関数が妨害しないレジスターまたはメモリーの場所にある変数値を保持するコードの生成を回避することもできます。これは、関数の呼び出しが少ない場合に特に効果的です。


2
範囲、LLVM、GCC、および私のコンパイラーをテストするときに減算を使用する必要は実際にはありません。少なくともこれは自動的に行われます。減算を伴うコードが何をするかを理解している人はほとんどいないでしょう。
Gratian Lup、2012

上記の例では、(x <0)の場合、a()が呼び出されるため、b()を呼び出すことはできません。
EvilTeach

@EvilTeachいいえ、ありません。結果としてa()が呼び出される比較は!x
nategoose

@nategoose。xが-3の場合、!xはtrueです。
EvilTeach 2018年

@EvilTeach In C 0は偽であり、その他はすべて真なので、-3は真、!-3は偽
nategoose

9

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

  1. ほとんどの関数を静的にします。これにより、プロシージャ間の定数伝播とエイリアス分析が機能します。そうでない場合、コンパイラーは、パラメーターの完全に不明な値を使用して、関数が変換単位の外部から呼び出されると想定する必要があります。よく知られているオープンソースライブラリを見ると、それらはすべて、externである必要があるものを除いて、すべて静的な関数としてマークされています。

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

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

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

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

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

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

  8. スイッチの使用は、if(a || b || ... || z)のようなテストを行うよりも高速です。あなたのコンパイラが自動的にこれをしなければ、一部はやる、それが持っているより読みだ最初のチェック場合けれども。


7

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

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


私の経験則では、動的に割り当てる必要がある場合は、配列を取得して、再度割り当てる必要がないようにします。ベクトルを事前に割り当てます。
EvilTeach

7

ばかげた小さなヒントですが、微妙な速度とコードの節約になります。

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

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

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

逆アセンブルされたコードを読んでいるとき、人々がこのルールに従わなかったために、とんでもないレジスターのシャッフルを見たことがあります。


2
CもC ++も、特定のレジスターやスタックの場所を渡すことについて保証も言及もしていません。パラメータの受け渡しの詳細を決定するのは、ABI(例:Linux ELF)です。
エメット14

5

上記のリストには載っていなかった2つのコーディング技術:

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

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

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

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

この手法は、プログラムを最初から有効にするように作成すれば十分簡単ですが、Cセマンティックスの一部が変更され、静的変数やマクロの衝突などの問題に対処できることにも注意する必要があります。ほとんどのプログラムでは、発生する小さな問題を簡単に解決できます。また、固有のソースとしてコンパイルすると処理速度が遅くなり、大量のメモリを消費する可能性があることにも注意してください(通常、最新のシステムでは問題ありません)。

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

registerキーワードと同様に、このトリックもすぐに廃止される可能性があります。リンカーによる最適化がコンパイラーgccでサポートされるようになりました:リンク時の最適化。。

ループ内のアトミックタスクを分離する

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

レジスタを使用する場合と同様に、この方法(このトリックを使用しているかどうかのプロファイルパフォーマンス)には注意してください。パフォーマンスが向上した場合よりもパフォーマンスが低下する可能性があります。


2
はい、現時点で、LTOはこの投稿の前半を冗長にし、おそらく悪いアドバイスにしています。
underscore_d

@underscore_d:まだいくつかの問題(主にエクスポートされたシンボルの可視性に関連)がありますが、単なるパフォーマンスの観点からはおそらくもう何もありません。
クリス

4

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


5
ソース内で物理的に近接して一緒に使用される関数を配置すると、それらがオブジェクトファイルでは互いに近くなり、実行可能ファイルでは互いに近くなる可能性が高くなります。この改善された命令の局所性は、実行中の命令キャッシュミスの回避に役立ちます。
paxos1977 2010年

AIXコンパイラーには、その動作を促進するコンパイラー・スイッチがあります-qipa [= <suboptions_list>] | -qnoipaプロシージャー間分析(IPA)と呼ばれる最適化のクラスをオンまたはカスタマイズします。
EvilTeach 2010年

4
最善の方法は、これを必要としない開発方法を持つことです。この事実をモジュール化されていないコードを書くための言い訳として使用すると、全体としてコードが遅くなり、メンテナンスの問題が発生します。
ホーガン

3
この情報は少し古いと思います。理論的には、現在多くのコンパイラに組み込まれているプログラム全体の最適化機能(gccの「リンク時最適化」など)は同じ利点を可能にしますが、完全に標準的なワークフロー(さらに、すべてを1つのファイルに入れるよりも高速な再コンパイル時間)を備えています。 !)
Ponkadoodle 2014

@Wallacoloo確かに、これは日付のfaaarです。FWIW、私は今日、GCCのLTOを初めて使用しただけで、他の点では同じですが-O3、プログラムから元のサイズの22%を爆発させました。(CPUバウンドではないので、速度についてはあまり言及していません。)
underscore_d

4

関数呼び出しを最適化できるため、最近のほとんどのコンパイラーは、末尾再帰を高速化する優れた機能を実行する必要があります。

例:

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を使用する要件は、末尾再帰によって最適化できるように特別に設計されていることは明らかです。


1
質問はCに関するものです。Cは末尾再帰を削除しないため、末尾またはその他の再帰では、再帰が深くなりすぎるとスタックが爆破する可能性があります。
ヒキガエル

1
gotoを使用して、呼び出し規約の問題を回避しました。その方法ではオーバーヘッドが少なくなります。
EvilTeach 2010年

2
@hogan:これは私にとって新しいものです。これを行うコンパイラを指摘していただけますか?そして、それが実際に最適化されていることをどのように確認できますか?これを実行する場合、実際に実行する必要があります。これは、コンパイラオプティマイザが期待するものではありません(機能するかどうかわからないインライン化など)
Toad

6
@hogan:私は正しい立場だ。GccとMSVCの両方が末尾再帰の最適化を行うことは正しいです。
ヒキガエル

5
この例は最後の再帰呼び出しではなく、その乗算であるため、末尾再帰ではありません。
ブライアンヤング

4

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

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

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();
}

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


3
  1. すべての変数宣言で可能な限りローカルなスコープを使用します。

  2. 使用する const可能な限りする

  3. いけない使用レジスタあなたが、それなしで両方のプロファイルする予定がない限り

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

registerキーワードを盲目的に使用することは、最適化を損なうのと同じくらい役立つ可能性があります。アセンブリの出力またはプロファイルを見るまで、何が重要であるかを知るのは非常に難しいです。

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



3

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

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

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

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


3

この回答に関する@MSaltersのコメントから私が学んだきちんとした手法により、コンパイラーは、いくつかの条件に従って異なるオブジェクトを返す場合でも、コピーの省略を行うことができます。

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

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

2

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

明示的なスタックを使用して非再帰的な方法で再帰関数を再実装することも多くの利益をもたらす可能性がありますが、その場合、開発時間と利益の領域にいることになります。


再帰をスタックに変換することは、レイトレーサーを開発し、他のレンダリングアルゴリズムを作成する人々にとって、ompf.orgで想定される最適化です。
トム

これに加えて、私の個人的なレイトレーサープロジェクトの最大のオーバーヘッドは、コンポジットパターンを使用したバウンディングボリューム階層のvtableベースの再帰です。実際には、ツリーとして構造化された入れ子になったボックスの集まりにすぎませんが、パターンを使用すると、データの膨張(仮想テーブルポインター)が発生し、命令の一貫性が低下します(小さい/タイトなループが関数呼び出しのチェーンになります)
トム

2

これが私の2番目の最適化アドバイスです。私の最初のアドバイスと同様に、これは汎用であり、言語やプロセッサ固有ではありません。

コンパイラーのマニュアルをよく読み、内容を理解してください。最大限にコンパイラを使用してください。

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

はい、コンパイラの作者はコーディングの巨人の種族からのものではなく、コンパイラには間違いがあり、マニュアルとコンパイラの理論によれば、物事をより速くすることが時々遅くするべきです。そのため、一度に1つのステップを実行し、調整前と調整後のパフォーマンスを測定する必要があります。

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

よろしく

マーク

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


2

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


x86-64ビットでは、レジスターで渡される多くのパラメーターも許可されます。これは、関数呼び出しのオーバーヘッドに劇的な影響を与える可能性があります。
トム

1

パフォーマンスについては、最初に保守可能なコード(コンポーネント化、疎結合など)の記述に焦点を当てます。そのため、再書き込み、最適化、または単にプロファイルするためにパーツを分離する必要がある場合、多くの労力なしで実行できます。

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


3
これは、結合の「インターフェース」自体が最適化の影響を受けやすい場合にのみ機能します。たとえば、冗長な検索や計算を強制したり、不正なキャッシュアクセスを強制したりすることにより、インターフェイスは本質的に「低速」になる可能性があります。
トム

1

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

プログラムが正しく記述され、完全に最適化されてコンパイルされ、テストされ、本番環境に導入されていると想定します。

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

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

それが行われた後、(ホットスポットの)マイクロ最適化はあなたに良い見返りを与えることができます。


1

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

多かれ少なかれ、私はコードをプロファイルします。次に、ホットスポットにぶら下がって、コンパイラーがより適切に機能できるようにコードを変更しようとします。

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

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


質問への回答に近づきつつあります...コンパイラーがそのような種類の最適化を実行できるようにするために、コードに対してどのようなことをしますか?
EvilTeach 2010年

1
Cスタイル(C ++ではなく)などでより多くの書き込みをしようとします。特に、絶対に必要とされない仮想関数を回避します。特に、頻繁に呼び出される場合は、AddRefsを避けます。インライン化が容易なコードを記述します。パラメーターを減らし、 "if" -sを減らします。絶対的な必要がない限り、グローバル変数を使用しないでください。データ構造では、幅の広いフィールドを最初に配置します(double、int64はintの前に移動します)。したがって、コンパイラーは最初のフィールドの自然なサイズで構造体を整列します。perfに適しています。
jf。

1
データのレイアウトとアクセスは、パフォーマンスにとって非常に重要です。したがって、プロファイリング後に、アクセスの局所性に従って構造をいくつかの構造に分割することがあります。もう1つの一般的なトリック-intまたはsize-t対charを使用します-データ値が小さい場合でも-さまざまなパフォーマンスを回避します。ロードブロッキングへのペナルティストア、部分的なレジスターストールの問題。もちろん、これはそのようなデータの大きな配列が必要な場合には適用されません。
jf。

もう1つ-本当に必要な場合を除いて、システムコールを回避します:)-それらは非常に高価です
jf。

2
@jf:私はあなたの回答に+1しましたが、回答をコメントから回答本文に移動してもらえますか?読みやすくなります。
クリス

1

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

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

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

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


6
std :: vectorの典型的な実装は、より多くの容量を予約()しても実際にはより多くのオブジェクトを構築しないと思います。ページを割り当てるだけです。コンストラクターは、オブジェクトをベクターに実際に追加するときに、配置newを使用して後で呼び出されます-これは(おそらく)init()を呼び出す直前なので、個別のinit()関数は実際には必要ありません。また、ソースコードでコンストラクターが "空"であっても、コンパイルされたコンストラクターには仮想テーブルやRTTIなどを初期化するコードが含まれている可能性があるため、構築時にページが操作されることにも注意してください。
Wizard、

1
うん。この例では、push_backを使用してベクターを配置します。オブジェクトには仮想関数がないため、問題ありません。コンストラクターで初めて試したとき、ページフォールトの量に驚かされました。私は何が起こったのかを理解し、コンストラクターの根性を取り除き、ページフォールトの問題を解消しました。
EvilTeach 2010年

それはむしろ私を驚かせます。どのC ++およびSTL実装を使用しましたか?
David Thornley、2010

3
他の人にも同意します。これはstd :: vectorの不適切な実装のように聞こえます。オブジェクトにvtableがあったとしても、それらはpush_backまで構築されません。デフォルトのコンストラクターをプライベートとして宣言することで、これをテストできるはずです。これは、すべてのベクトルに必要なのがpush_backのコピーコンストラクターであるためです。
トム

1
@David-実装はAIXで行われました。
EvilTeach 2010

1

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

全体的なパフォーマンスを改善する必要があった前回は、次善のアルゴリズムに目を光らせ、キャッシュに問題があると思われる場所を探しました。最初にパフォーマンスをプロファイルして測定し、変更のたびにもう一度測定しました。その後会社は崩壊しましたが、それはとにかく面白くて有益な仕事でした。


0

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


6
それはかつて真実でしたが、今日ではもうそうです。実際、正反対のことが当てはまります。2の累乗で配列を宣言すると、メモリ内で2の累乗離れた2つのポインタを操作する状況に遭遇する可能性が非常に高くなります。問題は、CPUキャッシュがそのように構成されており、2つのアレイが1つのキャッシュラインを巡って戦うことになるということです。あなたはそのように恐ろしいパフォーマンスを手に入れます。ポインターの1つを数バイト先(たとえば、2の累乗ではない)にすると、この状況を回避できます。
Nils Pipenbrinck

+1ニル。これが1つ発生するのは、Intelハードウェアでの「64kエイリアシング」です。
トム

ちなみにこれは分解を見て簡単に反証されるものです。数年前、gccがシフトと加算を使用してあらゆる種類の定数乗算をどのように最適化するかを見て、私は驚きました。たとえば、val * 7それ以外の場合は次のようになり(val << 3) - valます。
ダッシュトムバン

0

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


本当に?その理由と例を挙げていただけますか?それが真実ではないと言っているのではなく、場所が重要であると直感的に聞こえないだけです。
underscore_d

@underscore_d関数の定義がわかるまで、何かをインライン化することはできません。最近のコンパイラーは、コード生成時に定義がわかるように複数のパスを作成する場合がありますが、私はそれを想定していません。
Mark Ransom

コンパイラーは、物理的な関数の順序ではなく、抽象的な呼び出しグラフで機能すると想定していました。つまり、これは問題ではありません。確かに、私はそれが特別な注意を払うことに害を及ぼさないと思います-特に、パフォーマンスは別として、IMOは、それらを呼び出す関数の前に呼び出される関数を定義する方がより論理的に思えます。パフォーマンスをテストする必要がありますが、それが重要な場合は驚かされますが、それまでは、驚かされます。
underscore_d
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.