是-!(condition)从布尔值(mask-boolean)中获得完整位向量的正确方法

Is -!(condition) a correct way to obtain a full-bitvector from a boolean (mask-boolean)?

本文关键字:向量 方法 condition 布尔值 mask-boolean      更新时间:2023-10-16

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

我想出了一种方法,从取10:值的int b(或bool b)的输入中获得这个整数掩码布尔值

unsigned long boolean_mask = -(!b);

要获得相反的值:

unsigned long boolean_mask = -b;

以前有人见过这个建筑吗?我有什么事吗?当int值-1(我假设-b-(!b)确实产生)被提升为更大的无符号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的负担。

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

int it_was_true = b > c;

则CCD_ 8将是1或0但是那个1是从哪里来的

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

Compare R1 with R2 and store either 1 or 0 in R3

或者,事实上,诸如此类的事情。(我在这个答案的末尾对SSE做了一个注释,说明前一句话不太正确。)机器有一个内部条件寄存器,由几个条件位组成,比较指令和一些其他算术运算会以特定的方式修改这些条件位。随后,您可以基于一些条件位执行条件分支,或者执行条件加载,有时还可以执行其他条件操作。

因此,实际上,将1存储在变量中的效率可能比直接进行一些条件运算的效率低得多。可能是,但可能不是,因为编译器(或者至少是编写编译器的人)可能比你聪明,而且它可能只记得它应该在it_was_true中放入一个1,这样当你真正检查值时,编译器可以发出一个适当的分支或其他什么。

因此,说到聪明的编译器,您应该仔细查看由生成的汇编代码

uint64_t ffz_flipped = ~i&~(~i-1);

看看这个表达式,我可以数出五个运算:三个逐位取反,一个逐位求和(and),以及一个减法。但是在汇编输出中找不到五个操作(至少,如果使用gcc-O3)。你会找到三个。

在我们看汇编输出之前,让我们做一些基本的代数。这是最重要的身份:

-X == ~X + 1

你明白为什么这是真的吗?在2的补码中,-X只是表示2n- X的另一种方式,其中n是字中的位数。事实上,这就是为什么它被称为"2的补码"。~X呢?我们可以把它看作是从2的相应幂中减去X中的每一位的结果。例如,如果我们的字中有四个比特,并且X0101(它是5,或者22+20),那么~X1010,我们可以认为它是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;相反,它使用了所有1s的掩码。为什么?因为程序员下一步要做的事情很可能是用它来屏蔽值,我认为这正是你计划用-(!B)技巧做的,除了并行流版本。

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

以前有人见过这种构造吗?我有什么事吗?

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

并且,验证您的编译器是否真的在旧代码上生成了分支,它是否配置正确,以及这种微优化是否真的提高了性能。(最好记下每个优化策略所需的时间。)

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

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

请注意b尚未签名。由于无符号数总是正的,所以强制转换-1u的结果并不特殊,也不会用更多的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<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代码,以检测我要转换的类型是一个无符号整数类型。

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