我的任务是编写x86 asm代码,输出:
- (-x^2 + 1)如果x〈= -1;
- 如果x的绝对值小于1,则为0;
- (x^2 - 1)如果x〉= 1。
这是我的想法
- 第一个问题是,在第一种情况下,结果不是负数。如果我输入-3,结果是8,而不是-8。
- 其次,如果x的值小于1,例如0.5,则输出-0.75而不是0。
我是一个初学者在大会上,这让我有点难住了,所以如果你能帮助我得到它的工作正确,我真的很感激。
代码如下:
extern printf, scanf
global main
section .data
in_fmt db "%lf", 0
out_fmt db "Result: %lf", 10, 0
one dq 1.0
minus_one dq -1.0
section .bss
x resb 8
f resb 8
temp resb 8
section .text
main:
mov rbp, rsp; for correct debugging
;scanf()
mov rdx, x
mov rcx, in_fmt
sub rsp, 40
call scanf
add rsp, 40
fld qword[x] ;x (input) is pushed on stack
;if x <= -1
fld qword[minus_one] ;-1 and x are on stack
fcomi
jle case1
fstp qword[temp] ;x is on stack
;if |x| <= 1
fld qword[one] ;1 and x are on stack
fcomi
jle case2
case3:
fstp qword[temp] ;x is on stack
fmul qword[x] ;x*x is on stack
fsub qword[one] ;x*x + 1 is on stack
fstp qword[f] ; stack empty
jmp print
case2:
mov qword[f], 0
jmp print
case1:
fstp qword[temp] ;x is on stack
fmul qword[x] ;x*x is on stack
fmul qword[minus_one] ;-(x*x) is on stack
fadd qword[one] ;-(x*x) + 1 is on stack
fstp qword[f] ;stack empty
print:
mov rcx, out_fmt
mov rdx, qword[f]
sub rsp, 40
call printf
add rsp, 40
xor rax, rax
ret
1条答案
按热度按时间qlfbtfca1#
使用调试器单步调试代码并查看寄存器。此外,FP分支使用与无符号整数相同的条件,如
jae
。但是如果您花一些时间来寻找更有效的方法来计算结果,则无需在此处进行分支。x86-64通常情况下,你会想使用SSE 2
mulsd
/addsd
进行标量FP数学。特别是如果你不打算使用有用的x87指令,如fabs
作为检查|x| < 1.0
的一部分。(你可以很容易地对SSE 2做同样的事情,使用andps xmm0, [mask]
,其中掩码是align 16
/mask: dq (1<<63) - 1
来清除符号位)。我认为你可以将其优化为
copysign(x^2 - 1, x)
加上对|x| <= 0
的检查。copysign
与SSE数学只是andps
隔离符号位和orps
应用它的问题,因为你只关心“目标”是非负的情况。否则你也会andnps
来清除目标中的符号位-将符号位1与来自另一个的其它位混合。与FP逻辑无关,但
mov rbp, rsp; for correct debugging
前面没有push rbp
会很奇怪。(并将sub rsp, 40
调整为32
以保持堆栈对齐。)main
是一个应该遵循调用约定的函数,并且RBP是调用保留的。它碰巧工作,因为调用
main
的CRT代码显然在main返回后不使用RBP。类似于Jester指出的printf碰巧只关心RDX中的variadic参数的副本,而不是XMM 1。(https://learn.microsoft.com/en-us/cpp/build/x64-calling-convention?view = msvc-170#varargs)只是为了好玩,我很好奇这样做的效率有多高。
事实证明,如果我们使用FMA和AVX 1,只有4个FP指令实际操作FP值,不计算加载和清零寄存器。否则6。
你不需要一个单独的
fabs(x)
,x^2 - 1
只有当|x| <= 1.0
时才是负的。一个接近1.0但不完全相等的数字在平方时会远离1.0,所以FP舍入误差不会导致任何数字的x^2 - 1 >= 0.0
|x|〈1.0,即使使用默认值以外的舍入模式(最接近,使用偶数尾数作为抢七)。 因此,可能在XMM 0中计算
x^2 - 1,然后
pshufd xmm1, xmm0, 0b01010101(复制double的高dword)/
psrad xmm1, 31来广播double的符号位(没有
psraq64位算术右移)。 然后
andnps xmm1, xmm0将其归零(与全零位)或不加修改(与全1位)根据该符号位的倒数。我猜在
copysign之前做
pshufd/
psrad,(有一条
andnpd指令,但它的功能与
andnps相同,但在机器码中长了1个字节,所以它是无意义的。) 我看了看GCC/clang编译器的输出(Godbolt)对于一个接受double并返回你想要的值的函数,对于一个简单的分支版本,比如问题描述,以及copysign版本。它们做的工作比必要的多一点,但是clang确实为copysign版本制作了完全无分支的代码,像我建议的那样将
if (x*x - 1 >= 0)转换为按位操作,包括
andnpd来应用条件。这很有趣:) 它使用
xorpd-zeroing +
cmpltsd来实际与零进行比较,而不是在符号位上进行算术右移以获得相同的结果,这可能更有效(xor-zeroing非常便宜,但
cmpltsd与其他FP数学竞争相同的执行端口)。 我自己试着优化它,看看如果没有AVX,我们是否需要很多
movaps`寄存器复制指令;事实证明不是,如果我们在循环中执行此操作,而不是每次都将常量重新加载到新的寄存器中,则只有一个。我使用的是Linux而不是Windows,所以库函数的调用约定(printf/scanf)使用不同的寄存器,并且没有阴影空间。我没有优化阴影空间,所以它更容易转换回Windows。
使用AVX + FMA,我们可以保存更多的指令
vblendvpd
保存一条指令(vcmpltsd
),但在Intel上可能会更慢,特别是在桤木Lake E-cores(4 uops)上。https://uops.info/。非VEX版本(SSE4.1blendvpd
,它隐式地接受XMM 0中的混合控制)在Skylake和更高版本(包括Alder Lake P和E内核)上只有1 uop。我认为我们可以重新安排x^2-1在XMM 0中,而两个源在其他寄存器中。非AVX版本
blendvpd xmm1, xmm2
必须覆盖其中一个输入,所以这对于Windows来说很好,因为我们希望结果在XMM 1中,而不是XMM 0中。我们没有使用任何YMM寄存器的上半部分,因此将128位AVX指令和传统SSE混合使用是安全的,而不会产生转换损失。
如果我们不介意返回
-0.0
而不是+0.0
作为负输入,我们可以保存另一个寄存器,使用vandpd
的x & signmask
结果,基本上是copysign(0.0, x)
。问题指定返回0
,这与IEEE FP数学中的-0.0
不同,即使它与-0.0
相等。两个版本都能在+-1.0附近的边界附近正常工作;FMA只是一种效率提升;我们不需要在减法之前不将
x*x
四舍五入到最接近的double
的额外精度。输入接近1 + DBL_EPSILON(可表示为双精度型的大于1.0的最小数字),我们得到2x DBL_EPSILON的输出(2x 2.22e-16约为4.44e-16),例如(1+epsilon)^2 - 1
是(1^2 + 2*epsilon + epsilon^2) - 1
,ε的平方在舍入误差中丢失。我的FMA/blendvpd版本有一个bug:如果输入为
-1.0
,则产生-0
而不是+0
。在默认舍入模式下,1.0 - 1.0
的正确结果为+1.0。cmpltsd
版本通过检查|x| > 1.0
而不是|x| >= 1.0
来避免这种情况(cmpltsd
vs.cmplesd
)。由于x^2-1
在那一点上在数学上是0,因此通过将结果强制为零来获得它与从计算结果中获得它一样有效。事实上,如果我们关心零的符号,则对于FP数学更好,因为我们是在玩copysign
的把戏,而不是实际执行1 - x^2
或x^2 - 1
,这两个都将产生+0.0
。但是如果我们想直接使用
x^2 - 1
减法的符号位,这就像|x| >= 1.0
比较。正如布鲁斯Dawson)在一本优秀的series of articles about FP math中所写的那样,借用了道格拉斯·亚当斯(Douglas Adams)的一句话:
[浮点]数学很难。
你不会相信这有多么的困难。我的意思是,你可能认为很难计算出芝加哥和洛杉矶的火车什么时候会相撞,但这对浮点数学来说只是花生。
我花了大约几个小时的时间来处理这个问题,以使asm“更简单”和更有效,而不仅仅是天真地分支问题描述的方式。我很有信心现在两个版本都是正确的,但我只是在写这个答案的时候很晚才抓住了FMA版本的
-0.0
输出,而当时我正在处理vblendvpd
版本。只有在这一点上注意到一个错误的评论比较版本,它实际上是做|x| > 1.0
或|x| <= 1.0
,而不是|x| >= 1.0
像我以为我在做。:P巧妙地使用浮点数学通常是可能的,并且通常可以导致更有效的代码,但是100%确定它在所有情况下都是正确的需要careful testing。
(我还没有测试过+-Infinity输入,但两个版本都应该可以工作,产生正确符号的无穷大。对于NaN输入,我们应该得到一个带有输入符号的NaN输出。或者+0.0,这取决于我们如何比较。
0.0 < NaN
是false,所以我认为所有NaN输入都会为任何NaN输入给予零输出。)