8

コンパイラが実際にどのように最適化するか、およびさまざまなレベルの違い (たとえば、gcc の -O2 と -O3) については、あまり経験がありません。そのため、次の 2 つのステートメントが任意のコンパイラに対して同等であるかどうかはわかりません。

for(i=0;i<10;++i){
variable1*variable2*gridpoint[i];
}

variable3=variable1*variable2;
for(i=0;i<10;++i){
variable3*gridpoint[i];
}

処理時間の観点からは、変数 1 と変数 2 の積はループ内で変化しないため、一度だけ計算するのが理にかなっています。ただし、これには余分なメモリが必要であり、オプティマイザーがこのオーバーヘッドをどの程度考慮に入れているかはわかりません。紙や本からの方程式があり、それをコンピューターで読み取り可能なものに変換したい場合、最初の式が最も読みやすくなります。しかし、2番目が最速かもしれません-特に、ループ内に多くの変更されていない変数があるより複雑な方程式の場合(コードで人間が読めるようにしたいかなり厄介な非線形微分方程式があります)。変数を定数として宣言すると、この変更はありますか? gcc、Intel、およびポートランドのコンパイラの両方を使用しているため、私の質問が任意のコンパイラにとって意味があることを願っています。

4

3 に答える 3

4

任意のコンパイラについて、この質問に適切に答えることは困難です。このコードで何ができるかは、コンパイラだけでなく、ターゲット アーキテクチャにも依存します。優れた機能を備えた製品コンパイラーがこのコードに対してどのようなことができるかを説明しようと思います。

処理時間の観点からは、変数 1 と変数 2 の積はループ内で変化しないため、一度だけ計算するのが理にかなっています。

あなたが正しいです。そしてCatさんが指摘されているように、これを共通部分式の削除と呼んでいます。したがって、コンパイラは、式を 1 回だけ計算するコードを生成する場合があります (または、2 つのオペランドの値が一度に定数であることがわかっている場合は、コンパイル時に計算することさえあります)。

まともなコンパイラは、関数に副作用がないと判断できる場合、関数の部分式の削除も実行できます。たとえば、GCC は、本体が使用可能な場合に関数を分析できますが、この最適化の対象となる関数を明確にマークするために使用できる属性もpureあります (関数属性を参照)。const

副作用がなく、コンパイラがそれを判断できることを考えると(あなたの例では、邪魔になるものは何もありません)、この点で2つのスニペットは同等です(clangで確認しました:-))。

ただし、これには追加のメモリが必要であり、オプティマイザーがこのオーバーヘッドをどの程度考慮に入れているかはわかりません.

実際、これには余分なメモリは必要ありません。乗算はプロセッサレジスタで行われ、結果もレジスタに格納されます。多くのコードを削除し、単一のレジスタを使用して結果を格納するだけで済みます。これは常に素晴らしいことです (特にループでのレジスタ割り当てに関しては、間違いなく作業が楽になります)。したがって、この最適化を実行できる場合は、追加費用なしで実行できます。

最初の式が最も読みやすいです。

GCC と Clang の両方がこの最適化を実行します。ただし、他のコンパイラについてはわかりませんので、自分で確認する必要があります。しかし、部分式の削除を行わない優れたコンパイラを想像するのは困難です。

変数を定数として宣言すると、この変更はありますか?

かもしれません。これは、定数式 (定数のみを含む式) と呼ばれます。定数式は、実行時ではなくコンパイル時に評価できます。したがって、たとえば、A と B の両方が定数である場合に A、B、および C を乗算すると、コンパイラはA*Bその事前計算された値に対して複数の C のみを事前計算します。コンパイラは、コンパイル時にその値を決定でき、それが変更されていないことを確認できる場合、非定数値でもこれを行う場合があります。例えば:

$ cat test.c
inline int foo(int a, int b)
{
    return a * b;
}

int main() {
    int a;
    int b;
    a = 1;
    b = 2;
    return foo(a, b);
}
$ clang -Wall -pedantic -O4 -o test ./test.c
$ otool -tv ./test
./test:
(__TEXT,__text) section
_main:
0000000100000f70    movl    $0x00000002,%eax
0000000100000f75    ret

上記のスニペットの場合に実行できる他の最適化もあります。以下は、頭に浮かぶそれらのいくつかです。

最初の最も明白な例は、ループの展開です。反復回数は実行時にわかっているため、コンパイラはループの展開を決定する場合があります。この最適化が適用されるかどうかは、アーキテクチャによって異なります (つまり、一部の CPU は「ループをロック」し、展開されたバージョンよりも高速にコードを実行できます。これにより、使用するスペースが少なくなるため、余分な µOP フュージョン ステージが回避され、コードがよりキャッシュ フレンドリーになります)。など)。

文字通り 50 倍高速化できる 2 つ目の最適化は、SIMD命令 (SSE、AVX など) を使用することです。たとえば、GCC は非常に優れています (Intel も優れているとは言えませんが)。以下の機能を確認しました。

uint8_t dumb_checksum(const uint8_t *p, size_t size)
{
    uint8_t s = 0;
    size_t i;
    for (i = 0; i < size; ++i)
        s = (uint8_t)(s + p[i]);
    return s;
}

... は、各ステップが一度に 16 個の値を合計する (つまり のように) ループに変換され、_mm_add_epi8アライメントと奇数 (<16) 反復カウントを処理する追加のコードが含まれます。ただし、Clang は、私が最後に確認した時点では完全に失敗していました。そのため、反復回数が不明な場合でも、GCC はループをそのように削減する場合があります。

また、ボトルネックであることが判明しない限り、コードを最適化しないことをお勧めします。そうしないと、誤った時期尚早の最適化を行うために、多くの時間を無駄にする可能性があります。

これがあなたの質問に答えることを願っています。幸運を!

于 2013-01-09T17:47:23.400 に答える
3

はい、ループを介しても、コンパイラが部分式の削除を適切に実行することを期待できます。これにより、メモリ使用量がわずかに増加する可能性がありますが、それはすべて適切なコンパイラによって考慮されます。ほとんどの場合、部分式の削除を実行する方が有利です (ここで話しているメモリはレジスタであり、 L1 キャッシュ)。

これを「証明」するための簡単なテストもいくつかあります。結果は、基本的に、手動の部分式の削除を行うコンパイラーを裏切ろうとするべきではないことを示しています。自然にコーディングして、コンパイラーに得意なことをさせてください (これは、どの式を実際に削除する必要があり、どの式を指定してはならないかを判断するようなものです)。ターゲット アーキテクチャと周囲のコード)。

後で、コードのパフォーマンスに満足できない場合は、コードにプロファイラーを使用して、どのステートメントと式が最も時間を浪費しているかを確認してから、コードを再編成して改善できるかどうかを判断してください。ほとんどの場合、このような単純なことではなく、キャッシュ ストールを減らす (つまり、データをより適切に整理する) こと、冗長な手続き間の計算を排除すること、および次のようなことを行います。それ。

(FTR 次のコードで乱数を使用すると、コンパイラが変数の削除とループの展開に熱心になりすぎないようにするだけです)

プログラム1:

#include <stdlib.h>
#include <time.h>

int main () {
    srandom(time(NULL));
    int i, ret = 0, a = random(), b = random(), values[10];
    int loop_end = random() % 5 + 1000000000;
    for (i=0; i < 10; ++i) { values[i] = random(); }

    for (i = 0; i < loop_end; ++i) {
        ret += a * b * values[i % 10];
    }

    return ret;
}

プログラム2:

#include <stdlib.h>
#include <time.h>

int main () {
    srandom(time(NULL));
    int i, ret = 0, a = random(), b = random(), values[10];
    int loop_end = random() % 5 + 1000000000;
    for (i=0; i < 10; ++i) { values[i] = random(); }

    int c = a * b;
    for (i = 0; i < loop_end; ++i) {
        ret += c * values[i % 10];
    }

    return ret;
}

結果は次のとおりです。

> gcc -O2 prog1.c -o prog1; time ./prog1  
./prog1  1.62s user 0.00s system 99% cpu 1.630 total

> gcc -O2 prog2.c -o prog2; time ./prog2
./prog2  1.63s user 0.00s system 99% cpu 1.636 total

(これは経過時間を測定しているため、0.01 秒の差に注意を払う必要はありません。数回実行すると、両方とも 1.62 ~ 1.63 秒の範囲内にあるため、同じ速度になります)

興味深いことに、最適化を行わずにコンパイルした場合、prog1 の方が高速でした。

> gcc -O0 prog1.c -o prog1; time ./prog1  
./prog1  2.83s user 0.00s system 99% cpu 2.846 total

> gcc -O0 prog2.c -o prog2; time ./prog2 
./prog2  2.93s user 0.00s system 99% cpu 2.946 total

また、興味深いことに、コンパイルすると-O1最高のパフォーマンスが得られました..

gcc -O1 prog1.c -o prog1; time ./prog1 
./prog1  1.57s user 0.00s system 99% cpu 1.579 total

gcc -O1 prog2.c -o prog2; time ./prog2
./prog2  1.56s user 0.00s system 99% cpu 1.563 total

GCC と Intel は優れたコンパイラであり、このような処理については非常にスマートです。私は Portland コンパイラーの経験はありませんが、これらはコンパイラーにとって非常に基本的なことなので、このような状況をうまく処理できなかったら非常に驚くでしょう。

于 2013-01-09T17:18:06.743 に答える
0

私がコンパイラなら、これらのループの両方に左側のオペランドがなく、( に設定する以外に) 副作用がまったくないことを認識しているので、ループを完全に最適化するだけです。i10

これが実際に起こると言っているわけではありません。あなたが提供したコードから発生する可能性があるようです。

于 2013-01-09T17:43:09.727 に答える