16

私はいくつかのフォーラムでこのコードに出くわしました:

if ( a * b * c * d == 0 ) ....

そして所有者はこれが少し速いと主張します

if (a == 0 || b == 0 || c == 0 || d == 0)

これらの変数は次のように定義されます。

int a, b, c, d;

そして、それらの絶対値は100以下であることが保証されています(したがって、オーバーフローの可能性を無視することができます)

を無視readabilityしてパフォーマンスに焦点を当てるだけの場合、主張は本当に正しいのでしょうか。

時々「短絡」を利用できるので、2番目のアプローチは実際にはもっと速いかもしれないように私には思えます。しかし、それでは、私は何を知っていますか?!

4

5 に答える 5

10

C規格は、パフォーマンスについては何も述べていません。かどうかの問題

if ( a * b * c * d == 0 )

よりも速い

if (a == 0 || b == 0 || c == 0 || d == 0)

特定のマシンで実行されるコードを生成する特定のコンパイラのコンテキストでのみ意味があります。それらを比較する唯一の実際の方法は、自分のシステム、または関心のあるシステムでパフォーマンスを測定することです。

それでも、パフォーマンスがどうなるかについて推測することができます。

あなたが言ったようにa、、、、、bはタイプcdオブジェクトですint。また、[-100、+ 100]の範囲にあるとおっしゃいましたが、コンパイラは必ずしもそれを認識していません。

コンパイラーは、任意の式を同じことを行うコードに自由に置き換えることができます。

乗算は比較的複雑な操作であり、たとえば加算や比較よりも遅くなる可能性があります。コンパイラー、4つの変数のいずれかに値がある場合、最初の条件が真になることを認識し0、乗算をより高速なものに置き換えます。ただし、コンパイラーが実行する各最適化は、コンパイラーの開発者によって明示的にプログラムされる必要があり、この特定のパターンは、それを認識する努力に値するほど一般的ではない可能性があります。

値が十分に小さいため、オーバーフローが問題にならないということです。実際、その仮定を移植的に行うことできません。INT_MAXと同じくらい小さくすることができます32767intしかし、コンパイラーは、コードを生成しているシステム上でのがどれだけ大きいかを知っています。それでも、、、、、およびの値に関する情報がない限り、aオーバーフローが発生しないとは想定できません。bcd

はいを除いて、実際には、それはその仮定をすることができます。符号付き整数オーバーフローの動作は定義されていません。これにより、最適化コンパイラはオーバーフローが発生しないと想定することができます(発生した場合でも、プログラムが示す動作はすべて有効です)。

そうです、コンパイラは乗算をより単純なものに置き換えることができますが、そうなる可能性は低いです。

他の式に関してa == 0 || b == 0 || c == 0 || d == 0は、||演算子は短絡セマンティクスを持っています。左のオペランドが真(ゼロ以外)の場合、右のオペランドは評価されません。そして、そのような条件付きコードは、CPUパイプラインの問題が原因でパフォーマンスの問題を引き起こす可能性があります。どの部分式にも副作用がないため(変数が宣言されていないと仮定してvolatile)、コンパイラーは4つの部分式すべてを、おそらく並行して評価できます。

簡単な実験では、x86の場合はどちらの最適化gcc -O3も実行されないことが示されています。最初の式では、3つの乗算を実行するコードを生成します。2つ目は、条件付き分岐を生成し、正規の短絡評価を実装します(これを回避する方が速いかどうかはわかりません)。

最善の策は、ソースコードの読み取りと保守が容易になることと、コンパイラーがパターンを認識して最適化を実行する機会が増える可能性があることから、できるだけ簡単な妥当なコードを作成することです。ソースコードで派手なマイクロ最適化を行おうとすると、コンパイラの最適化を妨げる可能性があります。

コードを測定して遅すぎることがわかった場合を除いて、コードの速度についてあまり心配する必要はありません。コードを高速化する必要がある場合は、まず、改善されたアルゴリズムとデータ構造に集中してください。それが失敗した場合にのみ、ソースレベルのマイクロ最適化を検討してください。

プログラム最適化の最初のルール:それをしないでください。プログラム最適化の2番目のルール(専門家のみ!):まだ実行しないでください。

-マイケル・A・ジャクソン

于 2012-08-04T21:57:40.977 に答える
8

2つは同等ではありません。たとえば、私のマシン(32ビットx86 MSVC)では、a、b、c、およびdがすべて等しい0x100場合、最初のテストは合格しますが、2番目の条件は合格しません。

また、乗算はコストのかかる操作であるため、最初のバージョンが必ずしも高速であるとは限らないことに注意してください。

編集:最初のバージョン用に生成されたコード:

00401000 8B 44 24 04      mov         eax,dword ptr [esp+4] 
00401004 0F AF 44 24 08   imul        eax,dword ptr [esp+8] 
00401009 0F AF 44 24 0C   imul        eax,dword ptr [esp+0Ch] 
0040100E 0F AF 44 24 10   imul        eax,dword ptr [esp+10h] 
00401013 85 C0            test        eax,eax 
00401015 75 07            jne         f1+1Eh (40101Eh) 
00401017 ...

2番目のバージョン用に生成されたコード:

00401020 83 7C 24 04 00   cmp         dword ptr [esp+4],0 
00401025 74 15            je          f2+1Ch (40103Ch) 
00401027 83 7C 24 08 00   cmp         dword ptr [esp+8],0 
0040102C 74 0E            je          f2+1Ch (40103Ch) 
0040102E 83 7C 24 0C 00   cmp         dword ptr [esp+0Ch],0 
00401033 74 07            je          f2+1Ch (40103Ch) 
00401035 83 7C 24 10 00   cmp         dword ptr [esp+10h],0 
0040103A 75 07            jne         f2+23h (401043h) 
0040103C ...

私のマシンのベンチマーク(ナノ秒単位):最初のバージョンは約1.83 nsで実行され、2番目のバージョンは約1.39nsで実行されます。a、b、c、およびdの値は実行ごとに変化しなかったため、分岐予測子は分岐の100%を予測できたようです。

于 2012-08-04T20:28:26.017 に答える
1

いつものように、どちらがより速い質問ですが、これまでに何を試しましたか?コンパイルして逆アセンブルし、何が起こるかを確認しましたか?

unsigned int mfun ( unsigned int a, unsigned int b, unsigned int c, unsigned int d )
{
    if ( a * b * c * d == 0 ) return(7);
    else return(11);
}

unsigned int ofun ( unsigned int a, unsigned int b, unsigned int c, unsigned int d )
{
    if (a == 0 || b == 0 || c == 0 || d == 0) return(7);
    else return(11);
}

アーム1コンパイラはこれを提供します

00000000 <mfun>:
   0:   e0010190    mul r1, r0, r1
   4:   e0020291    mul r2, r1, r2
   8:   e0110293    muls    r1, r3, r2
   c:   13a0000b    movne   r0, #11
  10:   03a00007    moveq   r0, #7
  14:   e12fff1e    bx  lr

00000018 <ofun>:
  18:   e3500000    cmp r0, #0
  1c:   13510000    cmpne   r1, #0
  20:   0a000004    beq 38 <ofun+0x20>
  24:   e3520000    cmp r2, #0
  28:   13530000    cmpne   r3, #0
  2c:   13a0000b    movne   r0, #11
  30:   03a00007    moveq   r0, #7
  34:   e12fff1e    bx  lr
  38:   e3a00007    mov r0, #7
  3c:   e12fff1e    bx  lr

したがって、equalsとorsには短絡がありますが(それ自体はコストがかかります)、最悪のパスには時間がかかるため、パフォーマンスが不安定になり、乗算パフォーマンスはより決定論的で不安定ではなくなります。検査により、上記のコードの乗算ソリューションはより高速になるはずです。

mipsは私にこれをくれました

00000000 <mfun>:
   0:   00a40018    mult    a1,a0
   4:   00002012    mflo    a0
    ...
  10:   00860018    mult    a0,a2
  14:   00002012    mflo    a0
    ...
  20:   00870018    mult    a0,a3
  24:   00002012    mflo    a0
  28:   10800003    beqz    a0,38 <mfun+0x38>
  2c:   00000000    nop
  30:   03e00008    jr  ra
  34:   2402000b    li  v0,11
  38:   03e00008    jr  ra
  3c:   24020007    li  v0,7

00000040 <ofun>:
  40:   10800009    beqz    a0,68 <ofun+0x28>
  44:   00000000    nop
  48:   10a00007    beqz    a1,68 <ofun+0x28>
  4c:   00000000    nop
  50:   10c00005    beqz    a2,68 <ofun+0x28>
  54:   00000000    nop
  58:   10e00003    beqz    a3,68 <ofun+0x28>
  5c:   00000000    nop
  60:   03e00008    jr  ra
  64:   2402000b    li  v0,11
  68:   03e00008    jr  ra
  6c:   24020007    li  v0,7

ブランチが高すぎる場合を除いて、equalsとorsはより速く見えます。

Openrisc 32

00000000 <mfun>:
   0:   e0 64 1b 06     l.mul r3,r4,r3
   4:   e0 a3 2b 06     l.mul r5,r3,r5
   8:   e0 c5 33 06     l.mul r6,r5,r6
   c:   bc 26 00 00     l.sfnei r6,0x0
  10:   0c 00 00 04     l.bnf 20 <mfun+0x20>
  14:   9d 60 00 0b     l.addi r11,r0,0xb
  18:   44 00 48 00     l.jr r9
  1c:   15 00 00 00     l.nop 0x0
  20:   44 00 48 00     l.jr r9
  24:   9d 60 00 07     l.addi r11,r0,0x7

00000028 <ofun>:
  28:   e0 e0 20 02     l.sub r7,r0,r4
  2c:   e0 87 20 04     l.or r4,r7,r4
  30:   bd 64 00 00     l.sfgesi r4,0x0
  34:   10 00 00 10     l.bf 74 <ofun+0x4c>
  38:   e0 80 18 02     l.sub r4,r0,r3
  3c:   e0 64 18 04     l.or r3,r4,r3
  40:   bd 63 00 00     l.sfgesi r3,0x0
  44:   10 00 00 0c     l.bf 74 <ofun+0x4c>
  48:   e0 60 30 02     l.sub r3,r0,r6
  4c:   e0 c3 30 04     l.or r6,r3,r6
  50:   bd 66 00 00     l.sfgesi r6,0x0
  54:   10 00 00 08     l.bf 74 <ofun+0x4c>
  58:   e0 60 28 02     l.sub r3,r0,r5
  5c:   e0 a3 28 04     l.or r5,r3,r5
  60:   bd 85 00 00     l.sfltsi r5,0x0
  64:   0c 00 00 04     l.bnf 74 <ofun+0x4c>
  68:   9d 60 00 0b     l.addi r11,r0,0xb
  6c:   44 00 48 00     l.jr r9
  70:   15 00 00 00     l.nop 0x0
  74:   44 00 48 00     l.jr r9
  78:   9d 60 00 07     l.addi r11,r0,0x7

これは乗算の実装に依存します。1クロックの場合、乗算にはそれがあります。

ハードウェアが乗算をサポートしていない場合は、電話をかけてシミュレートする必要があります

00000000 <mfun>:
   0:   0b 12           push    r11     
   2:   0a 12           push    r10     
   4:   09 12           push    r9      
   6:   09 4d           mov r13,    r9  
   8:   0b 4c           mov r12,    r11 
   a:   0a 4e           mov r14,    r10 
   c:   0c 4f           mov r15,    r12 
   e:   b0 12 00 00     call    #0x0000 
  12:   0a 4e           mov r14,    r10 
  14:   0c 49           mov r9, r12 
  16:   b0 12 00 00     call    #0x0000 
  1a:   0a 4e           mov r14,    r10 
  1c:   0c 4b           mov r11,    r12 
  1e:   b0 12 00 00     call    #0x0000 
  22:   0e 93           tst r14     
  24:   06 24           jz  $+14        ;abs 0x32
  26:   3f 40 0b 00     mov #11,    r15 ;#0x000b
  2a:   39 41           pop r9      
  2c:   3a 41           pop r10     
  2e:   3b 41           pop r11     
  30:   30 41           ret         
  32:   3f 40 07 00     mov #7, r15 ;#0x0007
  36:   39 41           pop r9      
  38:   3a 41           pop r10     
  3a:   3b 41           pop r11     
  3c:   30 41           ret         

0000003e <ofun>:
  3e:   0f 93           tst r15     
  40:   09 24           jz  $+20        ;abs 0x54
  42:   0e 93           tst r14     
  44:   07 24           jz  $+16        ;abs 0x54
  46:   0d 93           tst r13     
  48:   05 24           jz  $+12        ;abs 0x54
  4a:   0c 93           tst r12     
  4c:   03 24           jz  $+8         ;abs 0x54
  4e:   3f 40 0b 00     mov #11,    r15 ;#0x000b
  52:   30 41           ret         
  54:   3f 40 07 00     mov #7, r15 ;#0x0007
  58:   30 41    

2つが同等であり、純粋数学的な意味から、乗算の結果をゼロにするためには、1つのオペランドをゼロにする必要があります。問題は、これがプロセッサ用のソフトウェアであるということです。乗算で簡単にオーバーフローし、ゼロ以外のオペランドを使用してもゼロを取得できるため、乗算が発生する必要のあるコードを適切に実装できます。

特にmulとdivideのコストのため、ソフトウェアではそれらをできるだけ避ける必要があります。この場合、2つのソリューションを同等にするための乗算ソリューションでは、オーバーフローのケースを検出または防止するためにさらに多くのコードが必要になります。誤検知に。はい、多くのプロセッサは1クロックでmulを実行し、除算も行います。除算が表示されない理由、およびmulが命令セットに実装されていない場合がある理由は、チップのスペースが必要であり、コストが電力、熱、パーツのコストなど。したがって、マルチとディバイドは、もちろんこれらに限定されるものではなく、高価なままですが、パーツのパフォーマンス、クロックレートに関して、テント内に長いポールを作成します。命令によりチップ全体の速度が低下し、マルチクロックが可能になる場合があります全体的なクロックレートが上がる可能性があります。テントの中の長いポールはたくさんあるので、mulを削除してもパフォーマンスは変わらないかもしれませんが、それはすべて異なります...

于 2012-08-04T21:13:54.470 に答える
0

はい、if命令が失敗した場合、この場合はat most 4 comparisons (Operations)2番目の命令で実行し、最初の命令では常に実行します4 operations

編集:説明

2番目のif命令は、常に最初の命令よりも高速です。

:a = 1、b = 2、c = 0、d = 4と仮定します。この場合:

  • 最初の命令の場合:3つの乗算と比較=4つの演算があります

  • 2番目のif命令の場合:aを0(結果KO)、次にbを0(再びKO)、cを0(OK)=3操作と比較します。

これは、この2つの命令の実行時間を出力する単純なプログラムであり、a、b、c、およびdを変更して、引数として命令の番号を渡すことができます。

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

/* This is a test program to demonstrate that the second if is faster always than the first one*/
int main(int argc, char **argv)
{
        int i;
        int a = 1;
        int b = 2;
        int c = 0;
        int d = 4;
        int instruction_number;
        clock_t begin, end;
        double time_spent;

        begin = clock();

        if (argc != 2)
        {
                fprintf(stderr, "Usage : ./a.out if_instruction_number (1 or 2)\n");

                exit(EXIT_FAILURE);
        }

        instruction_number = atoi(argv[1]);

        for (i = 1; i < 100000; i++)
        {
                switch (instruction_number)
                {
                        case 1:
                                fprintf(stdout, "First if instruction : \n");
                                if (a * b * c * d == 0)
                                        fprintf(stdout, "1st instruction\n");
                                break;
                        case 2:
                                fprintf(stdout, "Second if instruction : \n");
                                if (a == 0 || b == 0 || c == 0 || d == 0)
                                        fprintf(stdout, "2nd instruction\n");
                                break;
                        default:
                                break;
                }
        }
        end = clock();
        time_spent = (double)(end - begin) / CLOCKS_PER_SEC;
        fprintf(stdout, "Time to accomplish %d instruction ---> %f\n", instruction_number, time_spent);

        return 0;
} 

この助けを願っています。

よろしく。

于 2012-08-04T20:26:04.847 に答える
0

if ( a * b * c * d == 0 )コンパイル先(最適化なし)

movl   16(%esp), %eax
imull  20(%esp), %eax
imull  24(%esp), %eax
imull  28(%esp), %eax
testl  %eax, %eax
jne .L3

if (a == 0 || b == 0 || c == 0 || d == 0)コンパイルします

cmpl   $0, 16(%esp)
je  .L2
cmpl    $0, 20(%esp)
je  .L2
cmpl    $0, 24(%esp)
je .L2
cmpl    $0, 28(%esp)
jne .L4
于 2012-08-04T20:32:48.517 に答える