在从高性能代码中删除条件分支时,将真实布尔值转换为unsigned long i = -1
以设置所有位可能很有用。
我想出了一种从int b
(或bool b
)的输入获取此整数掩码布尔值的方法,其值为1
或0
:< / p>
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。听起来很可能编译器不会在这里给cpu带来负担。
答案 0 :(得分:5)
你应该问自己的问题是:如果你写:
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
你能明白为什么会这样吗?在{2}的补码中,-X
只是另一种说2n - X
的方式,其中n
是单词中的位数。事实上,这就是为什么它被称为“2的补充”。那么~X
呢?好吧,我们可以认为这是从2的相应幂中减去X中的每一位的结果。例如,如果我们的单词中有四位,X
是0101
(这是5,或2 2 + 2 0 ),然后~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
为true;相反,它使用所有1
s的掩码。为什么?因为程序员可能接下来会对比较结果做的事情就是使用它来掩盖值,我认为这正是你计划用你的-(!B)
技巧做的事情,除了并行流媒体版。
所以,请放心,它已被覆盖。
答案 1 :(得分:1)
以前有人看过这个建筑吗?我能做些什么吗?
很多人都看过了。它像岩石一样古老。这并不罕见,但您应该将其封装在内联函数中,以避免混淆代码。
并且,验证您的编译器实际上是在旧代码上生成分支,并且它是否已正确配置,并且此微优化实际上可以提高性能。 (并且记录每个优化策略削减的时间是个好主意。)
从好的方面来看,它完全符合标准。
当int值-1(我假设-b或 - (!b)确实产生)被提升为更大的unsigned int类型时,是否可以保证设置所有位?
不,因为无符号数总是正数,所以转换-1
的结果并不特殊,不会扩展为更多。
如果你有不同的尺码并希望成为肛门,试试这个:
template< typename uint >
uint mask_cast( bool f )
{ return static_cast< uint >( - ! f ); }
答案 2 :(得分:0)
struct full_mask {
bool b;
full_mask(bool b_):b(b_){}
template<
typename int_type,
typename=typename std::enable_if<std::is_unsigned<int_type>::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代码来检测我转换为的类型是无符号整数类型。
答案 3 :(得分:0)
只需递减即可将1/0转换为0 / -1。这反转了布尔条件,但是如果你可以在第一个位置生成布尔值的倒数,或者使用掩码的逆,那么它只是一个操作而不是两个操作。