8

私は最近、効率が非常に重要な研究プロジェクトのコードを書いています。私は、私が行っている通常の方法のいくつかをスクレイピングし、代わりにビット単位の XOR を使用することを検討しています。私が疑問に思っているのは、これが違いを生むかどうか (この操作を数百万回実行している場合)、または g++ で 03 を使用した後も同じかどうかです。

頭に浮かぶ2つの例:

(私は純粋に正の整数を使用しています) n が奇数の場合は n を n-1 に、n が偶数の場合は n を (n+1) に変更する必要がありました。私はいくつかのオプションがあると考えました:

if(n%2) // or (n%2==0) and flip the order
    n=n-1
else
    n=n+1

また

n=n+2*n%2-1; //This of course was silly, but was the only non-bitwise 1 line I could come up with

ついに:

n=n^1;

すべての方法は明らかに同じことを行いますが、3 番目の方法が最も効率的であると感じました。

次の例は、より一般的な注意事項です。2 つの正の整数を比較しているとします。これらの 1 つが他のものよりも優れたパフォーマンスを発揮します。または、この操作を数百万回実行しても、違いは本当に目立たないでしょうか。

if(n_1==n_2)
if(! (n_1 ^ n_2) )
if( n_1 ^ n_2) else \do work here

コンパイラは、これらすべてのインスタンスで同じ操作を実行しますか? ビット単位の操作を使用し、コンパイラが私のために作業を行うことを信頼しないインスタンスがあるかどうか、私はただ興味があります。

修正済み:問題の正しい記述。

4

8 に答える 8

9

チェックするのは簡単です。逆アセンブラを起動するだけです。見てみましょう:

FC:

unsigned int f1(unsigned int n)
{
  n ^= 1;  
  return n;
}

unsigned int f2(unsigned int n)
{
  if (n % 2)
    n=n-1;
  else
    n=n+1;

  return n;
}

ビルドと逆アセンブル:

$ cc -O3 -c f.c 
$ otool -tV f.o 
f.o:
(__TEXT,__text) section
_f1:
00  pushq   %rbp
01  movq    %rsp,%rbp
04  xorl    $0x01,%edi
07  movl    %edi,%eax
09  leave
0a  ret
0b  nopl    _f1(%rax,%rax)
_f2:
10  pushq   %rbp
11  movq    %rsp,%rbp
14  leal    0xff(%rdi),%eax
17  leal    0x01(%rdi),%edx
1a  andl    $0x01,%edi
1d  cmovel  %edx,%eax
20  leave
21  ret

少し短いように見えますf1()が、実際に問題になるかどうかはベンチマーク次第です。

于 2010-02-22T01:45:46.230 に答える
4

私はここでのほとんどの回答にちょっと同意しません.

XOR は、事実上、CPU が実行できる最速の操作であり、良い点は、すべての CPU が XOR をサポートしていることです。この理由は非常に簡単です。XOR ゲートは、4 つの NAND ゲートまたは 5 つの NOR ゲートだけで作成できます。つまり、シリコンのファブリックを使用して簡単に作成できるということです。当然のことながら、私が知っているすべての CPU は、XOR 演算を 1 クロック ティック (またはそれ以下) で実行できます。

配列内の複数の項目で XOR を実行する必要がある場合、最新の x64 CPU は f.ex のように一度に複数の項目で XOR をサポートします。Intel の SIMD 命令。

選択した代替ソリューションは、if-then-else を使用します。確かに、ほとんどのコンパイラはこの簡単なことを理解することができます...しかし、なぜ危険を冒す必要があり、結果はどうなるでしょうか?

コンパイラがそれを理解しない結果は、分岐予測エラーです。分岐予測が 1 回失敗すると、簡単に 17 クロック ティックかかります。プロセッサ命令の実行速度を一目見れば、特にランダム データを処理する場合、分岐がパフォーマンスに非常に悪いことがわかります。

これは、テストを正しく構成しないと、データによってパフォーマンス測定が台無しになることも意味することに注意してください。

結論として、まず考え、次にプログラムし、次にプロファイルします。その逆ではありません。そして、XOR を使用します。

于 2013-12-16T14:19:55.657 に答える
4

n が偶数の場合は n を n-1 に、奇数の場合は n を (n+1) に変更する必要がありました。

その場合、効率に関係なく、n = n ^ 1間違っています。

2番目のケースで==は、他のどのケースよりも効率的です(それ以上ではないにしても)。


一般に、最適化に関しては、自分でベンチマークする必要があります。潜在的な最適化がベンチマークに値しない場合、それは実際に実行する価値がありません。

于 2010-02-22T01:34:31.120 に答える
2

確実に知る唯一の方法は、テストすることです。次の出力を効率的に生成するには、かなり賢いコンパイラが必要であることに同意する必要があります。

if(n%2) // or (n%2==0) and flip the order
    n=n-1
else
    n=n+1

の場合とn ^= 1;同様ですが、最近、確実に言えるほど類似したものを確認していません。

2 番目の質問については、違いがあるとは思えません。これらの方法のいずれについても、等値比較は高速になります。速度が必要な場合、主に行うべきことは、ブランチがまったく関与しないようにすることです。たとえば、次のようなものです。

if (a == b)
    c += d;

次のように記述できます c += d * (a==b);。アセンブリ言語を見ると、2 番目の言語は少し面倒に見えることがよくあります (フラグから通常のレジスタへの比較の結果を取得するための見苦しい) が、分岐を回避することでパフォーマンスが向上することがよくあります。

編集: 少なくとも私が手元にあるコンパイラ (gcc & MSVC) は、 の を生成しませんが、cmovの をif生成しseteます* (a==b)。コードをテスト可能なものに拡張しました。

Edit2: Potatoswatter は、乗算の代わりにビットごとの AND を使用して別の可能性をもたらしたので、他のものと一緒にそれをテストすることにしました。これを追加したコードは次のとおりです。

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

int addif1(int a, int b, int c, int d) { 
    if (a==b) 
        c+=d;
    return c;
}

int addif2(int a, int b, int c, int d) {
    return c += d * (a == b);
}

int addif3(int a, int b, int c, int d) {
    return c += d & -(a == b);
}

int main() {
    const int iterations = 50000;
    int x = rand();
    unsigned tot1 = 0;
    unsigned tot2 = 0;
    unsigned tot3 = 0;

    clock_t start1 = clock();
    for (int i=0; i<iterations; i++) {
        for (int j=0; j<iterations; j++) 
            tot1 +=addif1(i, j, i, x);
    }
    clock_t stop1 = clock();

    clock_t start2 = clock();
    for (int i=0; i<iterations; i++) {
        for (int j=0; j<iterations; j++) 
            tot2 +=addif2(i, j, i, x);
    }
    clock_t stop2 = clock();

    clock_t start3 = clock();
    for (int i=0; i<iterations; i++) {
        for (int j=0; j<iterations; j++) 
            tot3 +=addif3(i, j, i, x);
    }
    clock_t stop3 = clock();

    std::cout << "Ignore: " << tot1 << "\n";
    std::cout << "Ignore: " << tot2 << "\n";
    std::cout << "Ignore: " << tot3 << "\n";

    std::cout << "addif1: " << stop1-start1 << "\n";
    std::cout << "addif2: " << stop2-start2 << "\n";
    std::cout << "addif3: " << stop3-start3 << "\n";    
    return 0;
}

次に、非常に興味深い部分です。3 番目のバージョンの結果は非常に興味深いものです。MS VC++ の場合、ほとんどの人がおそらく期待するであろう大まかな結果が得られます。

Ignore: 2682925904
Ignore: 2682925904
Ignore: 2682925904
addif1: 4814
addif2: 3504
addif3: 3021

&代わりに を使用すると*、明確な改善が*られifます。ただし、gcc を使用すると、結果はかなり異なります。

Ignore: 2680875904
Ignore: 2680875904
Ignore: 2680875904
addif1: 2901
addif2: 2886
addif3: 7675

この場合、 を使用するコードは を使用するコードの速度にはるかifに近いです*が、 を使用するコード&はどちらよりも遅くなります。誰かが気にかけている場合に備えて、これは驚くべきことであり、異なるフラグで数回再コンパイルし、それぞれで数回再実行するなど、結果は完全に一貫していました.使用するコードは一貫してかなり遅くなりました. .&

gcc でコンパイルされたコードの 3 番目のバージョンでの悪い結果は、私が最初に言ったことに戻ります [そしてこの編集を終了します]:

最初に言ったように、「確実に知る唯一の方法はテストすることです」-しかし、少なくともこの限られたテストでは、乗算は一貫してif. コンパイラー、コンパイラー・フラグ、CPU、データ・パターン、反復回数などのいくつかの組み合わせが、乗算よりも優先される場合があります。if違いが十分に小さいため、別の方向に進むテストが完全に信頼できることは間違いありません。とはいえ、これは知っておく価値のあるテクニックだと思います。主流のコンパイラと CPU では、かなり効果的です (ただし、gcc よりも MSVC の方が確実に役立ちます)

[edit2 の再開:] gcc を使用した結果は、&1) マイクロ最適化がコンパイラ固有である可能性、および 2) 実際の結果が期待とどの程度異なる可能性があるかを示しています。

于 2010-02-22T01:46:56.677 に答える
1

n^=1より速いですif ( n%2 ) --n; else ++n;か?はい。コンパイラがそれを最適化するとは思わないでしょう。ビット単位の操作は非常に簡潔であるため、XOR に慣れ、そのコード行にコメントを追加することは価値があるかもしれません。

プログラムの機能にとって非常に重要な場合は、移植性の問題と見なすこともできます。コンパイラでテストして高速である場合、別のコンパイラで試してみると驚かれる可能性があります。通常、これは代数最適化の問題ではありません。

x^yより速いですx==yか?いいえ、回りくどいやり方は一般的に良くありません。

于 2010-02-22T01:52:23.610 に答える
0

n ^= 1そしてn1==n2おそらくあなたができる最善のことですが、実際には、最大の効率を求めているのであれば、そのような小さなことを探してコードを目で見てはいけません。

これは、パフォーマンスを実際に調整する方法の例です。

低レベルの最適化が焦点を当てるべき場所であることがサンプリングで証明されるまで、低レベルの最適化が大いに役立つことを期待しないでください。

于 2010-02-22T16:07:25.927 に答える
0

優れたコンパイラは最適化n%2しますが、生成されたアセンブリをいつでもチェックして確認できます。分断が見られる場合は、自分で最適化を開始してください。分断は遅すぎるためです。

于 2010-02-22T01:37:42.587 に答える
0

コンパイラを信頼する必要があります。gcc/++ は何年にもわたる開発の成果であり、おそらく実行しようと考えているあらゆる最適化を行うことができます。そして、いじり始めると、コードを最適化するための努力を改ざんする可能性があります。

于 2010-02-22T01:45:06.870 に答える