NASM 浮点数 - 操作码和操作数的无效组合
NASM floating point - invalid combination of opcode and operands
我正在尝试从 this article on x86 assembly floating point:
编译以下代码示例(NASM 语法)
;; c^2 = a^2 + b^2 - cos(C)*2*a*b
;; C is stored in ang
global _start
section .data
a: dq 4.56 ;length of side a
b: dq 7.89 ;length of side b
ang: dq 1.5 ;opposite angle to side c (around 85.94 degrees)
section .bss
c: resq 1 ;the result ‒ length of side c
section .text
_start:
fld qword [a] ;load a into st0
fmul st0, st0 ;st0 = a * a = a^2
fld qword [b] ;load b into st1
fmul st1, st1 ;st1 = b * b = b^2
fadd st1, st0 ;st1 = a^2 + b^2
fld qword [ang] ;load angle into st0
fcos ;st0 = cos(ang)
fmul qword [a] ;st0 = cos(ang) * a
fmul qword [b] ;st0 = cos(ang) * a * b
fadd st0, st0 ;st0 = cos(ang) * a * b + cos(ang) * a * b = 2(cos(ang) * a * b)
fsubp st1, st0 ;st1 = st1 - st0 = (a^2 + b^2) - (2 * a * b * cos(ang))
;and pop st0
fsqrt ;take square root of st0 = c
fst qword [c] ;store st0 in c ‒ and we're done!
当我执行以下命令时:
nasm -f elf32 cosineSample.s -o cosineSample.o
我收到第 fmul st1, st1
行的以下错误:
error: invalid combination of opcode and operands
我需要做什么来解决这个问题?我需要将特殊参数传递给 nasm
吗?代码示例有误吗?
不幸的是,该密码已损坏。 fmul
不能对 st1, st1
进行操作,但即使可以,也不会达到作者想要的效果。根据评论,他想计算 b*b
但此时 b
在 st0
中。注释 load b into st1
是错误的,fld
总是加载到 st0
(堆栈的顶部)。您需要将 fmul st1, st1
更改为 fmul st0, st0
。此外,为了得到正确的结果,下面的 fadd st1, st0
也必须反转。该代码还使 fpu 堆栈变脏。
另请注意,程序没有结尾,因此除非您添加显式 exit
系统调用,否则它将出现段错误。
这里是固定代码,转换为 gnu 汇编语法:
.intel_syntax noprefix
.global _start
.data
a: .double 4.56 # length of side a
b: .double 7.89 # length of side b
ang: .double 1.5 # opposite angle to side c (around 85.94 degrees)
.lcomm c, 8
.text
_start:
fld qword ptr [a] # load a into st0
fmul st # st0 = a * a = a^2
fld qword ptr [b] # load b into st0
fmul st # st0 = b * b = b^2
faddp # st0 = a^2 + b^2
fld qword ptr [ang] # load angle into st0
fcos # st0 = cos(ang)
fmul qword ptr [a] # st0 = cos(ang) * a
fmul qword ptr [b] # st0 = cos(ang) * a * b
fadd st # st0 = cos(ang) * a * b + cos(ang) * a * b = 2(cos(ang) * a * b)
fsubp # st1 = st1 - st0 = (a^2 + b^2) - (2 * a * b * cos(ang))
# and pop st0
fsqrt # take square root of st0 = c
fstp qword ptr [c] # store st0 in c - and we're done!
# end program
mov eax, 1
xor ebx, ebx
int 0x80
我修复了 Wikibooks 上的代码并添加了一些额外的评论(Jester 的回答很好),所以现在它可以正确地组装和 运行s(使用 GDB 测试,使用 layout ret
单步执行 / tui reg float
)。 This is the diff between revisions. The revision that introduced the fmul st1,st1
invalid instruction bug is here,但即使在此之前它也未能在完成时清除 x87 堆栈。
只是为了好玩,我想写一个更高效的版本,只加载 a
和 b
一次。
并且通过首先处理涉及 cos
结果的所有 而不是 来允许更多的指令级并行性。即在将其乘以 cos(ang)
之前准备 2*a*b
,因此这些计算可以同时 运行。假设 fcos
是关键路径,我的版本只有一个 fmul
和一个 fsubp
从 fcos
结果到 fsqrt
输入的延迟。
default rel ; in case we assemble this in 64-bit mode, use RIP-relative addressing
... declare stuff, omitted.
fld qword [a] ;load a into st0
fld st0 ; st1 = a because we'll need it again later.
fmul st0, st0 ;st0 = a * a = a^2
fld qword [b] ;load b into st0 (pushing the a^2 result up to st1)
fmul st2, st0 ; st2 = a*b
fmul st0, st0 ;st0 = b^2, st1 = a^2, st2 = a*b
faddp ;st0 = a^2 + b^2 st1 = a*b; st2 empty
fxch st1 ;st0 = a*b st1 = a^2 + b^2 ; could avoid this, but only by using cos(ang) earlier, worse for critical path latency
fadd st0,st0 ;st0 = 2*a*b st1 = a^2 + b^2
fld qword [ang]
fcos ;st0 = cos(ang) st1 = 2*a*b st2 = a^2+b^2
fmulp ;st0=cos(ang)*2*a*b st1 = a^2+b^2
fsubp st1, st0 ;st0 = (a^2 + b^2) - (2 * a * b * cos(ang))
fsqrt ;take square root of st0 = c
fstp qword [c] ;store st0 in c and pop, leaving the x87 stack empty again ‒ and we're done!
当然,x87 已经过时了。在现代 x86 上,通常您会对任何浮点数使用 SSE2 标量(或压缩!)。
x87 在现代 x86 上有两个优点:硬件精度为 80 位(相对于 64 位 double
),并且它适用于小代码大小(机器代码字节,而不是指令数或源大小)。良好的指令缓存通常意味着代码大小不是使 x87 值得 FP 代码性能的重要因素,因为它通常比 SSE2 慢,因为额外的指令处理笨重的 x87 堆栈。
对于初学者或代码大小的原因,x87 具有超越函数,如 fcos
和 fsin
,并且 log/exp 作为单个指令内置。它们采用许多 uops 进行微编码,可能不会比标量库函数快,但在某些 CPU 上,您可能会接受它们所做的速度/精度权衡以及绝对速度。至少如果你首先使用 x87,否则你必须反弹结果 to/from XMM 注册 store/reload.
sin/cos 的范围缩减不做任何扩展精度的事情来避免非常接近 Pi 的倍数的巨大相对误差,仅使用内部 80 位(64 位尾数)值Pi的。 (库实现可能会或可能不会这样做,具体取决于所需的速度与精度的权衡。)参见 Intel Underestimates Error Bounds by 1.3 quintillion。
(当然,32 位代码中的 x87 可以让您与 Pentium III 和其他 CPU 兼容,这些 CPU 没有用于双精度的 SSE2,只有用于浮点的 SSE1 或根本没有 XMM 寄存器。x86-64 具有 SSE2 作为基线,因此 x86-64 上不存在此优势。)
对于初学者来说,x87 的巨大缺点是跟踪 x87 堆栈寄存器,而不是让东西堆积。您很容易得到只运行一次的代码,但当您将它放入循环时会给出 NaN,因为您没有平衡 x87 堆栈操作。
extern cos
global cosine_law_sse2_scalar
cosine_law_sse2_scalar:
movsd xmm0, [ang]
call cos ; xmm0 = cos(ang). Avoid using this right away so OoO exec can do the rest of the work in parallel
movsd xmm1, [a]
movsd xmm2, [b]
movaps xmm3, xmm1 ; copying registers should always copy the full reg, not movsd merging into the old value.
mulsd xmm3, xmm2 ; xmm3 = a*b
mulsd xmm1, xmm1 ; a^2
mulsd xmm2, xmm2 ; b^2
addsd xmm3, xmm3 ; 2*a*b
addsd xmm1, xmm2 ; a^2 + b^2
mulsd xmm3, xmm0 ; 2*a*b*cos(ang)
subsd xmm1, xmm3 ; (a^2 + b^2) - 2*a*b*cos(ang)
sqrtsd xmm0, xmm3 ; sqrt(that), in xmm0 as a return value
ret
;; This has the work interleaved more than necessary for most CPUs to find the parallelism
这个版本在 call cos
returns 之后只有 11 微指令。 (https://agner.org/optimize/)。它非常紧凑,非常简单。不跟踪 x87 堆栈。它具有与 x87 相同的良好依赖链,在我们已经 2*a*b
.
之前不使用 cos 结果
我们甚至可以一起加载 a
和 b
,作为一个 128 位向量。但是随后将其解包以对两半执行不同的操作,或者从顶部元素获取 b^2
作为标量,是笨拙的。如果 SSE3 haddpd
只有 1 uop 那会很棒(让我们用一条指令执行 a*b + a*b
和 a^2 + b^2
,给定正确的输入),但在所有拥有它的 CPU 上它是 3哎呀。
(PS vs. PD 只对像 MULSS/SD 这样的实际数学指令有影响。对于 FP 洗牌和寄存器副本,只需使用任何 FP 指令将数据放在你想要的地方,并有偏好对于 PS/SS,因为它们在机器代码中的编码较短。这就是我使用 movaps
的原因;movapd
总是错过优化,浪费 1 个字节,除非您故意使指令更长以进行对齐.)
;; I didn't actually end up using SSE3 for movddup or haddpd, it turned out I couldn't save uops that way.
global cosine_law_sse3_less_shuffle
cosine_law_sse3_less_shuffle:
;; 10 uops after the call cos, if both extract_high_half operations use pshufd or let movhlps have a false dependency
;; or if we had AVX for vunpckhpd xmm3, xmm1,xmm1
;; and those 10 are a mix of shuffle and MUL/ADD.
movsd xmm0, [ang]
call cos ; xmm0 = cos(ang). Avoid using this right away so OoO exec can do the rest of the work in parallel
movups xmm1, [a] ; {a, b} (they were in contiguous memory in this order. low element = a)
movaps xmm3, xmm1
; xorps xmm3, xmm3 ; break false dependency by zeroing. (xorps+movhlps is maybe better than movaps + unpckhpd, at least on SnB but maybe not Bulldozer / Ryzen)
; movhlps xmm3, xmm1 ; xmm3 = b
; pshufd xmm3, xmm1, 0b01001110 ; xmm3 = {b, a} ; bypass delay on Nehalem, but fine on most others
mulsd xmm3, [b] ; xmm3 = a*b ; reloading b is maybe cheaper than shufling it out of the high half of xmm1
addsd xmm3, xmm3 ; 2*b*a
mulsd xmm3, xmm0 ; 2*b*a*cos(ang)
mulpd xmm1, xmm1 ; {a^2, b^2}
;xorps xmm2, xmm2 ; we don't want to just use xmm0 here; that would couple this dependency chain to the slow cos(ang) critical path sooner.
movhlps xmm2, xmm1
addsd xmm1, xmm2 ; a^2 + b^2
subsd xmm1, xmm3 ; (a^2 + b^2) - 2*a*b*cos(ang)
sqrtsd xmm0, xmm1 ; sqrt(that), in xmm0 as a return value
ret
而且我们可以使用 AVX 做得更好,保存 MOVAPS 寄存器副本,因为指令的 3 操作数非破坏性 VEX 版本允许我们将结果放入新寄存器,而不会破坏任何一个输入。这对于 FP 洗牌来说真的很棒,因为 SSE* 没有任何用于 FP 操作数的复制和洗牌,只有 pshufd
这会在某些 CPU 上导致额外的旁路延迟。因此,它保存了 MOVAPS 和(已注释掉的)XORPS,这打破了对为 MOVHLPS 生成 XMM2 旧值的任何内容的依赖。 (MOVHLPS 将目标的低 64 位替换为 src 的高 64 位,因此它对两个寄存器都有输入依赖性)。
global cosine_law_avx
cosine_law_avx:
;; 9 uops after the call cos. Reloading [b] is good here instead of shuffling it, saving total uops / instructions
vmovsd xmm0, [ang]
call cos ; xmm0 = cos(ang). Avoid using this right away so OoO exec can do the rest of the work in parallel
vmovups xmm1, [a] ; {a, b} (they were in contiguous memory in this order. low element = a)
vmulsd xmm3, xmm1, [b] ; xmm3 = a*b
vaddsd xmm3, xmm3 ; 2*b*a. (really vaddsd xmm3,xmm3,xmm3 but NASM lets us shorten when dst=src1)
vmulsd xmm3, xmm0 ; 2*b*a*cos(ang)
vmulpd xmm1, xmm1 ; {a^2, b^2}
vunpckhpd xmm2, xmm1,xmm1 ; xmm2 = { b^2, b^2 }
vaddsd xmm1, xmm2 ; a^2 + b^2
vsubsd xmm1, xmm3 ; (a^2 + b^2) - 2*a*b*cos(ang)
vsqrtsd xmm0, xmm1,xmm1 ; sqrt(that), in xmm0 as a return value. (Avoiding an output dependency on xmm0, even though it was an ancestor in the dep chain. Maybe lets the CPU free that physical reg sooner)
ret
我只测试了第一个 x87 版本,所以我可能错过了其他版本中的一个步骤。
我正在尝试从 this article on x86 assembly floating point:
编译以下代码示例(NASM 语法);; c^2 = a^2 + b^2 - cos(C)*2*a*b
;; C is stored in ang
global _start
section .data
a: dq 4.56 ;length of side a
b: dq 7.89 ;length of side b
ang: dq 1.5 ;opposite angle to side c (around 85.94 degrees)
section .bss
c: resq 1 ;the result ‒ length of side c
section .text
_start:
fld qword [a] ;load a into st0
fmul st0, st0 ;st0 = a * a = a^2
fld qword [b] ;load b into st1
fmul st1, st1 ;st1 = b * b = b^2
fadd st1, st0 ;st1 = a^2 + b^2
fld qword [ang] ;load angle into st0
fcos ;st0 = cos(ang)
fmul qword [a] ;st0 = cos(ang) * a
fmul qword [b] ;st0 = cos(ang) * a * b
fadd st0, st0 ;st0 = cos(ang) * a * b + cos(ang) * a * b = 2(cos(ang) * a * b)
fsubp st1, st0 ;st1 = st1 - st0 = (a^2 + b^2) - (2 * a * b * cos(ang))
;and pop st0
fsqrt ;take square root of st0 = c
fst qword [c] ;store st0 in c ‒ and we're done!
当我执行以下命令时:
nasm -f elf32 cosineSample.s -o cosineSample.o
我收到第 fmul st1, st1
行的以下错误:
error: invalid combination of opcode and operands
我需要做什么来解决这个问题?我需要将特殊参数传递给 nasm
吗?代码示例有误吗?
不幸的是,该密码已损坏。 fmul
不能对 st1, st1
进行操作,但即使可以,也不会达到作者想要的效果。根据评论,他想计算 b*b
但此时 b
在 st0
中。注释 load b into st1
是错误的,fld
总是加载到 st0
(堆栈的顶部)。您需要将 fmul st1, st1
更改为 fmul st0, st0
。此外,为了得到正确的结果,下面的 fadd st1, st0
也必须反转。该代码还使 fpu 堆栈变脏。
另请注意,程序没有结尾,因此除非您添加显式 exit
系统调用,否则它将出现段错误。
这里是固定代码,转换为 gnu 汇编语法:
.intel_syntax noprefix
.global _start
.data
a: .double 4.56 # length of side a
b: .double 7.89 # length of side b
ang: .double 1.5 # opposite angle to side c (around 85.94 degrees)
.lcomm c, 8
.text
_start:
fld qword ptr [a] # load a into st0
fmul st # st0 = a * a = a^2
fld qword ptr [b] # load b into st0
fmul st # st0 = b * b = b^2
faddp # st0 = a^2 + b^2
fld qword ptr [ang] # load angle into st0
fcos # st0 = cos(ang)
fmul qword ptr [a] # st0 = cos(ang) * a
fmul qword ptr [b] # st0 = cos(ang) * a * b
fadd st # st0 = cos(ang) * a * b + cos(ang) * a * b = 2(cos(ang) * a * b)
fsubp # st1 = st1 - st0 = (a^2 + b^2) - (2 * a * b * cos(ang))
# and pop st0
fsqrt # take square root of st0 = c
fstp qword ptr [c] # store st0 in c - and we're done!
# end program
mov eax, 1
xor ebx, ebx
int 0x80
我修复了 Wikibooks 上的代码并添加了一些额外的评论(Jester 的回答很好),所以现在它可以正确地组装和 运行s(使用 GDB 测试,使用 layout ret
单步执行 / tui reg float
)。 This is the diff between revisions. The revision that introduced the fmul st1,st1
invalid instruction bug is here,但即使在此之前它也未能在完成时清除 x87 堆栈。
只是为了好玩,我想写一个更高效的版本,只加载 a
和 b
一次。
并且通过首先处理涉及 cos
结果的所有 而不是 来允许更多的指令级并行性。即在将其乘以 cos(ang)
之前准备 2*a*b
,因此这些计算可以同时 运行。假设 fcos
是关键路径,我的版本只有一个 fmul
和一个 fsubp
从 fcos
结果到 fsqrt
输入的延迟。
default rel ; in case we assemble this in 64-bit mode, use RIP-relative addressing
... declare stuff, omitted.
fld qword [a] ;load a into st0
fld st0 ; st1 = a because we'll need it again later.
fmul st0, st0 ;st0 = a * a = a^2
fld qword [b] ;load b into st0 (pushing the a^2 result up to st1)
fmul st2, st0 ; st2 = a*b
fmul st0, st0 ;st0 = b^2, st1 = a^2, st2 = a*b
faddp ;st0 = a^2 + b^2 st1 = a*b; st2 empty
fxch st1 ;st0 = a*b st1 = a^2 + b^2 ; could avoid this, but only by using cos(ang) earlier, worse for critical path latency
fadd st0,st0 ;st0 = 2*a*b st1 = a^2 + b^2
fld qword [ang]
fcos ;st0 = cos(ang) st1 = 2*a*b st2 = a^2+b^2
fmulp ;st0=cos(ang)*2*a*b st1 = a^2+b^2
fsubp st1, st0 ;st0 = (a^2 + b^2) - (2 * a * b * cos(ang))
fsqrt ;take square root of st0 = c
fstp qword [c] ;store st0 in c and pop, leaving the x87 stack empty again ‒ and we're done!
当然,x87 已经过时了。在现代 x86 上,通常您会对任何浮点数使用 SSE2 标量(或压缩!)。
x87 在现代 x86 上有两个优点:硬件精度为 80 位(相对于 64 位 double
),并且它适用于小代码大小(机器代码字节,而不是指令数或源大小)。良好的指令缓存通常意味着代码大小不是使 x87 值得 FP 代码性能的重要因素,因为它通常比 SSE2 慢,因为额外的指令处理笨重的 x87 堆栈。
对于初学者或代码大小的原因,x87 具有超越函数,如 fcos
和 fsin
,并且 log/exp 作为单个指令内置。它们采用许多 uops 进行微编码,可能不会比标量库函数快,但在某些 CPU 上,您可能会接受它们所做的速度/精度权衡以及绝对速度。至少如果你首先使用 x87,否则你必须反弹结果 to/from XMM 注册 store/reload.
sin/cos 的范围缩减不做任何扩展精度的事情来避免非常接近 Pi 的倍数的巨大相对误差,仅使用内部 80 位(64 位尾数)值Pi的。 (库实现可能会或可能不会这样做,具体取决于所需的速度与精度的权衡。)参见 Intel Underestimates Error Bounds by 1.3 quintillion。
(当然,32 位代码中的 x87 可以让您与 Pentium III 和其他 CPU 兼容,这些 CPU 没有用于双精度的 SSE2,只有用于浮点的 SSE1 或根本没有 XMM 寄存器。x86-64 具有 SSE2 作为基线,因此 x86-64 上不存在此优势。)
对于初学者来说,x87 的巨大缺点是跟踪 x87 堆栈寄存器,而不是让东西堆积。您很容易得到只运行一次的代码,但当您将它放入循环时会给出 NaN,因为您没有平衡 x87 堆栈操作。
extern cos
global cosine_law_sse2_scalar
cosine_law_sse2_scalar:
movsd xmm0, [ang]
call cos ; xmm0 = cos(ang). Avoid using this right away so OoO exec can do the rest of the work in parallel
movsd xmm1, [a]
movsd xmm2, [b]
movaps xmm3, xmm1 ; copying registers should always copy the full reg, not movsd merging into the old value.
mulsd xmm3, xmm2 ; xmm3 = a*b
mulsd xmm1, xmm1 ; a^2
mulsd xmm2, xmm2 ; b^2
addsd xmm3, xmm3 ; 2*a*b
addsd xmm1, xmm2 ; a^2 + b^2
mulsd xmm3, xmm0 ; 2*a*b*cos(ang)
subsd xmm1, xmm3 ; (a^2 + b^2) - 2*a*b*cos(ang)
sqrtsd xmm0, xmm3 ; sqrt(that), in xmm0 as a return value
ret
;; This has the work interleaved more than necessary for most CPUs to find the parallelism
这个版本在 call cos
returns 之后只有 11 微指令。 (https://agner.org/optimize/)。它非常紧凑,非常简单。不跟踪 x87 堆栈。它具有与 x87 相同的良好依赖链,在我们已经 2*a*b
.
我们甚至可以一起加载 a
和 b
,作为一个 128 位向量。但是随后将其解包以对两半执行不同的操作,或者从顶部元素获取 b^2
作为标量,是笨拙的。如果 SSE3 haddpd
只有 1 uop 那会很棒(让我们用一条指令执行 a*b + a*b
和 a^2 + b^2
,给定正确的输入),但在所有拥有它的 CPU 上它是 3哎呀。
(PS vs. PD 只对像 MULSS/SD 这样的实际数学指令有影响。对于 FP 洗牌和寄存器副本,只需使用任何 FP 指令将数据放在你想要的地方,并有偏好对于 PS/SS,因为它们在机器代码中的编码较短。这就是我使用 movaps
的原因;movapd
总是错过优化,浪费 1 个字节,除非您故意使指令更长以进行对齐.)
;; I didn't actually end up using SSE3 for movddup or haddpd, it turned out I couldn't save uops that way.
global cosine_law_sse3_less_shuffle
cosine_law_sse3_less_shuffle:
;; 10 uops after the call cos, if both extract_high_half operations use pshufd or let movhlps have a false dependency
;; or if we had AVX for vunpckhpd xmm3, xmm1,xmm1
;; and those 10 are a mix of shuffle and MUL/ADD.
movsd xmm0, [ang]
call cos ; xmm0 = cos(ang). Avoid using this right away so OoO exec can do the rest of the work in parallel
movups xmm1, [a] ; {a, b} (they were in contiguous memory in this order. low element = a)
movaps xmm3, xmm1
; xorps xmm3, xmm3 ; break false dependency by zeroing. (xorps+movhlps is maybe better than movaps + unpckhpd, at least on SnB but maybe not Bulldozer / Ryzen)
; movhlps xmm3, xmm1 ; xmm3 = b
; pshufd xmm3, xmm1, 0b01001110 ; xmm3 = {b, a} ; bypass delay on Nehalem, but fine on most others
mulsd xmm3, [b] ; xmm3 = a*b ; reloading b is maybe cheaper than shufling it out of the high half of xmm1
addsd xmm3, xmm3 ; 2*b*a
mulsd xmm3, xmm0 ; 2*b*a*cos(ang)
mulpd xmm1, xmm1 ; {a^2, b^2}
;xorps xmm2, xmm2 ; we don't want to just use xmm0 here; that would couple this dependency chain to the slow cos(ang) critical path sooner.
movhlps xmm2, xmm1
addsd xmm1, xmm2 ; a^2 + b^2
subsd xmm1, xmm3 ; (a^2 + b^2) - 2*a*b*cos(ang)
sqrtsd xmm0, xmm1 ; sqrt(that), in xmm0 as a return value
ret
而且我们可以使用 AVX 做得更好,保存 MOVAPS 寄存器副本,因为指令的 3 操作数非破坏性 VEX 版本允许我们将结果放入新寄存器,而不会破坏任何一个输入。这对于 FP 洗牌来说真的很棒,因为 SSE* 没有任何用于 FP 操作数的复制和洗牌,只有 pshufd
这会在某些 CPU 上导致额外的旁路延迟。因此,它保存了 MOVAPS 和(已注释掉的)XORPS,这打破了对为 MOVHLPS 生成 XMM2 旧值的任何内容的依赖。 (MOVHLPS 将目标的低 64 位替换为 src 的高 64 位,因此它对两个寄存器都有输入依赖性)。
global cosine_law_avx
cosine_law_avx:
;; 9 uops after the call cos. Reloading [b] is good here instead of shuffling it, saving total uops / instructions
vmovsd xmm0, [ang]
call cos ; xmm0 = cos(ang). Avoid using this right away so OoO exec can do the rest of the work in parallel
vmovups xmm1, [a] ; {a, b} (they were in contiguous memory in this order. low element = a)
vmulsd xmm3, xmm1, [b] ; xmm3 = a*b
vaddsd xmm3, xmm3 ; 2*b*a. (really vaddsd xmm3,xmm3,xmm3 but NASM lets us shorten when dst=src1)
vmulsd xmm3, xmm0 ; 2*b*a*cos(ang)
vmulpd xmm1, xmm1 ; {a^2, b^2}
vunpckhpd xmm2, xmm1,xmm1 ; xmm2 = { b^2, b^2 }
vaddsd xmm1, xmm2 ; a^2 + b^2
vsubsd xmm1, xmm3 ; (a^2 + b^2) - 2*a*b*cos(ang)
vsqrtsd xmm0, xmm1,xmm1 ; sqrt(that), in xmm0 as a return value. (Avoiding an output dependency on xmm0, even though it was an ancestor in the dep chain. Maybe lets the CPU free that physical reg sooner)
ret
我只测试了第一个 x87 版本,所以我可能错过了其他版本中的一个步骤。