有没有可能通过单个CPU指令来翻转0和1之间的位/整数/布尔值的任何代码?

3

一个x86指令能否在“0”和“1”之间切换布尔值?

我想到了以下几种方法,但使用gcc的-O3标志都需要两个指令。

status =! status;

status = 1 - status;

status  = status == 0 ? 1: 0;

int flip[2] = {1, 0};
status = flip[status];

有没有更快的方法来做这件事?

这是我尝试过的:https://godbolt.org/g/A3qNUw


我需要一个函数来切换输入并返回结果,编写方式应该只编译成一条指令。类似于这个函数:

int addOne(int n) { return n+1; }

在Godbolt上编译得到以下结果:

  lea eax, [rdi+1]    # return n+1 in a single instruction
  ret

5
status ^=1; 这是一种方式。 (翻译说明:尽可能保持原文意思和语法结构,使用通用词汇来翻译代码,不添加解释或额外信息。) - user2736738
3
什么两个指令?什么CPU? - Eugene Sh.
1
位翻转本身肯定是单条指令,即https://godbolt.org/g/XznrRA(第二个指令是解决调用约定)。所以你的问题前提是错误的,你的问题也没有意义。请添加上下文,说明你想要单一指令的地方,你尝试了什么,以及它失败的原因。 - Ped7g
也许可以使用 xor value,8 来处理第三位的比特位。但是为什么需要一个函数来执行这样简单的操作呢? - Weather Vane
1
关于edit2:您可以将+1的示例视为一种位翻转,但会损坏上位位(通过执行+that_bit_value可以翻转任何位,当您不关心上面的位被修改时)。但我认为没有单个指令SYSTEM V ABI函数仅翻转所需的位,因为参数在rdi/edi/...中传入,并且结果在rax/eax/...(和ret!)中返回。但这是奇怪的要求,因为这不是您在实际生产中需要的东西,因此您试图实现的任何内容都是纯粹人工的,并且与真实世界的软件无关。 - Ped7g
显示剩余4条评论
3个回答

12
要翻译的内容如下:

To flip a bit in an integer, use xor like this: foo ^= 1.

gcc知道这种优化对于bool类型已经实现了,因此你可以像正常人一样使用return !status;而不会失去任何效率。gcc也会将status ^= 1编译为异或指令。事实上,除表格查找外,所有想法都可以通过使用bool输入/返回值来编译成单个xor指令。

使用gcc -O3Godbolt编译器探索器上检查,同时查看boolint的汇编输出窗格。

MYTYPE func4(MYTYPE status) {
    status ^=1;
    return status;
}

  # same code for bool or int
  mov eax, edi
  xor eax, 1
  ret

对比。

MYTYPE func1(MYTYPE status) {
    status = !status;
    return status;
}

  # with -DMYTYPE=bool
  mov eax, edi
  xor eax, 1
  ret

  # with int
  xor eax, eax
  test edi, edi
  sete al
  ret

半相关:异或是无进位加法。 因此,如果您只关心低位,则可以使用lea eax,[rdi + 1]进行复制和翻转低位。 请参见检查数字是否为偶数,其中结合and eax,1使用可在2个指令中完成。

为什么boolint不同?

x86-64 System V ABI要求传递bool的调用者传递0或1值,而不仅仅是任何非零整数。因此,编译器可以假定输入的内容。

但对于int foo,C表达式!foo需要将值“布尔化”。!foo的类型为_Bool /(如果您#include <stdbool.h>,则为bool),并将其转换回整数必须产生值0或1。如果编译器不知道foo必须是01,它就不能将!foo优化为foo^=1,也无法意识到foo ^= 1在真/假之间翻转值的意义。(在C中,if(foo)的意思是if(foo!= 0))。

这就是为什么您会得到test/setcc(通过xor-zeroing a register将其零扩展为32位int,然后进行test)。

相关文章:编译器中8位布尔值的效率问题。像(bool1 && bool2) ? x : y这样的东西并不总是被编译成您希望的那样高效。编译器相当不错,但确实存在优化失误的 bug。


那多余的mov指令怎么办?

如果编译器不需要/不想保留旧的未翻转值以备后用,它将在内联时消失。但在独立函数中,第一个参数在edi中,返回值需要在eax中(在x86-64 System V调用约定中)。

像这样的小函数是对作为大函数一部分的内容的近似,(如果这个翻转不能被优化成其他东西),但需要在不同的寄存器中得到结果是一个混淆因素。


x86没有复制和异或整数指令,因此对于一个独立的函数,它至少需要一个mov从参数传递寄存器复制到eax

lea是特殊的:它是为数不多的整数ALU指令之一,可以将结果写入不同的寄存器而不是破坏其输入。 lea是一种复制和移位/加法指令,但在x86中没有复制和异或指令。许多RISC指令集具有3操作数指令,例如MIPS可以执行xor $t1,$t2,$t3

AVX引入了向量指令的非破坏性版本(在很多代码中节省了大量的movdqa/movups寄存器复制),但对于整数来说,只有一些新指令可以执行不同的操作。例如,rorx eax,ecx,16执行eax = rotate_right(ecx, 16),并使用与非破坏性AVX指令相同的VEX编码。


这是一个非常有趣的答案。 - Saiph

4
从这个 Godbolt 的代码运行(该代码基本包含我尝试的几个选项)来看,似乎异或运算可以给出一个语句来实现:-(正如你所说的,切换就是你要找的)。
status ^= 1;

归结为单个指令(这是使用-O0时的情况)。

xor DWORD PTR [rbp-4], 1

使用-O3,您可以看到您提到的所有方法都使用xor,特别是mov eax, edi/xor eax, 1
这确保了状态在01之间来回切换。(因为大多数体系结构中都有xor语句,在许多情况下很有用)。
我已经放弃了内存访问的其他选项 - 因为指针算术和解引用地址不会比这些选项(具有可能的内存访问)更快。
我根据在godbolt上的小试验建议了一种方法。从这里开始,您可以比较不同的方法,然后得出您得到的时间结果。据说,在您机器的体系结构中,执行XOR操作不会太差。
有趣的是,正如Peter Cordes在示例中展示的那样,这也适用于布尔值。

通过这个示例,很明显编译器优化了未经优化的代码与1版本异或的操作。这是一种支持在普通int操作中异或会产生更好结果的方式。对于布尔值,在使用-O3编译时,上述所有操作都会转换为mov eax, edi/xor eax, 1


是的,将其分解为单独的函数会更好。但这样可以让你做得更好:像 OP 在最后一次编辑中展示的那样,将函数参数和返回值结合起来更有意义。这是查看 gcc 如何优化一个微小片段而不涉及任何周围代码的正常方式。 - Peter Cordes
@PeterCordes:这是一个好建议。我进行了编辑。是的,所有的函数在优化时都会进行异或操作。这很有趣。这里是我的操作。 - user2736738
更新:是的,gcc知道在bool status的情况下针对!status的优化。https://godbolt.org/g/tsJgWw - Peter Cordes
这并不重要(当您启用优化编译时)。重要的是看一下将“status”设置为“bool”与“int”的区别。请参阅我的最后一个Godbolt链接。 - Peter Cordes
@PeterCordes:这意味着当它确定为0/1或布尔类型时,它会将所有内容优化为一个(它们相同)。因此,这从另一个方面证明了执行异或操作更好,因为优化后的布尔版本归结为未经优化的代码的x^=1部分。 - user2736738
显示剩余8条评论

3
如果你试图微调布尔运算,要么是过早优化了,要么是在大量进行布尔数据操作。对于前者,答案是不要这样做;对于后者,你可能问错了问题。如果真正的问题是如何优化(许多)布尔数据的(许多)操作,则答案是使用基于“标志”(即使用更好的算法)的替代表示。这将允许你将更多的数据可移植地和易读地适配到缓存中,并同时执行多个操作和测试。

为什么/如何更好?

缓存

考虑一个缓存行大小为64字节的系统。64个_Bool可以适配到数据缓存行中,而8倍于此的数据可以适配到其中。您也可能会有更小的指令代码——从1个附加指令到少32倍的指令。这在紧密循环中可能会产生很大的差异。

操作

大多数操作涉及一个或两个(通常非常快速)操作和单个测试,而无论您要测试多少个标志。由于这可以同时包含多个值,因此每个操作可以执行(通常是32或64倍)更多的工作。

分支

由于可以同时完成多个操作和测试,原本可能有32(或64)个可能的分支可以减少到一个。这可以减少分支预测错误。

可读性

通过使用一个名为掩码常量的良好命名的掩码,复杂嵌套的if-else-if-else块可以减少为单个易读的行。

可移植性

_Bool在早期版本的C中不可用,C++使用不同的机制来表示布尔类型;但是,标志在较旧的C版本中也适用,并且与C++兼容。
下面是如何使用标志设置掩码的实际示例:
int isconsonant(int c){
    const unsigned consonant_mask = (1<<('b'-'a'))|
    (1<<('c'-'a'))|(1<<('d'-'a'))|(1<<('f'-'a'))|(1<<('g'-'a'))|
    (1<<('h'-'a'))|(1<<('j'-'a'))|(1<<('k'-'a'))|(1<<('l'-'a'))|
    (1<<('m'-'a'))|(1<<('n'-'a'))|(1<<('p'-'a'))|(1<<('q'-'a'))|
    (1<<('r'-'a'))|(1<<('s'-'a'))|(1<<('t'-'a'))|(1<<('v'-'a'))|
    (1<<('w'-'a'))|(1<<('x'-'a'))|(1<<('y'-'a'))|(1<<('z'-'a'));
    unsigned x = (c|32)-'a'; // ~ tolower
    /* if 1<<x is in range of int32 set mask to position relative to `a`
     * as in the mask above otherwise it is set to 0 */
    int ret = (x<32)<<(x&31);
    return ret & consonant_mask;
}
//compiles to 7 operations to check for 52 different values
isconsonant:
  or edi, 32 # tmp95,
  xor eax, eax # tmp97
  lea ecx, [rdi-97] # x,
  cmp ecx, 31 # x,
  setbe al #, tmp97
  sal eax, cl # ret, x
  and eax, 66043630 # tmp96,
  ret

这个概念可以用于同时操作一个模拟的布尔值数组,例如:
//inline these if your compiler doesn't automatically
_Bool isSpecificMaskSet(uint32_t x, uint32_t m){
    return x==m; //returns 1 if all bits in m are exactly the same as x
}

_Bool isLimitedMaskSet(uint32_t x, uint32_t m, uint32_t v){
    return (x&m) == v;
    //returns 1 if all bits set in v are set in x
    //bits not set in m are ignored
}

_Bool isNoMaskBitSet(uint32_t x, uint32_t m){
    return (x&m) == 0; //returns 1 if no bits set in m are set in x
}

_Bool areAllMaskBitsSet(uint32_t x, uint32_t m){
    return (x&m) == m; //returns 1 if all bits set in m are set in x
}

uint32_t setMaskBits(uint32_t x, uint32_t m){
    return x|m; //returns x with mask bits set in m
}

uint32_t toggleMaskBits(uint32_t x, uint32_t m){
    return x^m; //returns x with the bits in m toggled
}

uint32_t clearMaskBits(uint32_t x, uint32_t m){
    return x&~m; //returns x with all bits set in m cleared
}

uint32_t getMaskBits(uint32_t x, uint32_t m){
    return x&m; //returns mask bits set in x
}

uint32_t getMaskBitsNotSet(uint32_t x, uint32_t m){
    return (x&m)^m; //returns mask bits not set in x
}

2
有趣的事实:gcc知道如何将switch语句优化为立即位图,用于与您的辅音位图完全相同类型的测试。 https://dev59.com/OXVD5IYBdhLWcg3wE3Xz#NZkToYgBc1ULPQZF4bJY。相关:我在一个代码高尔夫答案中使用了辅音位图[(https://codegolf.stackexchange.com/questions/123194/user-appreciation-challenge-1-dennis/123458#123458)],使用不同的方法拒绝非字母ASCII代码。 x <32很有趣,但是C中的超出范围的移位计数是UB。 在这种情况下,gcc的asm会做我们想要的事情。 - Peter Cordes
很好,(x<32) << (x&31) 应该能够在目标平台上可靠地编译为没有额外指令的代码,例如 x86 平台,其中移位指令掩码计数(gcc 也知道这一点)。现在,“单行可读性”有点牵强,不过可以加上注释,比如 // 1 << x 在字母范围内。在 C 或汇编中,我之前从未见过将布尔值转换为整数并用作移位操作的左操作数的惯用法。 - Peter Cordes
感谢@PeterCordes,&31在x86上确实被优化掉了。顺便说一下,我曾经使用(x<=('z'-'a'))而不是(x<32),但使用32来表示它在uint32_t的范围内(加上它可以帮助我测试过的编译器进行优化)...现在额外的&31只是重申了这一点。将bool->int用作移位的左侧是我编码风格的怪癖之一,我在优化一些ctype.h函数以避免分支和查找表时学到了这个技巧。 - technosaurus
另一个想法:如果在Broadwell及更高版本中没有BMI2用于单uop变量计数移位,则最佳汇编语言可能使用bt来测试位。即xor-zero / cmp / cmov(x <32?consonant_mask:0)/ bt / jcc(或cmov或adc,或者对CF中的is_consonant结果进行任何操作)。 - Peter Cordes
对于您的方法,(x<32) & (consonant_bitmap >> (x&31)) 可以创建更多的指令级并行性,因为移位不必等待比较/设置cc。作为奖励,它创建了一个可用作 bool 的 0 / 1。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接