在单CPU指令中可以在0和1之间翻转位/整数/布尔值的任何可能代码

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了在单CPU指令中可以在0和1之间翻转位/整数/布尔值的任何可能代码相关的知识,希望对你有一定的参考价值。

单个x86指令可以在“0”和“1”之间切换布尔值吗?

我想到了以下方法,但都导致了两个指令与-cc标志的gcc。

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; }

compiles on Godbolt对此:

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

要在整数中翻转一下,请使用xor,如下所示:foo ^= 1

gcc已经为bool知道了这个优化,所以你可以像普通人一样return !status;而不会失去任何效率。 gcc也会将status ^= 1编译为xor指令。实际上,除了表查找之外,您的所有想法都会使用xor输入/返回值编译为单个bool指令。

on the Godbolt compiler explorer查看gcc -O3,用boolint的asm输出窗格。

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

Why is bool different from int?

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

但是对于int foo,C表达式!foo需要“布尔化”该值。 !foo有类型_Bool /(如果你bool也称为#include <stdbool.h>),并将其转换回整数必须产生0或1的值。如果编译器不知道foo必须是01,它不能优化!foofoo^=1,并没有意识到foo ^= 1翻转truthy / falsy之间的价值。 (从某种意义上说,if(foo)意味着C中的if(foo != 0))。

这就是为什么你得到test / setcc(在int之前由xor-zeroing a register零扩展到32位test)。

相关:Boolean values as 8 bit in compilers. Are operations on them inefficient?。像(bool1 && bool2) ? x : y这样的东西并不总是像你希望的那样有效地编译。编译器非常好,但确实有错过优化错误。


What about that extra mov instruction?

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

像这样的微小函数非常接近你可能得到的大函数的一部分(如果这个翻转不能被优化成其他东西),但是需要将结果放在不同的寄存器中是一个混淆因素。


x86没有copy-and-xor整数指令,因此对于独立函数,至少需要一个mov才能从arg传递寄存器复制到eax

lea很特别:它是为数不多的整数ALU指令之一,它可以将结果写入不同的寄存器而不是破坏其输入。 lea是一个copy-and-shift/add instruction,但x86中没有copy-and-xor指令。许多RISC指令集具有3操作数指令,例如MIPS可以执行xor $t1, $t2, $t3

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

另一答案

从这个code run of Godbolt(这个代码基本上包含我尝试的几个选项)似乎XORing给出了一个可以做到的声明:-(正如你说切换是你正在寻找的)

status ^= 1;

归结为只是单指令(这是与-O0

xor DWORD PTR [rbp-4], 1

使用-O3你可以看到你提到的所有方法使用xor anf这特别是mov eax, edi/xor eax, 1

这确保了状态从01来回切换,反之亦然。 (因为有xor语句 - 在大多数体系结构中存在并且在许多情况下很有用)。

我让内存访问的另一个选项失败 - 因为指针算术和取消引用地址不会比这些更快(有可能的内存访问)。

我已经提出了一种基于godbolt中的小混乱的方法。你可以从这里做的是 - 比较不同的做法,然后得到你得到的时间的结果。据说,你将获得XOR-ing的结果在你的机器架构上不会那么糟糕。

有趣的是,作为示例showed的Peter Cordes,这也适用于布尔人。

有了这个example,显然编译器使用1版本优化到未优化代码的xoring。这是一种方法,支持xoring在正常int操作的情况下会产生更好的结果。使用-O3编译时的布尔值所有上面显示的都是mov eax, edi/xor eax, 1

另一答案

如果您要尝试微量优化布尔运算,那么您要么过早地进行优化,要么就是在很多布尔数据上进行大量操作。对于前者 - 答案是否定的;对于后者,你可能会问错误的问题。如果真正的问题是如何在(多)布尔数据上优化(许多)操作,答案是使用基于“标志”的替代表示(a.k.a.使用更好的算法)。这将允许您可移植且可读地将更多数据放入缓存中并同时执行多个操作和测试。

Why/How is this better?

Cache

考虑一个高速缓存行大小为64字节的系统。 64 _Bool将适合数据缓存行,而适合该数量的8倍。您可能也会有更小的指令代码 - 从1个额外指令到32倍更少。这可以在紧密循环中产生很大的差异。

Operations

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

Branching

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

Readability

通过使用名称良好的掩码常量,可以将复杂的嵌套if-else-if-else块简化为单个可读行。

Portability

_Bool在早期版本的C中不可用,C ++使用不同的布尔机制;但是,flags将在旧版本的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
}

以上是关于在单CPU指令中可以在0和1之间翻转位/整数/布尔值的任何可能代码的主要内容,如果未能解决你的问题,请参考以下文章

将4位整数转换为布尔列表

矩阵翻转:

硬币翻转指令

MySQL布尔值 - 翻转值?

使用整数作为位域的缺点?

按位运算符简单地翻转整数中的所有位?