你应该问自己的问题是:如果你写:
int it_was_true = b > c;
那么it_was_true
将是 1 或 0。但是那个 1 是从哪里来的呢?
机器的指令集不包含以下形式的指令:
Compare R1 with R2 and store either 1 or 0 in R3
或者,事实上,类似的东西。(我在这个答案的末尾写了一个关于 SSE 的注释,说明前一个陈述并不完全正确。)机器有一个内部条件寄存器,由几个条件位和比较指令 - 以及其他一些算术运算——导致以特定方式修改这些条件位。随后,您可以根据一些条件位或条件加载,有时还有其他条件操作,执行条件分支。
所以实际上,将 1 存储在变量中的效率可能比直接执行一些条件操作要低得多。可能是,但可能不是,因为编译器(或者至少是编写编译器的人)可能比你聪明,它可能只记得它应该把 1 放进去it_was_true
,这样当你真正绕过时要检查该值,编译器可以发出适当的分支或其他任何东西。
所以,说到聪明的编译器,你应该仔细看看以下产生的汇编代码:
uint64_t ffz_flipped = ~i&~(~i-1);
看看那个表达式,我可以数出五个运算:三个按位否定、一个按位合取 ( and
) 和一个减法。但是您不会在汇编输出中找到五个操作(至少,如果您使用 gcc -O3)。你会找到三个。
在我们查看汇编输出之前,让我们做一些基本的代数。这是最重要的身份:
-X == ~X + 1
你能明白为什么这是真的吗?-X
,在 2 的补码中,只是另一种说法,其中是单词中的位数。事实上,这就是为什么它被称为“2 的补码”。怎么样?好吧,我们可以把它看作是从 2 的相应幂中减去 X 中的每一位的结果。例如,如果我们的字中有四位,并且是(即 5,或 2 2 + 2 0), then是我们可以认为的,与 完全一样。或者,换句话说:2n - X
n
~X
X
0101
~X
1010
23×(1-0) + 22×(1-1) + 21×(1-0) + 20×(1-1)
1111 − 0101
−X == 2n − X
~X == (2n−1) − X
意思就是
~X == (−X) − 1
请记住,我们有
ffz_flipped = ~i&~(~i-1);
但是我们现在知道我们可以将 ~(~i−1) 变成minus
运算:
~(~i−1)
== −(~i−1) − 1
== −(−i - 1 - 1) − 1
== (i + 2) - 1
== i + 1
多么酷啊!我们可以这样写:
ffz_flipped = ~i & (i + 1);
这只有三个操作,而不是五个。
现在,我不知道你是否遵循了这一点,我花了一些时间才把它弄好,但现在让我们看看 gcc 的输出:
leaq 1(%rdi), %rdx # rdx = rdi + 1
movq %rdi, %rax # rax = rdi
notq %rax # rax = ~rax
andq %rax, %rdx # rdx &= rax
所以 gcc 只是自己解决了所有这些问题。
关于 SSE 的承诺说明:事实证明,SSE 可以进行并行比较,甚至可以在两个 16 字节寄存器之间一次进行 16 字节比较。条件寄存器不是为此而设计的,无论如何,没有人愿意在不需要时进行分支。因此,CPU 确实将其中一个 SSE 寄存器(一个 16 字节的向量,或 8 个“字”或 4 个“双字”,无论操作怎么说)更改为一个布尔指示符向量。但它并不1
适用于真实;相反,它使用所有1
s 的掩码。为什么?因为很可能程序员接下来对比较结果要做的事情就是用它来屏蔽值,我认为这正是你计划用你的-(!B)
技巧做的事情,除了在并行流版本中。
所以,请放心,它已经被覆盖了。