ChatGPT解决这个技术问题 Extra ChatGPT

if (a < 901)if (a <= 900) 快吗?

与这个简单的示例不完全一样,但循环复杂代码的性能略有变化。我想这必须与生成的机器代码有关,以防万一。

考虑到它的历史意义、答案的质量以及 performance 中的其他热门问题仍然存在这一事实,我认为没有理由关闭这个问题(尤其是不删除,因为投票显示了这一点)。最多它应该被锁定。此外,即使问题本身是错误的/幼稚的,它出现在书中的事实意味着原始错误信息存在于某处的“可靠”来源中,因此这个问题是建设性的,因为它有助于澄清这一点。
你从来没有告诉我们你指的是哪本书。
键入 < 比键入 <= 快两倍。
在 8086 上确实如此。
赞成票的数量清楚地表明,有数百人严重过度优化。

t
tolik518

不,在大多数架构上它不会更快。您没有指定,但在 x86 上,所有积分比较通常都将在两条机器指令中实现:

设置 EFLAGS 的测试或 cmp 指令

还有一条 Jcc(跳转)指令,具体取决于比较类型(和代码布局):

jne - 如果不相等则跳转 --> ZF = 0

jz - 如果为零(等于)则跳转 --> ZF = 1

jg - 如果大于则跳转 --> ZF = 0 且 SF = OF

(ETC...)

示例(为简洁而编辑)使用 $ gcc -m32 -S -masm=intel test.c 编译

    if (a < b) {
        // Do something 1
    }

编译为:

    mov     eax, DWORD PTR [esp+24]      ; a
    cmp     eax, DWORD PTR [esp+28]      ; b
    jge     .L2                          ; jump if a is >= b
    ; Do something 1
.L2:

    if (a <= b) {
        // Do something 2
    }

编译为:

    mov     eax, DWORD PTR [esp+24]      ; a
    cmp     eax, DWORD PTR [esp+28]      ; b
    jg      .L5                          ; jump if a is > b
    ; Do something 2
.L5:

因此,两者之间的唯一区别是 jgjge 指令。两者将花费相同的时间。

我想解决没有任何迹象表明不同的跳转指令需要相同的时间的评论。这个问题有点难以回答,但我可以给出以下几点:在 Intel Instruction Set Reference 中,它们都被组合在一个共同的指令 Jcc 下(如果满足条件则跳转)。在附录 C. 延迟和吞吐量中的 Optimization Reference Manual 下进行了相同的分组。

延迟 — 执行内核完成构成指令的所有微指令的执行所需的时钟周期数。

吞吐量——在发布端口可以再次自由地接受相同指令之前需要等待的时钟周期数。对于许多指令,一条指令的吞吐量可能远低于其延迟

Jcc 的值为:

      Latency   Throughput
Jcc     N/A        0.5

Jcc 上有以下脚注:

条件跳转指令的选择应基于第 3.4.1 节“分支预测优化”的建议,以提高分支的可预测性。当分支预测成功时,jcc 的延迟实际上为零。

因此,英特尔文档中的任何内容都不会将一条 Jcc 指令与其他指令区别对待。

如果考虑用于实现指令的实际电路,可以假设在 EFLAGS 的不同位上会有简单的 AND/OR 门,以确定是否满足条件。因此,一条指令测试两位的时间没有理由比一条只测试一位的指令花费更多或更少的时间(忽略门传播延迟,它远小于时钟周期。)

编辑:浮点

这也适用于 x87 浮点:(与上面的代码几乎相同,但使用 double 而不是 int。)

        fld     QWORD PTR [esp+32]
        fld     QWORD PTR [esp+40]
        fucomip st, st(1)              ; Compare ST(0) and ST(1), and set CF, PF, ZF in EFLAGS
        fstp    st(0)
        seta    al                     ; Set al if above (CF=0 and ZF=0).
        test    al, al
        je      .L2
        ; Do something 1
.L2:

        fld     QWORD PTR [esp+32]
        fld     QWORD PTR [esp+40]
        fucomip st, st(1)              ; (same thing as above)
        fstp    st(0)
        setae   al                     ; Set al if above or equal (CF=0).
        test    al, al
        je      .L5
        ; Do something 2
.L5:
        leave
        ret

@Dyppl 实际上 jgjnle 是相同的指令,7F :-)
更不用说如果一个选项确实比另一个更快,优化器可以修改代码。
仅仅因为某事导致相同数量的指令并不一定意味着执行所有这些指令的总时间将相同。实际上可以更快地执行更多指令。每个周期的指令数不是固定的,它取决于指令。
@jontejj 我非常清楚这一点。你甚至读过我的回答吗?我没有说明相同数量的指令,我说它们被编译成基本上完全相同的指令,除了一条跳转指令正在查看一个标志,而另一条跳转指令正在查看两个标志。我相信我已经提供了足够的证据来证明它们在语义上是相同的。
@jontejj 你说得很好。为了尽可能多地了解这个答案,我可能应该对其进行一些清理。感谢您的反馈。
P
Peter Mortensen

从历史上看(我们谈论的是 1980 年代和 1990 年代初期),有一些架构确实如此。根本问题是整数比较本质上是通过整数减法实现的。这导致了以下情况。

Comparison     Subtraction
----------     -----------
A < B      --> A - B < 0
A = B      --> A - B = 0
A > B      --> A - B > 0

现在,当 A < B 减法必须借一个高位以使减法正确时,就像手动加减时进位和借位一样。这个“借来的”位通常被称为进位位,并且可以通过分支指令进行测试。如果减法相同为零(这意味着相等),则将设置称为 零位 的第二位。

通常至少有两条条件分支指令,一条在进位位上分支,一条在零位上。

现在,为了了解问题的核心,让我们扩展上表以包括进位和零位结果。

Comparison     Subtraction  Carry Bit  Zero Bit
----------     -----------  ---------  --------
A < B      --> A - B < 0    0          0
A = B      --> A - B = 0    1          1
A > B      --> A - B > 0    1          0

因此,可以在一条指令中实现 A < B 的分支,因为在这种情况下,进位位是 only 清零的,也就是说,

;; Implementation of "if (A < B) goto address;"
cmp  A, B          ;; compare A to B
bcz  address       ;; Branch if Carry is Zero to the new address

但是,如果我们想要进行小于或等于比较,我们需要对零标志进行额外检查以捕捉相等的情况。

;; Implementation of "if (A <= B) goto address;"
cmp A, B           ;; compare A to B
bcz address        ;; branch if A < B
bzs address        ;; also, Branch if the Zero bit is Set

因此,在某些机器上,使用“小于”比较可能会节省一条机器指令。这在亚兆赫处理器速度和 1:1 CPU 与内存速度比的时代是相关的,但在今天几乎完全无关紧要。


此外,像 x86 这样的架构实现了像 jge 这样的指令,这些指令同时测试零和符号/进位标志。
即使对于给定的架构是正确的。编译器编写者从未注意到并添加优化以将较慢替换为较快的可能性是多少?
这在 8080 上是正确的。它有指令跳到零和跳到负,但没有一个可以同时测试两者。
6502 和 65816 处理器系列也是如此,它也扩展到了摩托罗拉 68HC11/12。
即使在 8080 上,也可以在 one 指令中通过交换操作数和测试 not < 来实现 <= 测试(相当于 >=)这是具有交换操作数的所需 <=cmp B,A; bcs addr。这就是英特尔省略了这个测试的原因,他们认为它是多余的,你当时买不起多余的指令:-)
D
David Schwartz

假设我们谈论的是内部整数类型,那么一种方法不可能比另一种更快。它们显然在语义上是相同的。他们都要求编译器做同样的事情。只有严重损坏的编译器会为其中之一生成劣质代码。

如果在某些平台上,对于简单的整数类型,<<= 快,那么编译器应该总是<= 转换为 < 以获得常量。任何没有的编译器都只是一个糟糕的编译器(对于那个平台)。


+1 我同意。 <<= 都没有速度,直到编译器决定它们的速度。当您考虑到编译器通常已经执行死代码优化、尾调用优化、循环提升(和展开,有时)、各种循环的自动并行化等时,这是一个非常简单的编译器优化... 为什么要浪费时间考虑过早的优化? 让原型运行,对其进行分析以确定最重要的优化在哪里,按重要性顺序执行这些优化,并在测量进度的过程中再次进行分析......
仍然存在一些边缘情况,其中具有一个常数值的比较在 <= 下可能会变慢,例如,当从 (a < C)(a <= C-1) 的转换(对于某些常数 C)导致 C 更加困难在指令集中进行编码。例如,在比较中,指令集可能能够以紧凑的形式表示从 -127 到 128 的有符号常量,但超出该范围的常量必须使用更长、更慢的编码或完全使用另一条指令来加载。所以像 (a < -127) 这样的比较可能没有直接的转换。
@BeeOnRope问题不在于执行由于具有不同常量而不同的操作是否会影响性能,而是使用不同常量表示 相同操作是否会影响性能。因此,我们不会将 a > 127a > 128 进行比较,因为在那里您别无选择,您可以使用您需要的那个。我们将 a > 127a >= 128 进行比较,它们不需要不同的编码或不同的指令,因为它们具有相同的真值表。一个的任何编码同样是另一个的编码。
我以一般方式回应您的陈述,即“如果在某些平台 [<= 较慢],编译器应始终将 <= 转换为 < 用于常量”。据我所知,这种转变涉及改变常数。例如,a <= 42 被编译为 a < 43,因为 < 更快。在某些极端情况下,这样的转换不会有成效,因为新常量可能需要更多或更慢的指令。当然 a > 127a >= 128 是等价的,编译器应该以(相同)最快的方式对这两种形式进行编码,但这与我所说的并不矛盾。
P
Peter Mortensen

我看到两者都不是更快。编译器在每个条件下生成具有不同值的相同机器代码。

if(a < 901)
cmpl  $900, -4(%rbp)
jg .L2

if(a <=901)
cmpl  $901, -4(%rbp)
jg .L3

我的示例 if 来自 Linux 上 x86_64 平台上的 GCC。

编译器编写者是非常聪明的人,他们会想到这些事情以及我们大多数人认为理所当然的许多其他事情。

我注意到如果它不是一个常数,那么在任何一种情况下都会生成相同的机器代码。

int b;
if(a < b)
cmpl  -4(%rbp), %eax
jge   .L2

if(a <=b)
cmpl  -4(%rbp), %eax
jg .L3

请注意,这是特定于 x86 的。
我认为您应该使用该 if(a <=900) 来证明它生成完全相同的 asm :)
@AdrianCornish对不起..我编辑了它..它或多或少是一样的..但是如果你将第二个更改为<=900,那么asm代码将完全相同:)现在几乎一样了..但是你知道..对于强迫症:)
@Boann 这可能会减少到 if (true) 并完全消除。
没有人指出这种优化只适用于不断的比较。我可以保证不会像这样比较两个变量。
r
ridiculous_fish

对于浮点代码,即使在现代架构上, <= 比较也可能确实更慢(通过一条指令)。这是第一个功能:

int compare_strict(double a, double b) { return a < b; }

在 PowerPC 上,首先执行浮点比较(更新 cr,条件寄存器),然后将条件寄存器移动到 GPR,将“比较小于”位移动到位,然后返回。它需要四个指令。

现在考虑这个函数:

int compare_loose(double a, double b) { return a <= b; }

这需要与上面的 compare_strict 相同的工作,但现在有两个感兴趣的部分:“小于”和“等于”。这需要一条额外的指令(cror - 条件寄存器按位或)将这两位合并为一个。所以 compare_loose 需要五个指令,而 compare_strict 需要四个。

你可能认为编译器可以像这样优化第二个函数:

int compare_loose(double a, double b) { return ! (a > b); }

但是,这将错误地处理 NaN。 NaN1 <= NaN2NaN1 > NaN2 都需要评估为 false。


幸运的是,它在 x86 (x87) 上不能这样工作。 fucomip 设置 ZF 和 CF。
@JonathonReinhart:我认为您误解了 PowerPC 正在做什么——条件寄存器 cr is 相当于 x86 上的 ZFCF 等标志。 (虽然 CR 更灵活。)发帖人所说的是将结果移动到 GPR:在 PowerPC 上需要两条指令,但 x86 有条件移动指令。
@DietrichEpp 我的意思是在我的声明之后添加的是:然后您可以根据 EFLAGS 的值立即跳转。抱歉不清楚。
@JonathonReinhart:是的,您也可以根据 CR 的值立即跳转。答案不是在谈论跳跃,这是额外指令的来源。
g
glglgl

也许那本无名书的作者读到 a > 0 的运行速度比 a >= 1 快,并认为这是普遍适用的。

但这是因为涉及 0(因为 CMP 可以根据架构替换为例如 OR)而不是因为 <


当然,在“调试”构建中,但如果要使 (a >= 1) 运行得比 (a > 0) 慢,则需要一个糟糕的编译器,因为优化器可以将前者简单地转换为后者。
@BeeOnRope 有时我很惊讶优化器可以优化哪些复杂的事情,以及它无法优化哪些简单的事情。
确实,检查 asm 输出的少数几个重要的函数总是值得的。也就是说,上述转换是非常基本的,甚至在简单的编译器中已经执行了几十年。
E
Eliot Ball

至少,如果这是真的,编译器可以简单地将 a <= b 优化为 !(a > b),因此即使比较本身实际上更慢,除了最天真的编译器之外,您不会注意到任何差异.


为什么 !(a>b) 是 a <=b 的优化版本。不是 !(a>b) 2 操作合二为一吗?
@AbhishekSingh NOT 只是由其他指令制作(jejne
在 IEEE 浮点中,a<=b!(a>b) 的含义不同。
M
Mark Booth

TL;DR 答案

对于体系结构、编译器和语言的大多数组合,< 不会比 <= 快。

完整答案

其他答案集中在 x86 架构上,我不知道 ARM 架构(您的示例汇编器似乎是)足以专门评论生成的代码,但这是 micro-optimisation 的示例 非常特定于架构,并且既可能是反优化,也可能是优化

因此,我建议这种 micro-optimisationcargo cult 编程的一个示例,而不是最佳软件工程实践。

反例

可能有一些架构可以优化,但我知道至少有一种架构可能相反。古老的 Transputer 架构只有 等于大于或等于 的机器代码指令,因此所有比较都必须从这些原语构建。

即便如此,在几乎所有情况下,编译器都可以以这样一种方式对评估指令进行排序,即在实践中,没有任何比较比其他任何比较有任何优势。但最坏的情况是,它可能需要添加反向指令 (REV) 来交换 operand stack 上的前两项。这是一个单字节指令,需要一个周期才能运行,因此开销可能最小。

概括

像这样的微优化是优化还是反优化取决于您使用的特定架构,因此养成使用特定架构微优化的习惯通常是个坏主意,否则您可能会本能地在不合适的时候使用一个,看起来这正是你正在阅读的书所提倡的。


P
Peter Mortensen

它们具有相同的速度。也许在某些特殊的架构中,他/她说的是对的,但在 x86 家族中,至少我知道它们是相同的。因为为此,CPU 将执行减法 (a - b),然后检查标志寄存器的标志。该寄存器的两位称为 ZF(零标志)和 SF(符号标志),它在一个周期内完成,因为它将通过一次掩码操作完成。


T
Telgin

这将高度依赖于 C 编译到的底层架构。一些处理器和体系结构可能具有明确的等于、小于和等于指令,它们以不同的周期数执行。

不过,这将是非常不寻常的,因为编译器可以解决它,使其无关紧要。


如果周期有差异。 1)它不会被检测到。 2) 任何称职的编译器都会在不改变代码含义的情况下从慢速形式转换为快速形式。因此,种植的最终指令将是相同的。
完全同意,无论如何,这将是一个非常微不足道和愚蠢的区别。在一本应该与平台无关的书中当然没有什么可提及的。
@lttlrck:我明白了。花了我一段时间(愚蠢的我)。不,它们是不可检测的,因为发生了许多其他事情,使它们的测量变得不可能。处理器停顿/缓存未命中/信号/进程交换。因此,在正常的操作系统情况下,单周期级别的事情无法进行物理测量。如果您可以消除测量中的所有干扰(在具有板载内存且没有操作系统的芯片上运行它),那么您仍然需要担心计时器的粒度,但理论上如果您运行足够长的时间,您可以看到一些东西。
s
shinkou

即使有任何差异,您也不应该注意到差异。此外,在实践中,您必须额外执行 a + 1a - 1 才能使条件成立,除非您要使用一些魔术常数,这无论如何都是一个非常糟糕的做法。


有什么不好的做法?增加或减少计数器?那么如何存储索引符号呢?
他的意思是,如果您要比较两种变量类型。当然,如果您要为循环或其他东西设置值,那是微不足道的。但是如果你有 x <= y,并且 y 是未知的,那么将它“优化”到 x < y + 1 会更慢
@JustinDanielson 同意。更不用说丑陋,混乱等。
P
Peter Cordes

当我写这个答案的第一个版本时,我只是在看关于 < 的标题问题。 vs. <= 一般而言,不是常量 a < 901a <= 900 的具体示例。许多编译器总是通过在 <<= 之间进行转换来缩小常量的大小,例如,因为 x86 立即操作数对 -128..127 有更短的 1 字节编码。

对于 ARM,能够编码为立即数取决于能够将窄域旋转到单词中的任何位置。所以 cmp r0, #0x00f000 是可编码的,而 cmp r0, #0x00efff 则不是。因此,用于比较与编译时常量的缩小规则并不总是适用于 ARM。与 32 位 ARM 和 Thumb 模式不同,对于 cmpcmn 等指令,AArch64 要么移位 12 位,要么不移位,而不是任意旋转。

< 与 <= 通常,包括运行时变量条件

在大多数机器上的汇编语言中,比较 <= 的成本与比较 < 的成本相同。无论您是在其上进行分支、对其进行布尔化以创建 0/1 整数,还是将其用作无分支选择操作的谓词(如 x86 CMOV),这都适用。其他答案只解决了这部分问题。

但这个问题是关于 C++ 运算符,即优化器的输入 通常它们都同样有效;书中的建议听起来完全是假的,因为编译器总是可以转换他们在 asm 中实现的比较。但至少有一个例外,使用 <= 可能会意外创建编译器无法优化的内容。

作为一个循环条件,当 <= 阻止编译器证明循环不是无限的时,在某些情况下,<= 在质量上不同于 < 这可以有很大的不同,禁用自动矢量化。

与有符号溢出 (UB) 不同,无符号溢出被明确定义为 base-2 环绕。有符号循环计数器通常不会出现这种情况,因为编译器不会基于有符号溢出 UB 进行优化:++i <= size 最终总是会变为假。 (What Every C Programmer Should Know About Undefined Behavior)

void foo(unsigned size) {
    unsigned upper_bound = size - 1;  // or any calculation that could produce UINT_MAX
    for(unsigned i=0 ; i <= upper_bound ; i++)
        ...

编译器只能以为所有可能的输入值保留 C++ 源代码的(已定义且可合法观察的)行为的方式进行优化,导致未定义行为的除外。

(一个简单的 i <= size 也会产生问题,但我认为计算上限是一个更现实的例子,它意外地为您不关心但编译器必须考虑的输入引入了无限循环的可能性。)

在这种情况下,size=0 导致 upper_bound=UINT_MAX,并且 i <= UINT_MAX 始终为真。因此,对于 size=0,这个循环是无限的,编译器必须尊重这一点,即使您作为程序员可能从不打算传递 size=0。如果编译器可以将此函数内联到一个调用者中,它可以证明 size=0 是不可能的,那么很好,它可以像对 i < size 一样进行优化。

如果循环 (Why are loops always compiled into "do...while" style (tail jump)?) 内不需要 i 的实际值,则像 if(!size) skip the loop; do{...}while(--size); 这样的 Asm 是优化 for( i<size ) 循环的一种通常有效的方法。

但这不会是无限的:如果使用 size==0 输入,我们将获得 2^n 次迭代。 (Iterating over all unsigned integers in a for loop C 可以在包括零在内的所有无符号整数上表示循环,但是没有进位标志就不容易,就像在 asm 中那样。)

由于循环计数器的环绕是可能的,现代编译器通常只是“放弃”,并且几乎没有积极地优化。

示例:从 1 到 n 的整数之和

使用无符号 i <= n 会破坏基于 Gauss 的 n * (n+1) / 2 公式优化具有封闭形式的 sum(1 .. n) 循环的 clang 习语识别。

unsigned sum_1_to_n_finite(unsigned n) {
    unsigned total = 0;
    for (unsigned i = 0 ; i < n+1 ; ++i)
        total += i;
    return total;
}

x86-64 asm from clang7.0 and gcc8.2 on the Godbolt compiler explorer

 # clang7.0 -O3 closed-form
    cmp     edi, -1       # n passed in EDI: x86-64 System V calling convention
    je      .LBB1_1       # if (n == UINT_MAX) return 0;  // C++ loop runs 0 times
          # else fall through into the closed-form calc
    mov     ecx, edi         # zero-extend n into RCX
    lea     eax, [rdi - 1]   # n-1
    imul    rax, rcx         # n * (n-1)             # 64-bit
    shr     rax              # n * (n-1) / 2
    add     eax, edi         # n + (stuff / 2) = n * (n+1) / 2   # truncated to 32-bit
    ret          # computed without possible overflow of the product before right shifting
.LBB1_1:
    xor     eax, eax
    ret

但是对于幼稚的版本,我们只是从 clang 中得到一个愚蠢的循环。

unsigned sum_1_to_n_naive(unsigned n) {
    unsigned total = 0;
    for (unsigned i = 0 ; i<=n ; ++i)
        total += i;
    return total;
}
# clang7.0 -O3
sum_1_to_n(unsigned int):
    xor     ecx, ecx           # i = 0
    xor     eax, eax           # retval = 0
.LBB0_1:                       # do {
    add     eax, ecx             # retval += i
    add     ecx, 1               # ++1
    cmp     ecx, edi
    jbe     .LBB0_1            # } while( i<n );
    ret

GCC 不使用任何一种封闭形式,所以循环条件的选择并没有真正伤害它;它使用 SIMD 整数加法自动矢量化,在 XMM 寄存器的元素中并行运行 4 个 i 值。

# "naive" inner loop
.L3:
    add     eax, 1       # do {
    paddd   xmm0, xmm1    # vect_total_4.6, vect_vec_iv_.5
    paddd   xmm1, xmm2    # vect_vec_iv_.5, tmp114
    cmp     edx, eax      # bnd.1, ivtmp.14     # bound and induction-variable tmp, I think.
    ja      .L3 #,       # }while( n > i )

 "finite" inner loop
  # before the loop:
  # xmm0 = 0 = totals
  # xmm1 = {0,1,2,3} = i
  # xmm2 = set1_epi32(4)
 .L13:                # do {
    add     eax, 1       # i++
    paddd   xmm0, xmm1    # total[0..3] += i[0..3]
    paddd   xmm1, xmm2    # i[0..3] += 4
    cmp     eax, edx
    jne     .L13      # }while( i != upper_limit );

     then horizontal sum xmm0
     and peeled cleanup for the last n%3 iterations, or something.
     

它还有一个简单的标量循环,我认为它用于非常小的 n 和/或无限循环情况。

顺便说一句,这两个循环都在循环开销上浪费了一条指令(以及 Sandybridge 系列 CPU 上的一个微指令)。 sub eax,1/jnz 而不是 add eax,1/cmp/jcc 会更有效。 1 uop 而不是 2(在 sub/jcc 或 cmp/jcc 的宏融合之后)。两个循环之后的代码无条件地写入 EAX,因此它没有使用循环计数器的最终值。


好人为的例子。关于 EFLAGS 使用对乱序执行的潜在影响,您的其他评论如何? JB 会导致比 JBE 更好的流水线,这纯粹是理论上的还是真的会发生?
@rustyx:我在另一个答案下的某个地方评论过吗?编译器不会发出导致部分标志停止的代码,当然也不会针对 C <<=。但可以肯定的是,如果设置了 ZF (ecx==0) 或设置了 CF(EAX==1 的第 3 位),test ecx,ecx / bt eax, 3 / jbe 将跳转,导致大多数 CPU 上的部分标志停止因为它读取的标志并非全部来自写入任何标志的最后一条指令。在 Sandybridge-family 上,它实际上并没有停止,只需要插入一个合并的 uop。 cmp/test 写入所有标志,但 bt 保持 ZF 不变。 felixcloutier.com/x86/bt
据我了解,AArch64 上 cmp 的可用立即数比您的回答听起来更简单:它需要一个 12 位立即数可选地移位 12 位,因此您可以使用 0xYYY0xYYY000,并且您也可以通过使用 cmn 来有效地否定立即数。这仍然支持您的观点,因为 cmp w0, #0xf000 是可编码的,而 cmp w0, #0xefff 不是。但是“旋转到任何位置”的措辞听起来更像是对“位掩码”立即数的描述,AFAIK 仅适用于按位逻辑指令:and, or, eor 等。
@NateEldredge:我认为我的描述非常适合 ARM 模式,它是一个 8 位字段,旋转了 2 的倍数。(所以 0x1fe 不可编码,但 0xff0 是。)当我写这个时,我没有t 了解 AArch64 和 ARM 立即数之间的区别,或者只有按位布尔 insns 可以使用位范围/重复位模式编码。 (并且 mov; or 零注册是利用这些编码的一种方法。)
E
Ecksters

您可以说该行在大多数脚本语言中是正确的,因为多余的字符会导致代码处理速度稍慢。但是,正如最重要的答案所指出的那样,它在 C++ 中应该没有效果,并且使用脚本语言所做的任何事情都可能并不关心优化。


我有些不同意。在竞争性编程中,脚本语言通常为问题提供最快的解决方案,但必须应用正确的技术(阅读:优化)才能获得正确的解决方案。
P
Puddle

仅当创建计算机的人不擅长布尔逻辑时。他们不应该这样。

每个比较 (>= <= > <) 都可以以相同的速度完成。

每一个比较是什么,只是一个减法(差异),看看它是正还是负。
(如果设置了 msb,则数字为负)

如何检查a >= b? Sub a-b >= 0 检查 a-b 是否为正。
如何检查 a <= b? Sub 0 <= b-a 检查 b-a 是否为正。
如何检查 a < b? Sub a-b < 0 检查 a-b 是否为负。
如何检查 a > b?子 0 > b-a 检查 b-a 是否为负。

简而言之,计算机可以在给定操作的底层执行此操作:

a >= b == msb(a-b)==0
a <= b == msb(b-a)==0
a > b == msb(b-a)==1
a < b == msb(a-b)==1

当然,计算机实际上也不需要执行 ==0==1
对于 ==0,它只需将电路中的 msb 反转即可。

无论如何,他们肯定不会将 a >= b 计算为 a>b || a==b 哈哈


不过,事情没那么简单。例如,如果 a 在寄存器中并且 b 是编译时常量,则 x86 可以在一条指令(sub rax, 12345cmp)中计算 a-b,但不能在 b-a 中计算。 reg - imm 有一条指令,但反之则不然。许多其他机器也有类似的情况。
这整个答案是错误的。例如,INT_MAX > INT_MIN,但 INT_MIN - INT_MAX 为 1(在机器代码级别,而不是在 C 中),其 msb 为 0。正确进行这些比较比您想象的要难。
@benrg 显然,在代码中实现并不像实际电路那样简单,可以访问进位/溢出。这个答案是为了解释不同比较的逻辑。 (这都是关于差异的。它是符号)它显然会在 ALU 中处理。 (也适用于不同类型,如浮点数)参见 this。所以答案没有错。你提出的是整数的限制。如果我们将 INT_MIN 放入 long 中,它将存储 ffffffff80000000。做减法会得到 ffffffff00000001。 (否定)
h
huseyin tugrul buyukisik

仅当计算路径取决于数据时:

a={1,1,1,1,1000,1,1,1,1}
while (i<=4)
{
     for(j from 0 to a[i]){ do_work(); }
     i++;
}

将计算 250 倍于 while(i<4)

真实世界的样本将是计算 mandelbrot 集。如果包含一个迭代 1000000 次的像素,它会导致滞后,但与 <= 使用概率的重合度太低。