【问题标题】:Ternary operator vs array?三元运算符与数组?
【发布时间】:2021-05-12 07:08:24
【问题描述】:

在 C 中,索引数组是否比 ?: 运算符更快?

例如,(const int[]){8, 14}[N > 10] 会比 N > 10? 14 : 8 快​​吗?

【问题讨论】:

  • 你编译它并查看生成了什么代码?我认为一般没有办法回答你的问题。
  • 你为什么不测量它或检查生成的代码?
  • 使用三元运算符而不是if/else 已经够糟糕了,但(const int[]){8, 14}[N > 10] 只适用于IOCCC
  • 福特和雪佛兰哪个更好? ——也就是说,这种问题几乎是完全无法回答的。如果您在意,则必须使用编译器在您的机器上进行测量。通常,差异是如此微小,您必须进行数百万次试验才能获得具有统计学意义的结果。通常,差异会很小,以至于毫无意义。最后,任何差异通常都无关紧要,因为程序的实际性能受其他问题支配。这种微优化很少重要。 (虽然,这是真的,但有时确实如此。)
  • 什么,不是8 + 6*(N>10)?还是fma(6, N>10, 8)

标签: arrays c performance conditional-operator micro-optimization


【解决方案1】:

坚持使用三元运算符:

  • 更简单
  • 输入的字符更少
  • 更易于阅读和理解
  • 更易于维护
  • 这可能不是您的应用程序的主要瓶颈
  • 对于 CPU,这是一个简单的比较
  • 编译器很聪明,如果数组解决方案更快,编译器就会为两种变体生成相同的代码

强制引用(强调我的):

程序员会浪费大量时间来思考或担心程序中非关键部分的速度,而这些提高效率的尝试实际上在考虑调试和维护时会产生强烈的负面影响。 我们应该忘记小的效率,比如大约 97% 的时间:过早优化是万恶之源。然而,我们不应该放弃那关键的 3% 的机会

——唐纳德·克努斯•https://wiki.c2.com/?PrematureOptimization


现在不碍事了,让我们比较一下编译器实际产生的结果。

#include <stdlib.h>
int ternary(int n) { return n > 10 ? 14 : 8; }
int array(int n) { return (const int[]){8, 14}[n > 10]; }

在 Ubuntu 中使用 (g)cc 10.2.1 编译并启用优化:

$ cc -O3 -S -fno-stack-protector -fno-asynchronous-unwind-tables ternary.c

-S 在编译后停止并且不汇编。您最终会得到一个包含生成的汇编代码的.s 文件。 (-fno… 标志用于禁用我们的示例不需要的其他代码生成)。

ternary.s 汇编代码,与删除的方法无关的行:

ternary:
    endbr64
    cmpl    $10, %edi
    movl    $8, %edx
    movl    $14, %eax
    cmovle  %edx, %eax
    ret
array:
    endbr64
    movq    .LC0(%rip), %rax
    movq    %rax, -8(%rsp)
    xorl    %eax, %eax
    cmpl    $10, %edi
    setg    %al
    movl    -8(%rsp,%rax,4), %eax
    ret
.LC0:
    .long   8
    .long   14

如果您比较它们,您会发现数组版本的指令要多得多:6 条指令与 4 条指令。 没有理由编写每个开发人员必须阅读两次的更复杂的代码;更短更直接的代码编译成更高效的机器代码。

【讨论】:

  • 适当的 Knuth 报价加 1。
【解决方案2】:

尽管有优化级别,但使用复合文字(以及一般的数组)的效率会低得多,因为数组是由当前的实际编译器创建的。更糟糕的是,它们是在堆栈上创建的,而不仅仅是索引静态常量数据(这仍然会比大多数现代 ISA 拥有的 x86 cmov 或 AArch64 csel 等 ALU 选择操作更慢,至少延迟更高)。

我已经使用我使用的所有编译器(包括 Keil 和 IAR)和一些我不使用的编译器(icc 和 clang)对其进行了测试。

int foo(int N)
{
    return (const int[]){8, 14}[N > 10]; 
}

int bar(int N)
{
    return  N > 10? 14 : 8;
}
foo:
        mov     rax, QWORD PTR .LC0[rip]     # load 8 bytes from .rodata
        mov     QWORD PTR [rsp-8], rax       # store both elements to the stack
        xor     eax, eax                      # prepare a zeroed reg for setcc
        cmp     edi, 10
        setg    al                            # materialize N>10 as a 0/1 integer
        mov     eax, DWORD PTR [rsp-8+rax*4]  # index the array with it
        ret

bar:
        cmp     edi, 10
        mov     edx, 8               # set up registers with both constants
        mov     eax, 14
        cmovle  eax, edx             # ALU select operation on FLAGS from CMP
        ret
.LC0:
        .long   8
        .long   14

https://godbolt.org/z/qK65Gv

【讨论】:

  • 该标准不需要这样的东西,因为复合文字是否实际上是在内存中创建的并不是可观察的行为。
  • 您声称复合文字“必须创建”。正如 Eric 解释的那样,标准中没有这样的要求。您不能从编译器的行为中推断出标准的内容。正确的措辞是“当前的编译器似乎没有意识到这一点”。
  • “我没有时间研究标准”,但您有时间写下声称说明标准要求的答案?编译器只需要生成产生可观察行为的代码这一事实是理解优化的基础。不需要太多的研究;编译器只需要在访问易失性对象、程序执行结束时写入文件的数据以及交互式设备的输入和输出动态(以及“与环境的通信”,C 2018 7.22)中生成与抽象机器匹配的代码.4)。复合文字不在其中。
  • @0___________ 实用的、现实世界的经验主义很好。过度依赖标准确实会变成一种凝视的练习。但是当你说“效率低得多,因为它必须被创建,尽管优化级别”时你会遇到麻烦(并且,我怀疑,邀请反对票) (强调我的)。如果您说“尽管有优化级别,编译器通常会创建它的效率往往较低”,那么您的陈述对 OP 也同样有用,而不会引起鹰眼的愤怒。
  • 我编辑了您的答案,指出问题不仅仅是“创建”数组(我认为您的意思是复制到堆栈),而是使用了负载。以及它正在复制的静态常量上缓存未命中的可能性。这些是 asm 中的 2 个实际选项,因此实际讨论它们的性能是有道理的,而不仅仅是从 asm 中的数组较慢的假设中争论而没有说明这一事实。无论如何,我认为我的编辑解决了以下问题:CPU 具有高效的“选择”指令。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-10-31
  • 2020-10-25
  • 1970-01-01
  • 1970-01-01
  • 2023-01-02
  • 2017-09-27
  • 2011-06-17
相关资源
最近更新 更多