是 – !(条件)从boolean(mask-boolean)获取完整位向量的正确方法?

在从高性能代码中删除条件分支时,将真实布尔值转换为unsigned long i = -1以设置所有位可能很有用。

我想出了一种从int b (或bool b )的输入获取此integer-mask-boolean的方法,取值为10

 unsigned long boolean_mask = -(!b); 

获得相反的价值:

 unsigned long boolean_mask = -b; 

以前有人看过这个建筑吗? 我有事吗? 当int值-1(我假设-b-(!b)确实生成)被提升为更大的unsigned int类型时,它是否保证设置所有位?

这是上下文:

 uint64_t ffz_flipped = ~i&~(~i-1); // least sig bit unset // only set our least unset bit if we are not pow2-1 i |= (ffz_flipped < i) ? ffz_flipped : 0; 

我会在下次问这样的问题之前检查生成的asm。 听起来很可能编译器不会在这里给分支带来负担。

你应该问自己的问题是:如果你写:

 int it_was_true = b > c; 

然后it_was_true将是1或0. 但是那个1来自哪里?

机器的指令集不包含以下forms的指令:

 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 n - X另一种方式,其中n是字中的位数。 事实上,这就是为什么它被称为“2的补充”。 ~X怎么样? 好吧,我们可以把它当作从2的相应幂中减去X中的每一位的结果。例如,如果我们的字中有四位,而X0101 (即5或2 2 + 2 0) ),那么~X1010 ,我们可以认为它是2 3 ×(1- 0 ) + 2 2 ×(1- 1 ) + 2 1 ×(1- 0 ) + 2 0 ×(1- 1 ) ,其中与1111 − 0101完全相同。 或者,换句话说:

−X == 2 n − X
~X == (2 n −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的承诺说明:事实certificate,SSE可以进行并行比较,甚至可以在两个16字节寄存器之间进行16字节比较。 条件寄存器不是为此而设计的,无论如何,没有人想要在不需要时进行分支。 因此,CPU确实将一个SSE寄存器(一个16字节的矢量,或8个“字”或4个“双字”,无论操作如何)更改为布尔指示符的向量。 但它并不使用1表示真实; 相反,它使用了所有1秒的掩码。 为什么? 因为程序员可能会对该比较结果做下一件事就是使用它来掩盖值,我认为这正是你计划用你的-(!B)技巧做的,除了并行流媒体版。

所以,请放心,它已被覆盖。

以前有人看过这个建筑吗? 我有事吗?

很多人都看过了。 它像岩石一样古老。 这并不罕见,但您应该将其封装在内联函数中,以避免混淆代码。

并且,validation您的编译器实际上是在旧代码上生成分支,并且它是否已正确配置,并且此微优化实际上可以提高性能。 (并且记录每个优化策略削减的时间是个好主意。)

从好的方面来看,它完全符合标准。

当int值-1(我假设-b或 – (!b)确实生成)被提升为更大的unsigned int类型时,它是否保证设置所有位?

不,因为无符号数总是正数,所以转换-1的结果不是特殊的,并且不会用更多的数字扩展。

如果你有不同的尺寸,并希望成为肛门,试试这个:

 template< typename uint > uint mask_cast( bool f ) { return static_cast< uint >( - ! f ); } 
 struct full_mask { bool b; full_mask(bool b_):b(b_){} template< typename int_type, typename=typename std::enable_if::value>::type > operator int_type() const { return -b; } }; 

使用:

 unsigned long long_mask = full_mask(b); unsigned char char_mask = full_mask(b); char char_mask2 = full_mask(b); // does not compile 

基本上我使用类full_mask来推断我们正在转换的类型,并自动生成该类型的位填充无符号值。 我扔了一些SFINAE代码来检测我转换为的类型是无符号整数类型。

您可以通过递减将1/0转换为0 / -1。 这反转了布尔条件,但是如果你可以在第一个位置生成布尔值的逆,或者使用掩码的逆,那么它只是一个操作而不是两个操作。