如果声明vs if-else语句,哪个更快?

时间:2017-04-04 08:28:49

标签: c++ performance c++11 assembly microbenchmark

我前几天和朋友争论过这两个片段。哪个更快,为什么?

java.util.HashMap

value = 5;
if (condition) {
    value = 6;
}

如果if (condition) { value = 6; } else { value = 5; } 是矩阵怎么办?

注意:我知道value存在且我希望它更快,但它不是一个选项。

修改(工作人员要求,因为此时问题已暂停):

  • 请通过考虑主流编译器( g ++,clang ++,vc,mingw )在优化和非优化版本中生成的 x86程序集 MIPS来回答装配
  • 当汇编不同时,解释为什么版本更快以及何时(例如“更好,因为没有分支和分支跟随问题blahblah”

6 个答案:

答案 0 :(得分:272)

TL; DR:在未经优化的代码中,String str = "Name.png"; if (str.endsWith(".")) { System.out.println("NOT CORRECT"); } else { System.out.println("CORRECT"); } 没有if似乎效率更高,但即使启用了最基本的优化级别,代码也基本上被重写为{ {1}}。

gave it a try并为以下代码生成了程序集:

else

在gcc 6.3上禁用了优化(value = condition + 5),相关的区别是:

int ifonly(bool condition, int value)
{
    value = 5;
    if (condition) {
        value = 6;
    }
    return value;
}

int ifelse(bool condition, int value)
{
    if (condition) {
        value = 6;
    } else {
        value = 5;
    }
    return value;
}

代表-O0,而 mov DWORD PTR [rbp-8], 5 cmp BYTE PTR [rbp-4], 0 je .L2 mov DWORD PTR [rbp-8], 6 .L2: mov eax, DWORD PTR [rbp-8] 代表

ifonly

后者看起来效率稍差,因为它有一个额外的跳跃,但两者至少有两个,最多三个任务,所以除非你真的需要挤压每一滴性能(提示:除非你正在航天飞机上工作你不要,即使那时你可能不这样做,差异也不会明显。

然而,即使最低优化级别(ifelse),两个函数都会减少到相同的值:

 cmp     BYTE PTR [rbp-4], 0
 je      .L5
 mov     DWORD PTR [rbp-8], 6
 jmp     .L6
.L5:
 mov     DWORD PTR [rbp-8], 5
.L6:
 mov     eax, DWORD PTR [rbp-8]

基本上相当于

-O1

假设test dil, dil setne al movzx eax, al add eax, 5 为零或一。 更高的优化级别并没有真正改变输出,除非他们设法通过在开始时有效地将return 5 + condition; 寄存器清零来避免condition

免责声明:您可能不应该自己编写movzx(即使标准保证将EAX转换为整数类型也会5 + condition),因为您的对于阅读代码的人来说,意图可能不会立即明显(可能包括你未来的自我)。这段代码的要点是表明编译器在两种情况下产生的内容(实际上)是相同的。 Ciprian Tomoiaga在评论中说得很清楚:

  人类的工作是为人类编写代码,让编译器机器编写代码>。

答案 1 :(得分:44)

来自CompuChip的答案显示,对于int,它们都针对同一个程序集进行了优化,因此无关紧要。

  

如果值是矩阵怎么办?

我将以更一般的方式解释这一点,即如果value是一种类型,其构造和赋值很昂贵(并且移动很便宜),那该怎么办呢。

然后

T value = init1;
if (condition)
   value = init2;

是次优的,因为如果condition为真,则对init1执行不必要的初始化,然后执行复制分配。

T value;
if (condition)
   value = init2;
else
   value = init3;

这样更好。但如果默认构造很昂贵,并且复制构造比初始化更昂贵,那么仍然是次优的。

你有条件运算符解决方案,这是好的:

T value = condition ? init1 : init2;

或者,如果你不喜欢条件运算符,你可以创建一个这样的辅助函数:

T create(bool condition)
{
  if (condition)
     return {init1};
  else
     return {init2};
}

T value = create(condition);

根据init1init2的不同,您也可以考虑以下事项:

auto final_init = condition ? init1 : init2;
T value = final_init;

但是我必须再次强调,只有当给定类型的构造和分配真的很昂贵时,这才是相关的。即便如此,只有通过分析,你才知道。

答案 2 :(得分:11)

在伪汇编语言中,

    li    #0, r0
    test  r1
    beq   L1
    li    #1, r0
L1:

可能会或可能不会快于

    test  r1
    beq   L1
    li    #1, r0
    bra   L2
L1:
    li    #0, r0
L2:

取决于实际CPU的复杂程度。从最简单到最高兴:

  • 使用大约1990年后制造的任何 CPU,良好的性能取决于instruction cache中的代码拟合。因此,如果有疑问,请尽量减少代码大小。这有利于第一个例子。

  • 使用基本的“in-order, five-stage pipeline”CPU,这仍然是您在许多微控制器中得到的CPU,每次采用分支条件或无条件时都会有pipeline bubble,所以最小化分支指令的数量也很重要。这也有利于第一个例子。

  • 有些更复杂的CPU - 看上去足够“out-of-order execution”,但不足以使用该概念的最着名的实现 - 可能会在遇到write-after-write hazards时产生管道泡沫。这有利于第二个示例,其中r0无论如何只写一次。这些CPU通常足以在指令提取器中处理无条件分支,因此不会仅仅为分支惩罚交换写后写入惩罚。

    我不知道是否有人还在制作这种CPU。但是,使用无序执行的“最着名的实现”的CPU可能会在不常使用的指令上偷工减料,所以你需要意识到这种事情可以发生。一个真实的例子是false data dependencies on the destination registers in popcnt and lzcnt on Sandy Bridge CPUs

  • 在最高端,OOO引擎将最终为两个代码片段发出完全相同的内部操作序列 - 这是硬件版本“不用担心它,编译器将生成相同的机器码无论如何。“但是,代码大小仍然很重要,现在您还应该担心条件分支的可预测性。 Branch prediction个失败可能会导致完整的管道 flush ,这对性能来说是灾难性的;请参阅Why is it faster to process a sorted array than an unsorted array?了解这可以带来多大的不同。

    如果分支 高度不可预测,并且您的CPU具有条件设置或条件移动指令,则可以使用它们:

        li    #0, r0
        test  r1
        setne r0
    

        li    #0, r0
        li    #1, r2
        test  r1
        movne r2, r0
    

    条件集版本也比任何其他替代版本更紧凑;如果该指令可用,则实际上保证对于这种情况是正确的事情,即使该分支是可预测的。条件移动版本需要一个额外的临时寄存器,并且总是浪费一个li指令的调度和执行资源;如果分支实际上是可预测的,那么分支版本可能会更快。

答案 3 :(得分:10)

在未经优化的代码中,第一个示例总是分配一次变量,有时两次。第二个例子只分配一次变量。两个代码路径上的条件相同,因此无关紧要。在优化代码中,它取决于编译器。

与往常一样,如果您有关,请生成程序集并查看编译器实际执行的操作。

答案 4 :(得分:8)

什么会让你认为他们中的任何一个甚至一个班轮更快还是更慢?

unsigned int fun0 ( unsigned int condition, unsigned int value )
{
    value = 5;
    if (condition) {
        value = 6;
    }
    return(value);
}
unsigned int fun1 ( unsigned int condition, unsigned int value )
{

    if (condition) {
        value = 6;
    } else {
        value = 5;
    }
    return(value);
}
unsigned int fun2 ( unsigned int condition, unsigned int value )
{
    value = condition ? 6 : 5;
    return(value);
}

高级语言的更多代码行使编译器可以使用更多代码,因此如果您想对其进行一般规则,请为编译器提供更多代码。如果算法与上面的情况相同,那么人们会期望编译器具有最小的优化来计算出来。

00000000 <fun0>:
   0:   e3500000    cmp r0, #0
   4:   03a00005    moveq   r0, #5
   8:   13a00006    movne   r0, #6
   c:   e12fff1e    bx  lr

00000010 <fun1>:
  10:   e3500000    cmp r0, #0
  14:   13a00006    movne   r0, #6
  18:   03a00005    moveq   r0, #5
  1c:   e12fff1e    bx  lr

00000020 <fun2>:
  20:   e3500000    cmp r0, #0
  24:   13a00006    movne   r0, #6
  28:   03a00005    moveq   r0, #5
  2c:   e12fff1e    bx  lr

以不同的顺序执行第一个函数,但执行时间相同,这并不是什么大惊喜。

0000000000000000 <fun0>:
   0:   7100001f    cmp w0, #0x0
   4:   1a9f07e0    cset    w0, ne
   8:   11001400    add w0, w0, #0x5
   c:   d65f03c0    ret

0000000000000010 <fun1>:
  10:   7100001f    cmp w0, #0x0
  14:   1a9f07e0    cset    w0, ne
  18:   11001400    add w0, w0, #0x5
  1c:   d65f03c0    ret

0000000000000020 <fun2>:
  20:   7100001f    cmp w0, #0x0
  24:   1a9f07e0    cset    w0, ne
  28:   11001400    add w0, w0, #0x5
  2c:   d65f03c0    ret

希望你明白你可以尝试过这个想法,如果不明显的是不同的实现并没有实际不同。

就矩阵而言,不确定这是多么重要,

if(condition)
{
 big blob of code a
}
else
{
 big blob of code b
}

只是将相同的if-then-else包装器放在大块代码周围,它们值= 5或更复杂的东西。同样地进行比较,即使它是一大堆代码,它仍然必须被计算,并且等于或不等于某些东西通常用负数编译,如果(条件)做​​某事通常被编译为好像不是条件goto。 / p>

00000000 <fun0>:
   0:   0f 93           tst r15     
   2:   03 24           jz  $+8         ;abs 0xa
   4:   3f 40 06 00     mov #6, r15 ;#0x0006
   8:   30 41           ret         
   a:   3f 40 05 00     mov #5, r15 ;#0x0005
   e:   30 41           ret         

00000010 <fun1>:
  10:   0f 93           tst r15     
  12:   03 20           jnz $+8         ;abs 0x1a
  14:   3f 40 05 00     mov #5, r15 ;#0x0005
  18:   30 41           ret         
  1a:   3f 40 06 00     mov #6, r15 ;#0x0006
  1e:   30 41           ret         

00000020 <fun2>:
  20:   0f 93           tst r15     
  22:   03 20           jnz $+8         ;abs 0x2a
  24:   3f 40 05 00     mov #5, r15 ;#0x0005
  28:   30 41           ret         
  2a:   3f 40 06 00     mov #6, r15 ;#0x0006
  2e:   30 41

我们刚刚与其他人在stackoverflow上进行了这项练习。这个mips编译器很有趣,在这种情况下不仅实现了功能相同,而且只有一个函数只是跳转到另一个以节省代码空间。虽然不是这样做的,但

00000000 <fun0>:
   0:   0004102b    sltu    $2,$0,$4
   4:   03e00008    jr  $31
   8:   24420005    addiu   $2,$2,5

0000000c <fun1>:
   c:   0004102b    sltu    $2,$0,$4
  10:   03e00008    jr  $31
  14:   24420005    addiu   $2,$2,5

00000018 <fun2>:
  18:   0004102b    sltu    $2,$0,$4
  1c:   03e00008    jr  $31
  20:   24420005    addiu   $2,$2,5

更多目标。

00000000 <_fun0>:
   0:   1166            mov r5, -(sp)
   2:   1185            mov sp, r5
   4:   0bf5 0004       tst 4(r5)
   8:   0304            beq 12 <_fun0+0x12>
   a:   15c0 0006       mov $6, r0
   e:   1585            mov (sp)+, r5
  10:   0087            rts pc
  12:   15c0 0005       mov $5, r0
  16:   1585            mov (sp)+, r5
  18:   0087            rts pc

0000001a <_fun1>:
  1a:   1166            mov r5, -(sp)
  1c:   1185            mov sp, r5
  1e:   0bf5 0004       tst 4(r5)
  22:   0204            bne 2c <_fun1+0x12>
  24:   15c0 0005       mov $5, r0
  28:   1585            mov (sp)+, r5
  2a:   0087            rts pc
  2c:   15c0 0006       mov $6, r0
  30:   1585            mov (sp)+, r5
  32:   0087            rts pc

00000034 <_fun2>:
  34:   1166            mov r5, -(sp)
  36:   1185            mov sp, r5
  38:   0bf5 0004       tst 4(r5)
  3c:   0204            bne 46 <_fun2+0x12>
  3e:   15c0 0005       mov $5, r0
  42:   1585            mov (sp)+, r5
  44:   0087            rts pc
  46:   15c0 0006       mov $6, r0
  4a:   1585            mov (sp)+, r5
  4c:   0087            rts pc

00000000 <fun0>:
   0:   00a03533            snez    x10,x10
   4:   0515                    addi    x10,x10,5
   6:   8082                    ret

00000008 <fun1>:
   8:   00a03533            snez    x10,x10
   c:   0515                    addi    x10,x10,5
   e:   8082                    ret

00000010 <fun2>:
  10:   00a03533            snez    x10,x10
  14:   0515                    addi    x10,x10,5
  16:   8082                    ret

和编译器

使用此i代码可以预期不同的目标也匹配

define i32 @fun0(i32 %condition, i32 %value) #0 {
  %1 = icmp ne i32 %condition, 0
  %. = select i1 %1, i32 6, i32 5
  ret i32 %.
}

; Function Attrs: norecurse nounwind readnone
define i32 @fun1(i32 %condition, i32 %value) #0 {
  %1 = icmp eq i32 %condition, 0
  %. = select i1 %1, i32 5, i32 6
  ret i32 %.
}

; Function Attrs: norecurse nounwind readnone
define i32 @fun2(i32 %condition, i32 %value) #0 {
  %1 = icmp ne i32 %condition, 0
  %2 = select i1 %1, i32 6, i32 5
  ret i32 %2
}


00000000 <fun0>:
   0:   e3a01005    mov r1, #5
   4:   e3500000    cmp r0, #0
   8:   13a01006    movne   r1, #6
   c:   e1a00001    mov r0, r1
  10:   e12fff1e    bx  lr

00000014 <fun1>:
  14:   e3a01006    mov r1, #6
  18:   e3500000    cmp r0, #0
  1c:   03a01005    moveq   r1, #5
  20:   e1a00001    mov r0, r1
  24:   e12fff1e    bx  lr

00000028 <fun2>:
  28:   e3a01005    mov r1, #5
  2c:   e3500000    cmp r0, #0
  30:   13a01006    movne   r1, #6
  34:   e1a00001    mov r0, r1
  38:   e12fff1e    bx  lr


fun0:
    push.w  r4
    mov.w   r1, r4
    mov.w   r15, r12
    mov.w   #6, r15
    cmp.w   #0, r12
    jne .LBB0_2
    mov.w   #5, r15
.LBB0_2:
    pop.w   r4
    ret

fun1:
    push.w  r4
    mov.w   r1, r4
    mov.w   r15, r12
    mov.w   #5, r15
    cmp.w   #0, r12
    jeq .LBB1_2
    mov.w   #6, r15
.LBB1_2:
    pop.w   r4
    ret


fun2:
    push.w  r4
    mov.w   r1, r4
    mov.w   r15, r12
    mov.w   #6, r15
    cmp.w   #0, r12
    jne .LBB2_2
    mov.w   #5, r15
.LBB2_2:
    pop.w   r4
    ret

现在从技术上讲,这些解决方案中存在一些性能差异,有时结果是5个案例跳过结果是6个代码,反之亦然,是一个比执行更快的分支?人们可以争辩,但执行应该有所不同。但是这更像是一个if条件vs如果没有条件在代码中导致编译器执行如果这跳过else执行通过。但这不一定是由于编码风格,而是比较以及if和else的情况,无论采用何种语法。

答案 5 :(得分:0)

好的,因为汇编是其中一个标签,我只假设您的代码是伪代码(并不一定是c),并将其转换为6502汇编。

第一个选项(没有别的)

library(ggplot2)
library(shiny)
library(plotly)
library(htmlwidgets)

ui <- basicPage(
  plotlyOutput("plot1")
)

server <- function(input, output) {

  p <- ggplot(mtcars, aes(x = wt, y = mpg)) + geom_point(alpha=0) + xlim(0,5) +ylim(-3,3)
  gp <- ggplotly(p)

  set.seed(3)
  myDF <- data.frame(X1=rnorm(10,-1), X2=rnorm(10,-1), X3=rnorm(10,-1), X4=rnorm(10,1), X5=rnorm(10,1), X6=rnorm(10,1))
  colNms <- colnames(myDF)
  nVar <- length(colNms)

  output$plot1 <- renderPlotly({
    gp %>% onRender("
    function(el, x, data) {

    var myDF = data.myDF
    var Traces = [];
    var dLength = myDF.length
    var vLength = data.nVar
    var cNames = data.colNms
    for (a=0; a<dLength; a++){
      xArr = [];
      yArr = [];
      for (b=0; b<vLength; b++){
        xArr.push(b)
        yArr.push(myDF[a][cNames[b]]);
      }
      var pcpLine = {
        x: xArr,
        y: yArr,
        mode: 'lines',
        line: {
          color: 'orange',
          width: 1
        },
        opacity: 0.9,
      }
      Traces.push(pcpLine);
    }
    Plotly.addTraces(el.id, Traces);

    el.on('plotly_selected', function(e) {
      var dLength = myDF.length
      var selectedPCP = []
      var xMin = e.range.x[0]
      var xMax = e.range.x[1]
      var yMin = e.range.y[0]
      var yMax = e.range.y[1]

      console.log([xMin, xMax, yMin, yMax])

      var Traces = []
      var drawRect = {
        type: 'rect',
        x0: xMin,
        y0: yMin,
        x1: xMax,
        y1: yMax,
        line: {
          color: 'green',
          width: 1
        },
        fillcolor: 'green'
      }
      Traces.push(drawRect);
      Plotly.addTraces(el.id, Traces);
    })
    }", data = list(myDF = myDF, nVar = nVar, colNms = colNms))})

    }
    shinyApp(ui, server)

第二个选项(带别号)

        ldy #$00
        lda #$05
        dey
        bmi false
        lda #$06
false   brk

假设:条件在Y寄存器中,在任一选项的第一行设置为0或1,结果将在累加器中。

因此,在计算每种情况的两种可能性的周期之后,我们看到第一种结构通常更快;条件为0时为9个循环,条件为1时为10个循环,而条件为0时选项2为9个循环,条件为1时为3个循环(循环计数不包括 ldy #$00 dey bmi else lda #$06 sec bcs end else lda #$05 end brk at结束)。

结论:BRKIf only构建更快。

为了完整起见,这是一个优化的If-Else解决方案:

value = condition + 5

这会将我们的时间减少到8个周期(再次不包括最后的ldy #$00 lda #$00 tya adc #$05 brk )。