gcc 最適化フラグ -O3 は -O2 よりもコードを遅くします 質問する

gcc 最適化フラグ -O3 は -O2 よりもコードを遅くします 質問する

このトピックを見つけたソートされた配列を処理する方がソートされていない配列を処理するよりも速いのはなぜですか?。そして、このコードを実行しようとします。そして、奇妙な動作が見つかります。このコードを-O3最適化フラグ付きでコンパイルすると2.98605 sec、実行に かかります。-O2付きでコンパイルすると かかり1.98093 secます。同じ環境の同じマシンでこのコードを複数回 (5 回または 6 回) 実行し、他のすべてのソフトウェア (Chrome、Skype など) を閉じます。

gcc --version
gcc (Ubuntu 4.9.2-0ubuntu1~14.04) 4.9.2
Copyright (C) 2014 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

それで、なぜこのようなことが起こるのか説明していただけますか? マニュアルを読んで、 が含まれているgccことがわかりました。 ご協力ありがとうございます。-O3-O2

追伸コードを追加

#include <algorithm>
#include <ctime>
#include <iostream>

int main()
{
    // Generate data
    const unsigned arraySize = 32768;
    int data[arraySize];

    for (unsigned c = 0; c < arraySize; ++c)
        data[c] = std::rand() % 256;

    // !!! With this, the next loop runs faster
    std::sort(data, data + arraySize);

    // Test
    clock_t start = clock();
    long long sum = 0;

    for (unsigned i = 0; i < 100000; ++i)
    {
        // Primary loop
        for (unsigned c = 0; c < arraySize; ++c)
        {
            if (data[c] >= 128)
                sum += data[c];
        }
    }

    double elapsedTime = static_cast<double>(clock() - start) / CLOCKS_PER_SEC;

    std::cout << elapsedTime << std::endl;
    std::cout << "sum = " << sum << std::endl;
}

ベストアンサー1

gcc -O3使用シーモブ条件文では、ループで運ばれる依存関係チェーンが長くなり、cmov(Intel Sandybridge CPUでは2つのuopと2サイクルのレイテンシになります。アグナー・フォグの指示表. も参照してくださいタグウィキ)。これはcmov最悪なケースの一つ

データが多少なりとも予測不可能であれば、cmovおそらく勝利となるでしょう。したがって、これはコンパイラにとってかなり賢明な選択です。(ただし、コンパイラは分岐のないコードを多用することがある

Godboltコンパイラエクスプローラーにコードを入れるasm を表示します (適切に強調表示され、無関係な行がフィルタリングされます。ただし、main() に到達するには、すべてのソート コードを下にスクロールする必要があります)。

.L82:  # the inner loop from gcc -O3
    movsx   rcx, DWORD PTR [rdx]  # sign-extending load of data[c]
    mov     rsi, rcx
    add     rcx, rbx        # rcx = sum+data[c]
    cmp     esi, 127
    cmovg   rbx, rcx        # sum = data[c]>127 ? rcx : sum
    add     rdx, 4          # pointer-increment
    cmp     r12, rdx
    jne     .L82

gcc は ADD の代わりに LEA を使用して MOV を保存できたはずです。

ループの 1 回の反復で CMO を使用して rbx が書き込まれ、次の反復で ADD を使用して rbx が読み取られるため、ループは ADD->CMOV (3 サイクル) のレイテンシでボトルネックになります。

ループには 8 つの融合ドメイン uop のみが含まれているため、2 サイクルごとに 1 つの発行が可能です。実行ポートの負荷も、sum依存関係チェーンのレイテンシほどボトルネックにはなりませんが、それに近いものです (Sandybridge には 3 つの ALU ポートしかありませんが、Haswell には 4 つあります)。

sum += (data[c] >= 128 ? data[c] : 0);ところで、ループで運ばれる依存関係チェーンからを取り出すように書くと、cmov潜在的に便利です。まだ命令はたくさんありますが、cmov各反復の は独立しています。これは-O2gcc6.3以前では期待通りにコンパイルされますcmovですが、gcc7 はクリティカルパス上で に最適化を解除します(https://gcc.gnu.org/bugzilla/show_bug.cgi?id=82666if())。(また、この書き方よりも古いバージョンの gcc でも自動ベクトル化されます。)

Clang は、元のソースであっても、cmov をクリティカル パスから外します。


gcc -O2分岐を使用します (gcc5.x 以前)。これは、データがソートされているため、予測が正確です。最近の CPU は分岐予測を使用して制御依存関係を処理するため、ループで運ばれる依存関係チェーンは短くなりますadd(レイテンシは 1 サイクルのみ)。

分岐予測 + 投機的実行により、各反復の比較と分岐は独立しており、分岐方向が確実にわかる前に実行を続行できます。

.L83:   # The inner loop from gcc -O2
    movsx   rcx, DWORD PTR [rdx]  # load with sign-extension from int32 to int64
    cmp     ecx, 127
    jle     .L82        # conditional-jump over the next instruction 
    add     rbp, rcx    # sum+=data[c]
.L82:
    add     rdx, 4
    cmp     rbx, rdx
    jne     .L83

ループによって運ばれる依存関係チェーンが 2 つあります。sumループ カウンターsumは 0 または 1 サイクルの長さで、ループ カウンターは常に 1 サイクルの長さです。ただし、ループは Sandybridge では 5 つの融合ドメイン uop であるため、いずれにしても反復ごとに 1c で実行することはできないため、レイテンシはボトルネックにはなりません。

これはおそらく、2 サイクルあたり約 1 回の反復で実行されます (分岐命令のスループットがボトルネック)。これに対して、-O3 ループでは 3 サイクルあたり 1 回です。次のボトルネックは、ALU uop のスループットです。ALU uop は 4 つ (分岐しない場合) ですが、ALU ポートは 3 つだけです (ADD はどのポートでも実行できます)。

このパイプライン分析の予測は、-O3 の場合は約 3 秒、-O2 の場合は約 2 秒というタイミングとほぼ正確に一致しています。


Haswell/Skylakeは、分岐しない分岐を分岐する分岐と同じサイクルで実行でき、4つのALUポートを備えているため、分岐しないケースを1.25サイクルに1回実行できます。(または、5 uop ループは毎サイクル 4 uop で発行されるわけではない)。

(テストしたところ、Skylake @ 3.9GHz では、プログラム全体のブランチバージョンを 1.45 秒で実行し、ブランチレスバージョンを 1.68 秒で実行しました。そのため、その差ははるかに小さくなります。)


g++6.3.1cmovは でも を使用します-O2が、g++5.4 は依然として 4.9.2 のように動作します。

g++6.3.1 と g++5.4 の両方で、-fprofile-generate/ を使用すると、 ( の場合)-fprofile-useでも分岐バージョンが生成されます。-O3-fno-tree-vectorize

新しい gcc のループの CMOV バージョンでは、 CMP/CMOV の代わりにadd ecx,-128/ を使用しますcmovge rbx,rdx。これは少し奇妙ですが、おそらく速度は低下しません。ADD はフラグだけでなく出力レジスタにも書き込むため、物理レジスタの数にさらに負担がかかります。ただし、それがボトルネックにならない限り、ほぼ同じになるはずです。


新しい gcc は -O3 でループを自動ベクトル化します。これにより、SSE2 だけでも大幅な高速化が実現します。(たとえば、私の i7-6700k Skylake ではベクトル化されたバージョンを 0.74 秒で実行します。つまり、スカラーの約 2 倍の速度です。または-O3 -march=native、AVX2 256b ベクトルを使用すると 0.35 秒です)。

ベクトル化されたバージョンは命令がたくさんあるように見えますが、それほど悪くはなく、そのほとんどはループで運ばれる依存関係チェーンの一部ではありません。最後のほうで 64 ビット要素に展開するだけで済みます。pcmpgtdただし、条件によってすべての負の整数がすでにゼロになっている場合、符号拡張ではなくゼロ拡張だけで済むことに気付かないため、2 回実行されます。

おすすめ記事