任何可以在单个 CPU 指令中在 0 和 1 之间翻转位/整数/布尔值的可能代码
Any possible code that can flip a bit/integer/bool between 0 and 1 in single CPU instruction
单个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
要在整数中翻转位,请使用如下xor
:foo ^= 1
.
GCC已经知道这种优化bool
,因此您可以像正常人一样return !status;
而不会损失任何效率。 GCC 也会将status ^= 1
编译为 XOR 指令。 事实上,除了表查找之外,您的所有想法都编译为具有bool
输入/返回值的单个xor
指令。
在 Godbolt 编译器资源管理器上查看它gcc -O3
,带有用于bool
和int
的 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
半相关:XOR 是不带进位的添加。 因此,如果您只关心低位,则可以使用lea eax, [rdi+1]
复制和翻转低位。 请参阅检查一个数字是否有效,与and eax, 1
结合使用,以在 2 条指令中完成它。
为什么bool
与int
不同?
x86-64 System V ABI 要求传递bool
的调用方传递 0 或 1 值,而不仅仅是任何非零整数。 因此,编译器可以假定关于输入。
但是对于int foo
,C 表达式!foo
需要"布尔化"值。!foo
的类型为_Bool
/(如果您#include <stdbool.h>
,则为bool
),将其转换回整数必须产生 0 或 1 的值。 如果编译器不知道foo
必须0
或1
,它就不能优化!foo
foo^=1
,也无法意识到foo ^= 1
在真/假之间翻转一个值。 (从某种意义上说,if(foo)
在 C 中意味着if(foo != 0)
)。
这就是为什么你会得到test/setcc(通过在test
之前xor
将寄存器归零,将零扩展到32位int
)。
相关:编译器中的布尔值为 8 位。对它们的操作效率低下吗? 像(bool1 && bool2) ? x : y
这样的东西并不总是像你希望的那样有效地编译。 编译器相当不错,但确实有遗漏的优化错误。
那额外的mov
指令呢?
如果编译器不需要/不想保留旧的未翻转值以供以后使用,它将在内联时消失。 但在独立函数中,第一个 arg 在edi
中,返回值需要以eax
为单位(在 x86-64 System V 调用约定中)。
像这样的微小函数与作为大函数的一部分可能得到的非常接近(如果这种翻转不能优化为其他东西),但需要在不同的寄存器中得到结果是一个混淆因素。
x86 没有复制和异或整数指令,因此对于独立函数,从 arg 传递寄存器复制到eax
至少需要mov
。
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编码。
从Godbolt的这个代码运行(这段代码基本上包含我尝试过的几个选项)来看,XORing似乎给出了一个可以做到这一点的语句:-(正如你所说,切换是你正在寻找的)
status ^= 1;
归结为只有一条指令(这是-O0
)
xor DWORD PTR [rbp-4], 1
有了-O3
,您可以看到您提到的所有方法都xor
并且特别mov eax, edi/xor eax, 1
。这确保了状态在0
到1
之间来回切换,反之亦然。(因为有xor
语句 - 它在大多数体系结构中都存在,在许多情况下很有用)。
我已经让内存访问的另一个选项失败了 - 因为指针算术和取消引用地址不会比这些更快(有可能的内存访问)。
我提出了一种基于Godbolt中的小混乱的方法。你可以从这里开始做的是 - 比较不同的方法,然后得到你得到的时间的结果。据说,你会得到的结果XOR
-ing对你的机器架构来说不会那么糟糕。
有趣的是,正如彼得·科德斯(Peter Cordes)在示例中表明的那样,这也适用于布尔值。
通过此示例,很明显编译器会根据未优化的代码进行优化1
版本。这是支持这样一个事实的一种方式,即在正常的 int 操作的情况下,xoring 会产生更好的结果。使用布尔值编译时-O3
上面显示的所有布尔值都会涓涓细流到mov eax, edi/xor eax, 1
。
如果您尝试对布尔运算进行微优化,那么您要么过早地优化,要么正在对大量布尔数据进行大量操作。 对于前者 - 答案是不要;对于后者,您可能会问错误的问题。 如果真正的问题是我如何优化(许多)布尔数据的(许多)操作,那么答案是使用基于"标志"的替代表示(即使用更好的算法)。 这将允许您以可移植且可读的方式将更多数据放入缓存中,并同时执行多个操作和测试。
为什么/如何更好?
缓存
考虑缓存行大小为 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
}
- 在没有定义返回类型的函数中返回布尔值,并将结果保存在无错误的char编译中-为什么
- 变量定义到C++布尔值转换
- 如何确保在使用基于布尔值的两个方法之一调用方法时避免分支预测错误
- 重载更少,则运算符返回相反的布尔值
- 将此布尔值传递给此函数的最有效方法是什么?
- 如何设置 c++ 类的布尔值?
- 使用 MAKEWORD / MAKEWPARAM 使用布尔值而不是布尔值
- 将 10 个线程与原子布尔值同步
- QT按钮组和可检查的按钮:如何将切换信号与整数和布尔值连接?
- 整数在 C++ 中没有被类型转换为布尔值
- 任何可以在单个 CPU 指令中在 0 和 1 之间翻转位/整数/布尔值的可能代码
- 如何接受 0 作为整数而不是布尔值
- 将整数算术与布尔值混合 - Z3 证明器
- 在比较过程中C++布尔值是否转换为整数
- C++ WIN32 在共享内存中创建整数和布尔值数组
- 将 0-1 整数 r 值转换为布尔值
- 0 或 1 以外的整数的布尔值是多少
- 将函数的整数返回值与 C++ 中的布尔值进行比较
- 使用布尔值表示整数
- 哪个值更好用?布尔值true或整数1