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 但此时 bst0 中。注释 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 堆栈。


只是为了好玩,我想写一个更高效的版本,只加载 ab 一次。

并且通过首先处理涉及 cos 结果的所有 而不是 来允许更多的指令级并行性。即在将其乘以 cos(ang) 之前准备 2*a*b,因此这些计算可以同时 运行。假设 fcos 是关键路径,我的版本只有一个 fmul 和一个 fsubpfcos 结果到 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 具有超越函数,如 fcosfsin,并且 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 结果

我们甚至可以一起加载 ab,作为一个 128 位向量。但是随后将其解包以对两半执行不同的操作,或者从顶部元素获取 b^2 作为标量,是笨拙的。如果 SSE3 haddpd 只有 1 uop 那会很棒(让我们用一条指令执行 a*b + a*ba^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 版本,所以我可能错过了其他版本中的一个步骤。