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

时间:2012-12-17 01:07:00

标签: c++ c logic bit-manipulation

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

我想出了一种从int b(或bool b)的输入获取此整数掩码布尔值的方法,其值为10:< / 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带来负担。

4 个答案:

答案 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中的每一位的结果。例如,如果我们的单词中有四位,X0101(这是5,或2 2 + 2 0 ),然后~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;相反,它使用所有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。这反转了布尔条件,但是如果你可以在第一个位置生成布尔值的倒数,或者使用掩码的逆,那么它只是一个操作而不是两个操作。