NASM с плавающей точкой - недопустимая комбинация кода операции и операндов - PullRequest
0 голосов
/ 02 февраля 2019

Я пытаюсь скомпилировать следующий пример кода (синтаксис NASM) из этой статьи о x86 с плавающей запятой сборки :

;; 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?Пример кода неправильный?

Ответы [ 2 ]

0 голосов
/ 02 февраля 2019

Я исправил код в Wikibooks и добавил несколько дополнительных комментариев (ответ Шута хороший), так что теперь он собирается и работает правильно (протестировано с GDB, одноступенчатое с layout ret / tui reg float). Это разница между ревизиями .Ревизия, которая представила ошибку fmul st1,st1 недопустимая инструкция , находится здесь , но даже до этого ей не удалось очистить стек x87, когда это было сделано.


Просто ради интереса, яхотел написать более эффективную версию, которая загружает a и b только один раз.

и которая обеспечивает больший параллелизм на уровне команд, выполняя все , а не , включая результат cosпервый.то есть подготовьте 2*a*b перед тем, как умножить это на cos(ang), чтобы оба этих вычисления могли выполняться параллельно.Предполагая, что 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!

OfКонечно, x87 в значительной степени устарел.На современном x86 обычно вы используете скаляр SSE2 (или упакованный!) Для чего-либо с плавающей запятой.

x87 имеет две вещи для современного x86: аппаратная точность 80 бит (против64-битный double), и это хорошо для небольшого размера кода (байты машинного кода, а не количество инструкций или размер исходного кода).Хорошие кэши команд обычно означают, что размер кода не является достаточно важным фактором, чтобы сделать x87 оправданным для производительности кода FP, потому что он, как правило, медленнее, чем SSE2, из-за дополнительных инструкций, связанных с неуклюжим стеком x87.

И для новичков, или из-за размера кода, x87 имеет трансцендентные функции, такие как fcos и fsin, и log / exp встроены в виде отдельных инструкций.Они микрокодируются многими мопами, и, вероятно, не быстрее, чем функция скалярной библиотеки, но на некоторых процессорах вы можете согласиться с компромиссом между скоростью и точностью, который они делают, и абсолютной скоростью.По крайней мере, если вы используете x87 в первую очередь, в противном случае вы должны отразить результаты в / из регистров XMM с сохранением / перезагрузкой.

Уменьшение диапазона для sin / cos не делает никакого расширенного-точный материал, чтобы избежать огромных относительных ошибок, очень близких к кратному Пи, просто используя внутреннее 80-битное (64-битное значение и) значение Пи.(Реализация библиотеки может или не может сделать это, в зависимости от желаемого соотношения скорости и точности.) См. Intel недооценивает границы ошибок на 1,3 квинтиллиона .

(И, конечно, x87 в 32-битовый код обеспечивает совместимость с Pentium III и другими процессорами, у которых не было 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

В этой версии только 11 мопов послеcall cos возвращается.(https://agner.org/optimize/). Это довольно компактно и довольно просто. Не отслеживает стек x87. И имеет те же цепочки зависимостей, что и x87, не используя результат cos, пока у нас уже не будет 2*a*b.

Мы можем даже поиграть с загрузкой a и b вместе, как одним 128-битным вектором, но затем распаковать его, чтобы сделать разные вещи с двумя половинками, или получить b^2 из верхнего элемента какскалярный, неуклюжий. Если бы SSE3 haddpd был только 1 моп, это было бы здорово (и давайте сделаем a*b + a*b и a^2 + b^2 с одной инструкцией, учитывая правильные входные данные), но на всех процессорах, у которых это есть, это 3микрооперации.

(PS против 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 shuffle, потому что в SSE * нет опций копирования и перемешивания для операндов FP, только pshufd, что может вызвать дополнительную задержку обхода на некоторых процессорах.Таким образом, он сохраняет MOVAPS и (закомментированные) XORPS, которые нарушают зависимость от того, что произвело старое значение XMM2 для MOVHLPS.(MOVHLPS заменяет младшие 64 бита места назначения старшими 64 битами src, поэтому он имеет входную зависимость от обоих регистров).

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, поэтому возможновозможно, пропустил шаг в одном из других.

0 голосов
/ 02 февраля 2019

К сожалению, этот код не работает.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
...