x86汇编语言中是否存在abs()
函数?
(这个问题最初提到了获取两个有符号整数的差异,但如果需要在减法中避免可能的有符号溢出,则这实际上是一个单独的问题。否则,只需使用abs(x-y)
,可能需要先扩展输入。)
x86汇编语言中是否存在abs()
函数?
(这个问题最初提到了获取两个有符号整数的差异,但如果需要在减法中避免可能的有符号溢出,则这实际上是一个单独的问题。否则,只需使用abs(x-y)
,可能需要先扩展输入。)
abs()
在汇编中无需分支语句的实现方式: abs(x) = (x XOR y) - y
其中y = x >> 31
(假设输入为32位),>>
是算术右移运算符。
以上公式的解释:我们只想生成负数x
的二进制补码。
y = 0xFFFFFFFF, if x is negative
0x00000000, if x is positive
当x
为正时,x XOR 0x00000000
等于x
。当x
为负时,x XOR 0xFFFFFFFF
等于x
的1's补码。现在我们只需要加上1
来得到它的2's补码,这就是表达式-y
所做的事情。因为0xFFFFFFFF
在十进制中表示-1。
让我们看看由gcc
(我机器上的4.6.3版)生成的以下代码汇编:
C代码:
main()
{
int x;
int output = abs(x);
}
gcc 4.6.3生成的汇编片段(AT&T语法),附有我的注释:
movl -8(%rbp), %eax # -8(%rbp) is memory for x on stack
sarl $31, %eax # shift arithmetic right: x >> 31, eax now represents y
movl %eax, %edx #
xorl -8(%rbp), %edx # %edx = x XOR y
movl %edx, -4(%rbp) # -4(%rbp) is memory for output on stack
subl %eax, -4(%rbp) # (x XOR y) - y
奖励(来自《黑客的乐趣》):如果你有一个快速的加1和减1的乘法,那么下面的代码将给出32位x
的abs(x)
: ((x >> 30) | 1) * x
0xFFFF
将是y
值。32位则为0xFFFF_FFFF
。 - ecmcdq
技巧,Mark Wilkins的回答涵盖了它。(但是Hal的使用xor
清零和sub
替代mov
+neg
的cmovl答案更好。) - Peter Cordes>>>
作为算术右移。这与 JavaScript 语言相反,其中 >>
是算术右移运算符,而 >>>
是无符号(逻辑)右移运算符。 - Alexis Wilke>>>
了——但我已经修复了,因为几乎所有通用编程语言都使用 >>
。 - bits虽然这是一个旧的帖子,但如果我来晚了,你也许也会看到这里... abs是一个很好的例子,所以应该在这里。
; abs(eax), with no branches.
; intel syntax (dest, src)
mov ebx, eax ;store eax in ebx
neg eax
cmovl eax, ebx ;if eax is now negative, restore its saved value
xor ebx,ebx
/ sub ebx,eax
可以将 mov
操作从关键路径中移除,因此在某些 CPU 上(包括 Ice Lake,其中 mov 消除被破坏)效果更好。在 Sandybridge 系列上,xor-zeroing 与 NOP 一样便宜,就像消除的 MOV 一样,即使在其他仍需要后端执行端口的 CPU 上,它也没有输入依赖性,因此可以在输入值准备好之前的任何时候运行。此外,对于 int64_t 版本,它避免了第一个指令的 REX 前缀,而其余指令使用 rax/rbx。 - Peter Cordesneg
/cmov
。不幸的是,两者都没有使用xor
-zero / sub
/ cmov
。 - Peter Cordes如果是x86汇编,根据万能的维基百科的说法,以下方法应该可行。将一个值减去另一个值,然后对结果使用这些指令:
cdq
xor eax, edx
sub eax, edx
如果你想正确处理所有情况,就不能只是减法并取绝对值。因为两个有符号整数的差不一定能表示为有符号整数。例如,假设你正在使用32位2s补码整数,并且要找到INT_MAX
(0x7fffffff
)和INT_MIN
(0x80000000
)之间的差异。进行减法运算会得到:
0x7fffffff - 0x80000000 = 0xffffffff
当你取绝对值时,-1的结果是1;而这两个数字之间的实际差值是0xffffffff,被解释为无符号整数(UINT_MAX)。
两个有符号整数之间的差值总是可以表示为一个无符号整数。要得到这个值(使用2s补码硬件),只需从较大的输入中减去较小的输入,并将结果解释为无符号整数即可,不需要绝对值。
以下是在x86上执行此操作的一种方法(其中包含许多方法,不一定是最佳方法),假设这两个整数位于eax和edx中:
cmp eax, edx // compare the two numbers
jge 1f
xchg eax, edx // if eax < edx, swap them so the bigger number is in eax
1: sub eax, edx // subtract to get the difference
jge
可能会导致 CPU 中的 分支预测器
出现 错误预测
,这将严重降低 CPU 的速度。因此,如果性能是一个问题,最好使用 @bits 或 @Hal 的答案。 - Ericpsubd
计算差值,然后使用pabsd
获取差值的绝对值。cdq
技巧获取绝对值。这需要使用一些特定的寄存器(cdq
将eax
符号扩展为edx
,不使用其他寄存器),因此您可能希望使用其他操作码。例如:mov r2, r1
sar r2, 31
将r1
中的符号扩展计算到寄存器r2
中(如果r1
为正或零,则为0,如果r1
为负,则为0xFFFFFFFF)。这适用于所有32位寄存器r1
和r2
,并替代了cdq
指令。
一种简短而直接的方法是使用条件移动指令(适用于 Pentium 及以上版本):
; compute ABS(r1-r2) in eax, overwrites r2
mov eax, r1
sub eax, r2
sub r2, r1
cmovg eax, r2
sub指令的标志位与cmp指令相同。
ABS(EAX)
test eax, eax ; Triger EFLAGS [CF, OF, PF, SF, and ZF]
jns AbsResult ; If (SF) is off, jmp AbsResult
neg eax ; If (SF) is on. (negation nullify by this opcode)
AbsResult:
test
。如果输入值在正数和负数之间随机分布,分支预测错误会使其变慢。一次返回8个字符
inline u64 abs_8(u64 x)
{
u64 y=(x>>7)&0x0101010101010101ull;
return (x^((y<<8)-y))+y;
}
pabsb
来执行16字节的并行操作。但是可能仍然有一些没有SSSE3的CPU存在,例如AMD Phenom II是最新的没有它的CPU。因此,在这种情况下,您的位运算可能会很有用,尽管您可以使用psubb
/ pcmpgtb
/混合。或者,如果您实际上需要像问题正文所要求的绝对差异,则可以使用psadbw
在8字节块内添加绝对差异。 - Peter Cordespsubb
然后使用 pmaxsb
保留正元素 (abs(x) = max(x, -x)
) 来避免混合,但是 pmaxsb
是 SSE4.1。可能使用 SSE2 的 pminub
来保留 没有 符号位设置的元素?是的,这也可以工作,包括特殊情况 0 和 -128
。(因为它是自己的二进制补码反转,minub(128u, 128u) = -128 = 128u
) - Peter Cordes__m128i vx = _mm_cvtsi64_si128(x)
; return _mm_min_epu8(vx, _mm_sub_epi8(_mm_setzero_si128(), vx);
。 https://godbolt.org/z/GdanTcqGd 证实了clang甚至会在可用时将其优化为pabsb,这意味着它必须是正确的!因此,在x86-64汇编中,这个位操作并不有用,即使您只使用基线SSE2。除非可能在内核代码中,您只能使用通用寄存器。 - Peter Cordes如果您想要做A-B,可以使用SUB指令。
希望这能帮到您。
unsigned abs(int x)
与正负整数的绝对差的高效/正确实现是不同的问题。例如,可以查看使用SSE计算无符号整数之间的绝对差异以获取适用于SIMD元素的一些策略,并且可以使用标量代码完成。 或者 快速、无分支的无符号整数绝对差异。通过取最小值/最大值并相减似乎很好。对于有符号数,扩展两个输入、相减(因此不可能溢出),然后再求绝对值也可以。 - Peter Cordes