x86汇编abs()实现?

34

x86汇编语言中是否存在abs()函数?

(这个问题最初提到了获取两个有符号整数的差异,但如果需要在减法中避免可能的有符号溢出,则这实际上是一个单独的问题。否则,只需使用abs(x-y),可能需要先扩展输入。)


如何比较并有条件地交换,然后再进行减法运算。您能提供一个x86的示例吗? - Greg C.
你的意思是“距离”,而不是“差异”。 - Andreas Rejbrand
设置两个整数的符号位为0是可能的吗? - Anatoly
顺带一提,unsigned abs(int x)与正负整数的绝对差的高效/正确实现是不同的问题。例如,可以查看使用SSE计算无符号整数之间的绝对差异以获取适用于SIMD元素的一些策略,并且可以使用标量代码完成。 或者 快速、无分支的无符号整数绝对差异。通过取最小值/最大值并相减似乎很好。对于有符号数,扩展两个输入、相减(因此不可能溢出),然后再求绝对值也可以。 - Peter Cordes
但是这里所有的答案都是关于单个abs()的,所以让我们保持这种方式。 - Peter Cordes
显示剩余2条评论
9个回答

39
这是C库函数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位xabs(x)
      ((x >> 30) | 1) * x

1
对于16位有符号算术,0xFFFF将是y值。32位则为0xFFFF_FFFF - ecm
@PeterCordes 我不记得9年前我用了什么命令来生成汇编代码,我很想了解你的想法——也许可以通过一个新的答案来演示它? - bits
你是指查看带参数和返回值的函数的汇编输出吗?这个问题已经在6年前解决了,参见如何从GCC/clang汇编输出中去除“噪音”?。或者如果你是指汇编中的cdq技巧,Mark Wilkins的回答涵盖了它。(但是Hal的使用xor清零和sub替代mov+neg的cmovl答案更好。) - Peter Cordes
1
有趣的是你使用 >>> 作为算术右移。这与 JavaScript 语言相反,其中 >> 是算术右移运算符,而 >>> 是无符号(逻辑)右移运算符。 - Alexis Wilke
1
啊,@AlexisWilke,好眼力啊。使用正确的语法肯定是有道理的,我甚至都不记得为什么要用>>>了——但我已经修复了,因为几乎所有通用编程语言都使用 >> - bits
显示剩余3条评论

29

虽然这是一个旧的帖子,但如果我来晚了,你也许也会看到这里... 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

2
这个方法非常简单高效,通过避免“分支预测器”,绝对应该被接受为答案。 - Eric
xor ebx,ebx / sub ebx,eax 可以将 mov 操作从关键路径中移除,因此在某些 CPU 上(包括 Ice Lake,其中 mov 消除被破坏)效果更好。在 Sandybridge 系列上,xor-zeroing 与 NOP 一样便宜,就像消除的 MOV 一样,即使在其他仍需要后端执行端口的 CPU 上,它也没有输入依赖性,因此可以在输入值准备好之前的任何时候运行。此外,对于 int64_t 版本,它避免了第一个指令的 REX 前缀,而其余指令使用 rax/rbx。 - Peter Cordes
顺便说一下,这就是clang所做的(以及GCC应该做的)。请参见此处此处,其中包含clang和GCC输出的两个示例,其中GCC使用2的补码位操作标识符,clang使用neg/cmov。不幸的是,两者都没有使用xor-zero / sub / cmov - Peter Cordes
1
刚刚将这个与 gcc-9 进行了比较。实际上,这个更快,速度有明显的可测量的提升! - Charles Lohr

20

如果是x86汇编,根据万能的维基百科的说法,以下方法应该可行。将一个值减去另一个值,然后对结果使用这些指令:

cdq
xor eax, edx
sub eax, edx

16

如果你想正确处理所有情况,就不能只是减法并取绝对值。因为两个有符号整数的差不一定能表示为有符号整数。例如,假设你正在使用32位2s补码整数,并且要找到INT_MAX0x7fffffff)和INT_MIN0x80000000)之间的差异。进行减法运算会得到:

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

3
使用 jge 可能会导致 CPU 中的 分支预测器 出现 错误预测,这将严重降低 CPU 的速度。因此,如果性能是一个问题,最好使用 @bits 或 @Hal 的答案。 - Eric

6
假设您的整数存储在MMX或XMM寄存器中,使用psubd计算差值,然后使用pabsd获取差值的绝对值。
如果您的整数存储在普通的"正常"寄存器中,则进行减法运算,然后使用cdq技巧获取绝对值。这需要使用一些特定的寄存器(cdqeax符号扩展为edx,不使用其他寄存器),因此您可能希望使用其他操作码。例如:
mov  r2, r1
sar  r2, 31

r1中的符号扩展计算到寄存器r2中(如果r1为正或零,则为0,如果r1为负,则为0xFFFFFFFF)。这适用于所有32位寄存器r1r2,并替代了cdq指令。


5

一种简短而直接的方法是使用条件移动指令(适用于 Pentium 及以上版本):

; compute ABS(r1-r2) in eax, overwrites r2
mov eax, r1
sub eax, r2
sub r2, r1
cmovg eax, r2

sub指令的标志位与cmp指令相同。


1
cmov是P6(ppro/PII)中的新功能,但现在你可以假设它已经存在了。gcc也支持它。 - Peter Cordes

2

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:

如果eax中的值已经由任何生成标志的方式设置好了,您就不需要使用test。如果输入值在正数和负数之间随机分布,分支预测错误会使其变慢。
对于RAX、AX和AL,这个过程是相同的。

3
"or reg, reg" 比 "test reg,reg" 总是一个更糟糕的选择。分支指令并不总是需要一个时钟周期完成,它们可能是 ~0 个时钟周期(预测正确),也可能是 ~15 个时钟周期(预测错误)。原文链接为:https://dev59.com/x1sX5IYBdhLWcg3whvsn#33724806。 - Peter Cordes

0

一次返回8个字符

inline u64 abs_8(u64 x)
{
    u64 y=(x>>7)&0x0101010101010101ull;
    return (x^((y<<8)-y))+y;
}

目前你的回答不够清晰。请编辑并添加更多细节,以帮助其他人理解它如何回答所提出的问题。你可以在帮助中心找到有关如何撰写好答案的更多信息。 - Community
1
在x86汇编中,我们通常使用SSSE3 pabsb来执行16字节的并行操作。但是可能仍然有一些没有SSSE3的CPU存在,例如AMD Phenom II是最新的没有它的CPU。因此,在这种情况下,您的位运算可能会很有用,尽管您可以使用psubb / pcmpgtb /混合。或者,如果您实际上需要像问题正文所要求的绝对差异,则可以使用psadbw在8字节块内添加绝对差异。 - Peter Cordes
我在思考从零开始取反的 psubb 然后使用 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

-2

如果您想要做A-B,可以使用SUB指令。

希望这能帮到您。

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