使用按位运算符进行优化

时间:2011-12-12 19:05:43

标签: c embedded bit-manipulation

我正在为嵌入式系统编写一个软件,它可以在很短的时间内完成许多输入读数(包括频率),因此主循环需要尽可能快。

我无法决定如何实现以下代码。哪个版本的代码运行得更快?编译器会自动优化代码吗? AND操作比赋值慢吗?

第一个代码:

   if ((a&b) == (b&c))
   {
     if (a&b)
       //something;
     else
       //something else;
   }

第二段代码:

 int p;
 if ((p = (a&b)) == (b&c))
   {
     if (p)
       //something;
     else
       //something else;
   }

3 个答案:

答案 0 :(得分:6)

那么,为什么我们不看看编译器实际上做了什么?以下是MSVC 2010生成的代码片段的输出,其中/Ox开关用于执行优化:

  • 第一个序列:

    ; 6    :     if ((a&b) == (b&c)) {
    
      00000 8b 44 24 08  mov     eax, DWORD PTR _b$[esp-4]
      00004 8b 4c 24 04  mov     ecx, DWORD PTR _a$[esp-4]
      00008 23 c8        and     ecx, eax
      0000a 23 44 24 0c  and     eax, DWORD PTR _c$[esp-4]
      0000e 3b c8        cmp     ecx, eax
      00010 75 0e        jne     SHORT $LN1@first
    
    ; 7    :         if (a&b)
    
      00012 85 c9        test    ecx, ecx
      00014 74 05        je  SHORT $LN2@first
    
    ; 8    :             dummy1();
    
      00016 e9 00 00 00 00   jmp     _dummy1
    $LN2@first:
    
    ; 9    :         else
    ; 10   :             dummy2();
    
      0001b e9 00 00 00 00   jmp     _dummy2
    
  • 第二个序列(使用临时变量):

    ; 6    :     int p;
    ; 7    :     
    ; 8    :     if ((p = (a&b)) == (b&c)) {
    
      00000 8b 4c 24 08  mov     ecx, DWORD PTR _b$[esp-4]
      00004 8b 44 24 04  mov     eax, DWORD PTR _a$[esp-4]
      00008 23 c1        and     eax, ecx
      0000a 23 4c 24 0c  and     ecx, DWORD PTR _c$[esp-4]
      0000e 3b c1        cmp     eax, ecx
      00010 75 0e        jne     SHORT $LN1@second
    
    ; 9    :         if (p)
    
      00012 85 c0        test    eax, eax
      00014 74 05        je  SHORT $LN2@second
    
    ; 10   :             dummy1();
    
      00016 e9 00 00 00 00   jmp     _dummy1
    $LN2@second:
    
    ; 11   :         else
    ; 12   :             dummy2();
    
      0001b e9 00 00 00 00   jmp     _dummy2
    

正如我所料,它们几乎是逐字节相同的(寄存器使用方面略有不同)。但是,使用不同的编译器可能会得到不同的结果。

如果这段代码非常重要,您必须进行微优化,您需要自己查看生成的代码。请记住,代码中使用的最小更改或使用的编译器选项可能会导致生成的代码中存在足够的差异,您可能需要仔细查看它(或者自己在程序集中对其进行编码)。

答案 1 :(得分:6)

一般情况下应该没有什么区别,这取决于你对变量p做了什么。在下面的例子中,p从未使用过,因此应该优化,但是gcc做了一些非常有趣的事情。

unsigned int fun1 ( unsigned int a, unsigned int b, unsigned int c )
{

   if ((a&b) == (b&c))
   {
     if (a&b)
        return(1);
     else
        return(2);
   }

}

unsigned int fun2 ( unsigned int a, unsigned int b, unsigned int c )
{

 int p;
 if ((p = (a&b)) == (b&c))
   {
     if (a&b)
        return(1);
     else
        return(2);
   }

}

首先根据您的特定布尔代数进行优化,使用此处理器选择不同的按位运算符,您可能看不出差异。

00000000 <fun1>:
   0:   e0222000    eor r2, r2, r0
   4:   e1120001    tst r2, r1
   8:   1a000003    bne 1c <fun1+0x1c>
   c:   e1110000    tst r1, r0
  10:   13a00001    movne   r0, #1
  14:   03a00002    moveq   r0, #2
  18:   e12fff1e    bx  lr
  1c:   e12fff1e    bx  lr

00000020 <fun2>:
  20:   e0010000    and r0, r1, r0
  24:   e0022001    and r2, r2, r1
  28:   e1500002    cmp r0, r2
  2c:   0a000000    beq 34 <fun2+0x14>
  30:   e12fff1e    bx  lr
  34:   e3500000    cmp r0, #0
  38:   13a00001    movne   r0, #1
  3c:   03a00002    moveq   r0, #2
  40:   e12fff1e    bx  lr

因此,在你为p赋值的情况下,即使它从未使用过,也会预先设置r0来保存该值。非常奇怪,编译器没有抓住它。因为我没有指定返回值,所以你可以使用上面的fun2代码返回。如果在最后添加一个返回值,那么编译器只需在上面的两个函数中解决这个问题。编译器也应该抱怨我没有返回值而且没有。

对于fun1(),它似乎是使用快捷方式决定是否进入顶级,然后从那里开始。 Fun2正在生成p变量,然后在编写C代码时使用它(比较ands)。对于fun2()的这个实现,你将烧掉一条额外的指令,因此速度较慢。如果不是xor快捷方式,使用这个处理器,如果它已经完成两个并且执行时间本来是相同的,那么编译器可能只是决定将其中一个寄存器保留为p以便稍后或者只是丢弃寄存器。因此,如果您使用不同的按位运算符,您可以期望相同的代码速度。

使用llvm而不是gcc,还要注意我在底部添加了返回值:

unsigned int fun1 ( unsigned int a, unsigned int b, unsigned int c )
{

   if ((a&b) == (b&c))
   {
     if (a&b)
        return(1);
     else
        return(2);
   }
   return(3);

}

unsigned int fun2 ( unsigned int a, unsigned int b, unsigned int c )
{

 int p;
 if ((p = (a&b)) == (b&c))
   {
     if (a&b)
        return(1);
     else
        return(2);
   }
 return(3);
}
在获得特定处理器之前

(注意这是前端的铿锵声)

define i32 @fun1(i32 %a, i32 %b, i32 %c) nounwind readnone {
  %1 = xor i32 %c, %a
  %2 = and i32 %1, %b
  %3 = icmp eq i32 %2, 0
  br i1 %3, label %4, label %8

; <label>:4                                       ; preds = %0
  %5 = and i32 %b, %a
  %6 = icmp eq i32 %5, 0
  br i1 %6, label %7, label %8

; <label>:7                                       ; preds = %4
  br label %8

; <label>:8                                       ; preds = %7, %4, %0
  %9 = phi i32 [ 2, %7 ], [ 1, %4 ], [ 3, %0 ]
  ret i32 %9
}

define i32 @fun2(i32 %a, i32 %b, i32 %c) nounwind readnone {
  %1 = xor i32 %c, %a
  %2 = and i32 %1, %b
  %3 = icmp eq i32 %2, 0
  br i1 %3, label %4, label %8

; <label>:4                                       ; preds = %0
  %5 = and i32 %b, %a
  %6 = icmp eq i32 %5, 0
  br i1 %6, label %7, label %8

; <label>:7                                       ; preds = %4
  br label %8

; <label>:8                                       ; preds = %7, %4, %0
  %9 = phi i32 [ 2, %7 ], [ 1, %4 ], [ 3, %0 ]
  ret i32 %9
}

它已经优化了p变量,因为它没有被使用......

00000000 <fun1>:
   0:   e1a03000    mov r3, r0
   4:   e3a00003    mov r0, #3
   8:   e0222003    eor r2, r2, r3
   c:   e1120001    tst r2, r1
  10:   11a0f00e    movne   pc, lr
  14:   e3a00001    mov r0, #1
  18:   e1110003    tst r1, r3
  1c:   03a00002    moveq   r0, #2
  20:   e1a0f00e    mov pc, lr

00000024 <fun2>:
  24:   e1a03000    mov r3, r0
  28:   e3a00003    mov r0, #3
  2c:   e0222003    eor r2, r2, r3
  30:   e1120001    tst r2, r1
  34:   11a0f00e    movne   pc, lr
  38:   e3a00001    mov r0, #1
  3c:   e1110003    tst r1, r3
  40:   03a00002    moveq   r0, #2
  44:   e1a0f00e    mov pc, lr

为两个函数提供相同的代码。他们做了xor测试,而不是做两个ands。

我通常希望有一条额外的指令来保留第二个函数中的值,具体取决于处理器,优化以及使用该值执行的操作。相对于其余代码实现比较,或者当您添加堆栈/内存访问以保留该值时,影响可能无关小,可能会花费您50%或更多的时间。

出于好奇,我也试过了这个

unsigned int fun3 ( unsigned int a, unsigned int b, unsigned int c )
{

 int p;
 p = a&b;
 if (p == (b&c))
   {
     if (p)
        return(1);
     else
        return(2);
   }
 return(3);
}

GCC

0000004c <fun3>:
  4c:   e0010000    and r0, r1, r0
  50:   e0022001    and r2, r2, r1
  54:   e1500002    cmp r0, r2
  58:   0a000001    beq 64 <fun3+0x18>
  5c:   e3a00003    mov r0, #3
  60:   e12fff1e    bx  lr
  64:   e3500000    cmp r0, #0
  68:   03a00002    moveq   r0, #2
  6c:   13a00001    movne   r0, #1
  70:   e12fff1e    bx  lr

LLVM

00000048 <fun3>:
  48:   e0013000    and r3, r1, r0
  4c:   e0021001    and r1, r2, r1
  50:   e3a00003    mov r0, #3
  54:   e1530001    cmp r3, r1
  58:   11a0f00e    movne   pc, lr
  5c:   e3a00001    mov r0, #1
  60:   e3530000    cmp r3, #0
  64:   03a00002    moveq   r0, #2
  68:   e1a0f00e    mov pc, lr

gcc正在燃烧一个分支,其成本和llvm正在采用管道方法燃烧一些额外的指令周期,但节省了分支和管道冲洗。

你是否开始明白这个想法?您的问题是处理器和编译器以及特定的编译选项和系统(存储器周期的成本与指令周期等)。我希望通过合理的速度存储器接口,性能相同或慢15%到200%。

如果你担心这几行代码的速度......用汇编语言写出来......

答案 2 :(得分:1)

如果关注效率,您可能希望完全摆脱那些if - 语句。它们每个只能施加一个机器指令,但如果你的分支预测器未能做出正确的决定,你最终会得到一个空指令管道,导致相当大的开销。

如果您的条件是布尔值,那么用算术表达式替换if - 语句有一个巧妙的技巧。鉴于以下代码:

if (a) {
    return 3;
} else {
    return 0;
}

然后你可以用以下等效代码摆脱条件跳转:

return a*3;

如果您对0分支中else的返回值感到不满意,您仍可以通过将if (a) { return x; } else { return y; }重写为...来获得性能提升。

return a*x + (!a)*y;

...依靠你的ALU来弄清楚如何最有效地计算结果。大多数ALU可以非常有效地计算0 * x。

将上述技术应用于您的代码,我们获得:

((a&b)==(b&c)) * ((a&b) * /*something*/ + (!(a&b)) * /*something else*/)

由于这与ISO / IEC的先知所考虑的唯一真道无关,我们必须考虑到0*xx*0可能会导致不同的结果执行速度,所以这里需要进行一些测试。


通过这种技术,我在1995年的特定星期二获得了一个小的性能提升,但很可能你的机器无论如何都会更快地执行条件跳转。祝你好运!