ChatGPT解决这个技术问题 Extra ChatGPT

if 语句与 if-else 语句,哪个更快?

前几天我和一个朋友就这两个片段发生了争执。哪个更快,为什么?

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

和:

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

如果 value 是矩阵怎么办?

注意:我知道 value = condition ? 6 : 5; 存在并且我希望它更快,但它不是一个选项。

编辑(由工作人员要求,因为问题目前处于搁置状态):

请通过考虑由优化和非优化版本的主流编译器(例如 g++、clang++、vc、mingw)或 MIPS 程序集生成的 x86 程序集来回答。

当组装不同时,解释为什么版本更快以及何时(例如“更好,因为没有分支和分支有以下问题等等”)

优化将杀死所有这些......没关系......
您可以对其进行分析,我个人怀疑您是否会看到使用现代编译器的任何区别。
使用 value = condition ? 6 : 5; 而不是 if/else 很可能会生成相同的代码。如果您想了解更多信息,请查看汇编输出。
在这种情况下最重要的是避免分支,这是这里最昂贵的东西。 (管道重新加载,丢弃预取指令等)
唯一一次对这样的速度进行微优化是有意义的,它是在一个将运行很多次的循环中,或者优化器可以优化所有分支指令,就像 gcc 可以用于这个简单的例子,或者在现实世界中性能在很大程度上取决于正确的分支预测(必须链接到 stackoverflow.com/questions/11227809/…)。如果您不可避免地在循环内分支,您可以通过生成配置文件并使用它重新编译来帮助分支预测器。

C
Community

TL;DR:在未优化的代码中,没有 elseif 似乎更高效,但即使启用了最基本的优化级别,代码也基本上被重写为 value = condition + 5

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

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) 的 gcc 6.3 上,相关区别是:

 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 为零或一。更高的优化级别并不会真正改变输出,除了它们通过在开始时有效地将 EAX 寄存器清零来设法避免 movzx

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

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


这是一个很好的答案,应该被接受。
我从来没有想过使用加法(<- python 对你做了什么。)
@CiprianTomoiaga,除非您正在编写优化器,否则您不应该!在几乎所有情况下,您都应该让编译器进行这样的优化,尤其是在它们严重降低代码可读性的情况下。只有当性能测试显示某段代码存在问题时,您才应该开始尝试对其进行优化,即便如此,也要保持它的整洁和良好的注释,并且只执行能够产生可衡量差异的优化。
我想回复 Muzer,但它不会在线程中添加任何内容。但是,我只想重申,人类的工作是为人类编写代码,让编译器为机器编写代码。我是从编译器开发人员 PoV 那里说的(我不是,但我对它们有所了解)
The value true converted to int always yields 1, period. 当然,如果您的条件仅仅是“真实”而不是 booltrue,那么这是完全不同的事情。
b
bolov

CompuChip 的答案表明,对于 int,它们都针对同一个程序集进行了优化,因此没关系。

如果 value 是一个矩阵呢?

我将以更一般的方式解释这一点,即如果 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;

但我必须再次强调,这仅在给定类型的构造和分配非常昂贵时才相关。即便如此,只有通过分析你才能确定。


昂贵且未优化。例如,如果默认构造函数将矩阵清零,编译器可以意识到赋值只是要覆盖那些 0,所以根本不将其清零,而是直接写入这个内存。当然,优化器是挑剔的野兽,所以很难预测它们什么时候开始......
@MatthieuM。当然。我所说的“昂贵”是指“即使在编译器优化之后,执行起来也很昂贵(按指标,无论是 CPU 时钟、资源利用率等)。
在我看来,默认建设不太可能会很昂贵,但移动很便宜。
@plugwash 考虑一个分配了非常大的数组的类。默认构造函数分配和初始化数组,这是昂贵的。移动(不是复制!)构造函数只需与源对象交换指针,不需要分配或初始化大数组。
只要部分简单,我肯定更喜欢使用 ?: 运算符而不是引入新函数。毕竟,您可能不仅会将条件传递给函数,还会传递一些构造函数参数。根据条件,其中一些甚至可能不会被 create() 使用。
z
zwol

在伪汇编语言中,

    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,良好的性能取决于指令缓存中的代码。因此,如有疑问,请尽量减少代码大小。这有利于第一个例子。

使用基本的“有序、五级流水线”CPU(这仍然是您在许多微控制器中得到的大致内容),每次执行分支(有条件或无条件)时都会出现流水线气泡,因此最小化也很重要分支指令的数量。这也有利于第一个例子。

稍微复杂一点的 CPU——足够花哨以执行“乱序执行”,但不够花哨以使用该概念的最知名实现——在遇到写后写危险时可能会引发管道气泡。这有利于第二个示例,其中 r0 无论如何只写入一次。这些 CPU 通常足够花哨,可以处理指令提取器中的无条件分支,因此您不只是用写后写惩罚来换取分支惩罚。不知道现在还有没有人在做这种CPU。但是,确实使用乱序执行的“最知名的实现”的 CPU 可能会在不常用的指令上偷工减料,因此您需要注意这种事情可能会发生。一个真实的例子是 Sandy Bridge CPU 上 popcnt 和 lzcnt 中目标寄存器的错误数据依赖关系。

在最高端,OOO 引擎最终会为两个代码片段发出完全相同的内部操作序列——这是硬件版本的“不用担心,无论哪种方式,编译器都会生成相同的机器代码”。但是,代码大小仍然很重要,现在您还应该担心条件分支的可预测性。分支预测失败可能会导致完整的管道刷新,这对性能来说是灾难性的;请参阅为什么处理排序数组比处理未排序数组更快?了解这可以产生多大的影响。如果分支是高度不可预测的,并且您的 CPU 有条件设置或条件移动指令,那么是时候使用它们了:li #0, r0 test r1 setne r0 or li #0, r0 li #1, r2 test r1 movne r2, r0 条件集版本也比任何其他替代方案更紧凑;如果该指令可用,即使分支是可预测的,它实际上也可以保证在这种情况下是正确的。条件移动版本需要一个额外的临时寄存器,并且总是浪费一条 li 指令的调度和执行资源;如果分支实际上是可预测的,那么分支版本可能会更快。


关于CPU是否有一个因写后写危险而延迟的乱序引擎,我将改写你的第二点。如果 CPU 有一个乱序引擎,可以毫不拖延地处理这些危险,那没有问题,但如果 CPU 根本没有乱序引擎,也没有问题。
@supercat 最后的段落旨在涵盖该案例,但我会考虑如何使其更清晰。
我不知道当前的 CPU 有哪些缓存,这会导致第二次按顺序执行的代码比第一次运行得更快(一些基于闪存的 ARM 部件有一个可以缓冲几行闪存数据的接口,但是可以像执行代码一样快地按顺序获取代码,但是使分支繁重的代码在这些代码上快速运行的关键是将其复制到 RAM 中)。完全没有任何乱序执行的 CPU 比那些会因写后写危险而延迟的 CPU 要常见得多。
这是非常有见地的
N
Neil

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

与往常一样,如果您很担心,请生成程序集并查看编译器实际在做什么。


如果担心性能,那么他们不会在未优化的情况下编译。但当然,优化器有多“好”取决于编译器/版本。
AFAIK 没有关于哪个编译器/CPU 架构等的评论,因此他们的编译器可能不会进行优化。他们可以在任何东西上编译,从 8 位 PIC 到 64 位 Xeon。
o
old_timer

是什么让你觉得他们中的任何一个,即使是一个班轮更快或更慢?

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 包装器,无论它们 value=5 还是更复杂的东西。同样,比较即使它是一大块代码,它仍然需要计算,并且等于或不等于某事通常用否定编译,如果(条件)做某事通常编译为 if not condition goto。

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

和编译器

有了这个我的代码,人们会期望不同的目标也能匹配

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 条件与 if not 条件,导致编译器执行 if this 跳过 else 执行。但这不一定是由于编码风格,而是比较以及任何语法中的 if 和 else 情况。


G
Glen Yates

好的,由于程序集是标签之一,我将假设您的代码是伪代码(不一定是 c)并由人工将其翻译成 6502 程序集。

第一个选项(没有其他)

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

第二个选项(其他)

        ldy #$00
        dey
        bmi else
        lda #$06
        sec
        bcs end
else    lda #$05
end     brk

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

因此,在计算每种情况的两种可能性的周期之后,我们看到第一个构造通常更快;条件为 0 时为 9 个周期,条件为 1 时为 10 个周期,而选项二在条件为 0 时也是 9 个周期,但条件为 1 时为 13 个周期。(循环计数不包括末尾的 BRK )。

结论:If onlyIf-Else 构造更快。

为了完整起见,这里有一个优化的 value = condition + 5 解决方案:

ldy #$00
lda #$00
tya
adc #$05
brk

这将我们的时间减少到 8 个周期(同样不包括最后的 BRK)。


不幸的是,对于这个答案,将相同的源代码提供给 C 编译器(或 C++ 编译器)会产生与将其提供给 Glen 的大脑截然不同的输出。在源代码级别的任何替代方案之间没有区别,没有“优化”潜力。只需使用最易读的那个(大概是 if/else 那个)。
@是的。编译器要么可能将两种变体优化为最快的版本,要么可能增加额外的开销,远远超过两者之间的差异。或两者。
假设它“不一定是 C”似乎是一个明智的选择,因为这个问题被标记为 C++(不幸的是,它没有设法声明所涉及的变量的类型)。