是-!(condition)从布尔值(mask-boolean)中获得完整位向量的正确方法
Is -!(condition) a correct way to obtain a full-bitvector from a boolean (mask-boolean)?
在从高性能代码中删除条件分支时,将真布尔值转换为unsigned long i = -1
以设置所有位可能很有用。
我想出了一种方法,从取1
或0
:值的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中的每一位的结果。例如,如果我们的字中有四个比特,并且X
是0101
(它是5,或者22+20),那么~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)
技巧做的,除了并行流版本。
所以,请放心,它已经被覆盖了。
以前有人见过这种构造吗?我有什么事吗?
很多人都看过。它像石头一样古老。这并不罕见,但您应该将其封装在内联函数中,以避免混淆代码。
并且,验证您的编译器是否真的在旧代码上生成了分支,它是否配置正确,以及这种微优化是否真的提高了性能。(最好记下每个优化策略所需的时间。)
从好的方面来说,它完全符合标准。
当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。这会反转布尔条件,但如果你可以首先生成布尔的反转,或者使用掩码的反转,那么这只是一个操作,而不是两个。
- C++从另一个类访问公共静态向量的正确方法是什么
- 有没有一种"cleaner"的方法可以在指向基的指针向量中找到派生类的第一个实例?
- 声明高维向量的更简洁的方法
- 在C++中初始化向量映射的最有效方法
- C++11 迭代向量的新方法?
- std::find,返回所有找到的值的替代方法,而不仅仅是存在重复的向量的第一个值
- C++ STD 函数运算符:有没有一种方法可以通过函数将一个向量映射到另一个向量上?
- C++:将向量传递到构造函数以创建成员变量的最佳方法?
- 有什么方法可以将具有不同模板参数的模板类实例放入向量中?
- 当映射包含字符串向量作为值时,从值中获取键的有效方法
- 在向量中查找大于 0(或通常为 k)的最小元素的最佳方法是什么?
- 从数组中删除非唯一值、保持顺序和不使用向量的最佳方法?
- 使用192/256位整数求和无符号64位整数向量的点积的最快方法
- 访问类成员向量最后一项的正确方法
- 使用 std::vector::swap 方法在C++中交换两个不同的向量是否安全?
- 从列表/向量制作嵌套 for 循环的最佳方法是什么?
- 检查两个向量是否并行的最有效方法
- 如何从 node-ffi 调用 c++ 中以结构向量作为参数的方法?
- 返回向量元素的 l 值的正确方法是什么?
- 如何在方法主体中返回声明向量的引用?