12.2 Skaláris lebegőpontos utasítások

12.2.1 Motiváció: ahol az egész aritmetika szerződése megszűnik

A A lebegőpontos regiszterkészlet és az IEEE 754 ábrázolás alfejezetben egy konkrét szám bitmezőin keresztül láttuk, hogy a float és a double nem kettes komplemens egész, és nem az Xn regisztereken utazik. Az ott felépített reprezentáció azonban önmagában passzív: ahhoz, hogy egy mérnöki feladatot megoldjunk, utasítások kellenek, amelyek ezeken a bitmintákon aritmetikát végeznek.

Vegyük a tipikus kalibrációs láncot: az ADC egy nyers egész értéket ad, abból (x - offset) * scale alakú lineáris transzformációval lesz fizikai mennyiség, majd statisztikához (átlag, szórás) lebegőpontoson számolunk, végül a megjelenítéshez vagy egy protokoll-mezőhöz egészre kell visszakonvertálni. Ez a lánc két határátlépést tartalmaz az egész és a lebegőpontos világ között, és ezek a határátlépések az assembly szintjén nem automatikusak: külön utasítás kell rájuk. Ez az alfejezet pontosan ezt a réteget tanítja: a skaláris lebegőpontos aritmetikát, a bit szintű mozgatást és a konverziókat.

A hatókör szűk: FADD, FSUB, FMUL, FDIV, FMOV, SCVTF/UCVTF, FCVTZS/FCVTZU, valamint az FCMP és az abból származó feltételkódok. A vektoros (NEON) párhuzamosítás a A NEON regiszterkészlet és adatelrendezés és a Vektorizált összeadás/szorzás: egy skaláris ciklus NEON-átirata témája; a feltételkódok általános mechanikáját pedig a Feltételes végrehajtás: CSET/CSEL és feltételkódok alfejezetben már felépítettük — itt csak azt nézzük meg, hogyan viselkednek lebegőpontos összehasonlítás után.

12.2.2 Definíció: mitől „skaláris”, és hogyan kódolja a mnemonic a pontosságot

Skaláris lebegőpontos utasítás (scalar floating-point instruction): olyan A64 ISA utasítás, amely egyetlen lebegőpontos értéken (egy Sn vagy Dn regiszteren) hajt végre műveletet, szemben a vektorossal, amely egyszerre több elemet dolgoz fel egy Vn regiszterben.

Az AArch64 sajátossága, hogy a mnemonic nem tartalmaz pontosság-suffixet. Nincs fadd.f32; a pontosságot a regiszternév kódolja:

Regiszternév Nézet Pontosság
Sn (pl. S0) Vn alsó 32 bitje binary32 (float)
Dn (pl. D0) Vn alsó 64 bitje binary64 (double)

Ebből két kemény szabály következik:

  1. Precízióegyezés. Az operandusoknak és az eredménynek azonos pontosságúnak kell lenniük: FADD Sd, Sn, Sm és FADD Dd, Dn, Dm érvényes, a FADD S0, S1, D2 viszont assemble-idejű hiba. Nincs implicit önpontosítás (widening).
  2. Nincs immediate operandus. Az FADD/FSUB/FMUL/FDIV egyik formája sem fogad el azonnali konstanst. A konstans háromféleképpen kerülhet be: az FMOV korlátozott immediate-készletével, egy egész regiszterből SCVTF/UCVTF-fal, vagy memóriából (LDR Sn, [Xn]). Az FMOV immediate-kódolása 8 bites, ezért nem minden konstans írható le vele — ez az MOV-variánsok és immediate-kódolás korlátjának lebegőpontos megfelelője.

Az utasításcsoportok áttekintése:

Utasítás Mit csinál Regisztertípus
FADD, FSUB, FMUL, FDIV aritmetika Sn/Dn
FMOV bit szintű másolás (nem konverzió!) FP ↔︎ egész / FP ↔︎ FP
SCVTF, UCVTF egész → lebegőpontos Wn/Xn → Sn/Dn
FCVTZS, FCVTZU lebegőpontos → egész, csonkítva Sn/Dn → Wn/Xn
FCVT float ↔︎ double Sn ↔︎ Dn
FCMP, FCMPE összehasonlítás, NZCV beállítás Sn/Dn

12.2.3 Aritmetika: FADD, FSUB, FMUL, FDIV

Az alapműveletek szemantikája az IEEE 754-2019 binary32/binary64 szabvány szerint helyesen kerekített (correctly rounded): az eredmény a pontos matematikai eredmény kerekítése az aktuális kerekítési mód szerint. A módot az FPCR (Floating-point Control Register) RMode mezője választja ki: round-to-nearest-ties-to-even (RN, ez az alapérték), +∞ felé (RP), −∞ felé (RM) vagy nulla felé (RZ). „A legközelebbi ábrázolható szomszéd” csak RN módban igaz; a többi módban az eredmény a pontos érték megfelelő irányú szomszédja. Az FPCR további bitjei (például a flush-to-zero FZ és az alapértelmezett NaN DN) szintén eltéríthetik az eredményt az IEEE-alapviselkedéstől; Linux alatt a felhasználói programok alapértelmezésben IEEE-módban indulnak. A program MRS Xt, FPCR / MSR FPCR, Xt utasításokkal érheti el a regisztert.

Néhány, a C-fejlesztőt ténylegesen érintő tulajdonság:

Művelet Illusztratív latencia-nagyságrend (ellenőrizendő) Jelleg
FADD/FSUB néhány ciklus teljesen futószalagosítható
FMUL néhány ciklus teljesen futószalagosítható
FDIV egy nagyságrenddel több részben vagy nem futószalagosítható

Ez az eltérés a Teljesítmény-tudatos kód mérési szakaszában lesz fontos: egy FDIV csere FMUL-ra (reciprokkal) gyakran nagyobb nyereséget ad, mint bármilyen vektorizáció.

12.2.4 FMOV: bit szintű mozgatás, nem konverzió

Az FMOV a leggyakrabban félreértett utasítás ebben a csoportban. Az FMOV nem konvertál, hanem a biteket másolja, és a célregiszter ezeket úgy értelmezi, ahogyan az ő típusa előírja.

Forma Hatás
FMOV Wd, Sn Sn alsó 32 bitje → Wd (a bitminta egészként olvasva)
FMOV Sd, Wn Wn → Sd alsó 32 bitje (a bitminta lebegőpontosként értelmezve)
FMOV Xd, Dn Dn → Xd
FMOV Dd, Xn Xn → Dd
FMOV Sd, Sn / FMOV Dd, Dn FP → FP, bitazonos
FMOV Sd, #imm korlátozott, 8 bites kódolású konstanskészlet

A gyakorlati haszna az irányított olvasás: FMOV Wd, Sn-nel hozzáférünk az IEEE 754 bitmezőkhöz (előjel, exponens, mantissza) egész műveletekkel — ez a Logikai alapműveletek és bitmaszkolás eszköztárával kombinálva adja a bitmanipulációs trükköket. Fordított irányban FMOV Sd, Wn akkor hasznos, ha egy memóriából olvasott nyers 32 bites szót akarunk float-ként értelmezni (pl. protokoll-deszerializálás).

A tipikus hiba pontosan itt keletkezik: FMOV-ot használni konverzióra. Ha W0 = 10, akkor az FMOV S0, W0 nem 10.0f-et ad, hanem a 0x0000000A bitmintát, ami egy rendkívül kicsi denormális szám (≈1.4e-44). Egész → lebegőpontos átalakításra az SCVTF/UCVTF való.

12.2.5 Konverzió: SCVTF/UCVTF és FCVTZS/FCVTZU

Utasítás Irány Forrás előjelkezelés
SCVTF Sd, Wn / SCVTF Dd, Wn / SCVTF Sd, Xn / SCVTF Dd, Xn egész → FP előjeles
UCVTF Sd, Wn / … egész → FP előjel nélküli
FCVTZS Wd, Sn / FCVTZS Xd, Dn / … FP → egész előjeles, Z = toward zero
FCVTZU Wd, Sn / … FP → egész előjel nélküli, toward zero

Az SCVTF a forrásregiszter méretéből és az S/D célból egyértelműen azonosítja a konverziót: SCVTF Dd, Xn egy 64 bites előjeles egészt alakít double-ré. Az egész → FP irányban a kerekítés az FPCR módja szerint történik, és 64 bites forrásból Sn-be konvertálva pontosságvesztés lehetséges.

Az FP → egész irányban a Z az FCVTZS kerekítési módját jelöli: csonkítás nullához (round toward zero). Ez azért kritikus, mert a C (int)y kasztja is csonkít nullához. Ha más kerekítés kell, külön utasítás létezik: FCVTNS (nearest), FCVTPS (plus infinity), FCVTMS (minus infinity), illetve ezek …U előjel nélküli párjai, valamint a FRINT* család, ha nem egészbe, hanem lebegőpontosba kerekítünk.

Két fontos részlet:

Lebegőpontos ↔︎ lebegőpontos átalakításra az FCVT Dd, Sn és FCVT Sd, Dn szolgál — ez az egyetlen módja a float és double közötti váltásnak, mert az aritmetikai utasítások nem keverik a pontosságot.

12.2.6 Összehasonlítás: FCMP és a feltételkódok

Az FCMP Sn, Sm, FCMP Dn, Dm és FCMP Sn, #0.0 formák lebegőpontos összehasonlítást végeznek, és az eredményt az NZCV-be írják. Innen a Feltételes végrehajtás feltételkódjai használhatók — de nem ugyanúgy, mint egész összehasonlítás után.

A NaN jelenléte miatt az összehasonlításnak négy kimenetele van: egyenlő, nagyobb, kisebb és rendezetlen (unordered). Az ARM az alábbi flag-kiosztást használja:

Összehasonlítás N Z C V
egyenlő 0 1 1 0
nagyobb 0 0 1 0
kisebb 1 0 0 0
rendezetlen (NaN) 0 0 1 1

Ebből a feltételkódok lebegőpontos jelentése:

Feltételkód NZCV-feltétel Lebegőpontos jelentés C-beli megfelelő
EQ Z==1 egyenlő a == b
NE Z==0 nem egyenlő vagy rendezetlen a != b
MI N==1 kisebb (rendezett) a < b
CC/LO C==0 kisebb (rendezett) a < b
GT Z==0 && N==V nagyobb (rendezett) a > b
GE N==V nagyobb vagy egyenlő (rendezett) a >= b
LS C==0 \|\| Z==1 kisebb vagy egyenlő (rendezett) a <= b
LT N!=V kisebb vagy rendezetlen —
LE Z==1 \|\| N!=V kisebb vagy egyenlő vagy rendezetlen —
HI C==1 && Z==0 nagyobb vagy rendezetlen —
CS/HS C==1 nagyobb vagy egyenlő vagy rendezetlen —
VS V==1 rendezetlen (NaN) isnan(a) \|\| isnan(b)
VC V==0 rendezett egyik sem NaN

A tanulság: a C > operátora rendezett nagyobb-than, ezért a helyes feltételkód a GT, nem a HI — HI ugyanis NaN esetén is igaz lenne. A != viszont szándékosan NE, mert az IEEE 754 szerint NaN != x igaz. A FCMPE a jelző (signaling) változat: csendes NaN esetén is kiváltja az invalid operation kivételt, FCMP nem.

12.2.7 Kidolgozott példa: átlag számítása és csonkítása

Munkakönyvtár: ~/arm-labor/a64/a045/ — az alfejezet minden laborfájlja ide kerül, és minden parancs innen futtatandó (a könyvtárrendet a Labor-alapkörnyezet alfejezet írja le).

mkdir -p ~/arm-labor/a64/a045
cd ~/arm-labor/a64/a045

A példa három egész átlagát számolja float-ként, majd csonkítva visszaalakítja egésszé, és a kilépési kódban adja vissza.

// lab-a045-avg.s — skaláris lebegőpontos átlag és csonkítás
// target: qemu-aarch64 (user-mode), A64 ISA, little-endian
    .text
    .global _start

_start:
    mov     w0, #10
    mov     w1, #20
    mov     w2, #13

    scvtf   s0, w0              // s0 = 10.0f
    scvtf   s1, w1              // s1 = 20.0f
    scvtf   s2, w2              // s2 = 13.0f

    fadd    s3, s0, s1          // s3 = 30.0f
    fadd    s3, s3, s2          // s3 = 43.0f

    mov     w3, #3
    scvtf   s4, w3              // s4 = 3.0f  (nincs FP immediate!)
    fdiv    s5, s3, s4          // s5 = 14.333333f

    fcvtzu  w0, s5              // w0 = 14 (csonkítás nullához)
    mov     x8, #93             // __NR_exit
    svc     #0
    .section .note.GNU-stack,"",%progbits

Assemble, link és futtatás:

aarch64-linux-gnu-as -o lab-a045-avg.o lab-a045-avg.s
aarch64-linux-gnu-ld -o lab-a045-avg lab-a045-avg.o
# elvárt: GNU_STACK RW
readelf -lW lab-a045-avg \
  | awk '$1=="GNU_STACK"{f=$7} END{print "GNU_STACK", f; exit !(f=="RW")}'
qemu-aarch64 ./lab-a045-avg ; echo $?

Várt kimenet: 14. A 43/3 = 14.333… pontos értéke binary32-ban nem ábrázolható, ezért az FDIV itt IXC-t állít az FPSR-ben; a csonkítás után mégis 14 lesz. A kilépési státusz a w0 alsó 8 bitje, ezért a 14 közvetlenül olvasható.

Kiegészítő részlet (nem önálló program), amely a többi műveletet és az összehasonlítást mutatja:

    // feltételezés: s0 = a, s1 = b  (mindkettő float)
    fsub    s2, s0, s1          // s2 = a - b
    fmul    s3, s0, s1          // s3 = a * b
    fdiv    s4, s0, s1          // s4 = a / b
    fcmp    s0, s1              // a ? b
    cset    w5, gt              // w5 = 1, ha a > b (rendezett)
    // s6 = (a > b) ? a : b; rendezetlen összehasonlításnál b, nem FMAX
    fcsel   s6, s0, s1, gt
    fcvt    d7, s0              // d7 = (double)a, pontosságnövelés

12.2.8 Tipikus hibák és ellenpróbák

1. FMOV konverzióra használata. Tünet: az SCVTF helyett FMOV S0, W0 után az FCVTZU eredménye 0 lesz, holott a bemenet 10 volt. Hipotézis: a bitminta denormálisként értelmeződött, és a csonkítás nullát ad. Adatgyűjtés: aarch64-linux-gnu-objdump -d lab-a045-avg | grep -A3 -B3 fmov — ha fmov szerepel ott, ahol scvtf-nek kellene, megvan a hiba. Egy változtatás: cseréld scvtf-re, majd echo $? ellenőrzés.

2. Precízió-keverés. Az fadd s0, s0, d1 assemble-idejű hibát ad („operand 3 must be a scalar FP register of the same size”). Ez nem hiba, hanem a szerződés működése: előbb fcvt s1, d1, utána az összeadás.

3. HI/LS használata >/<= helyett. Tünet: NaN bemenetre az elágazás a „nagyobb” ágra fut. Ellenpróba: futtasd a kódot a = NaN, b = 1.0f bemenettel, és cseréld a feltételkódot GT-re. Ugyanez a hiba LS használatakor <= helyett: LS valójában az ordered <=, ami C-ben helyes, de ha valaki LE-t használ, NaN-ra is igazat kap.

4. Negatív érték csonkítása előjel nélküli célba. FCVTZU -1.5f-re telít a 0-ra (és IOC-t állít), míg FCVTZS -1-et ad. Tünet: a negatív mérési értékek 0-ként jelennek meg. Ellenpróba: futtasd mindkét változatot -1.5f-en, és hasonlítsd össze.

5. Pontosságra épülő egyenlőségvizsgálat. 0.1f tízszeri összeadása nem lesz pontosan 1.0f, ezért az fcmp s0, s1 NE-t adhat. Ez nem assembly-hiba, hanem IEEE 754 következmény; a megoldás epsilon-összehasonlítás (fsub + fabs + fcmp).

12.2.9 Feladatok

1. feladat (magyarázó, alapszint — cél: megértés-ellenőrzés). Magyarázd el, miért nincs FADD-nak előjeles és előjel nélküli változata, miközben SCVTF-nek és UCVTF-nek van! Add meg FMOV X0, D1 és SCVTF D0, X1 esetén, mit tartalmaz a célregiszter, ha X1 = 3. Melyik forma ad 3.0-t double-ként, és melyik ad egy 3 értékű egész bitmintát lebegőpontosnak értelmezve?

2. feladat (implementációs, középszint — cél: rutinépítés). Írj olyan assembly-sorozatot, amely az S0-ban lévő x float bemenetből kiszámítja y = (x - 2.5f) * 0.5f-et, az eredményt S0-ban hagyja, majd egy W1-be csonkítva konvertálja! Használj FMOV immediate-ot, ha az assembler elfogadja a 2.5-öt és a 0.5-öt; ha nem, töltsd a konstansokat .word direktívával a .rodata szekcióból. Ellenőrizd x = 5.0f-re, hogy az eredmény 1.25f, W1 = 1 legyen, és indokold, mi történik x = 1.0f esetén.

3. feladat (hibakeresési/mérési, középszint — cél: rutinépítés). Adott egy assembly program, amely állítólag 3.0f-et konvertál int-té, de 0-t ad. Írj fel egy háromlépéses hibakeresési tervet: (a) milyen objdump-kimenetet keresel, (b) mit vizsgálsz GDB-ben, (c) milyen egyszeri változtatással ellenőrzöd a hipotézist! Milyen bitmintát vársz 3.0f esetén W regiszterben, ha FMOV Wd, Sn-nel olvasod ki? Ugyanezt add meg 1.0f-re is.

12.2.10 Megoldások

1. megoldás — Segítség. Gondold át, mit jelent az, hogy az IEEE 754 formátum előjel-nagyság alakú, és hogy az FMOV egy bitminta-másolás, nem egy érték-konverzió — melyik utasítás végez ténylegesen konverziót?

1. megoldás — Teljes megoldás. Az IEEE 754 lebegőpontos szám előjel-nagyság (sign-magnitude) alakú: nincs kettes komplemens, ezért az „előjeles/előjel nélküli” megkülönböztetésnek nincs értelme az aritmetikában — a negatív szám ábrázolása önmagában tartalmazza az előjelet. Az egész ↔︎ lebegőpontos átalakításnál viszont a forrás egész értelmezése kétértelmű: a 0xFFFFFFFF jelenthet -1-et vagy 4294967295-öt, ezért itt explicit SCVTF/UCVTF döntés kell. FMOV X0, D1 az X1 = 3 esetén nem 3-at ad X0-ban, hanem a 3.0 double bitmintáját (0x4008000000000000). SCVTF D0, X1 viszont valóban 3.0 double-t állít elő. Tehát a 3.0 értéket az SCVTF adja; az FMOV bitmintát másol.

2. megoldás — Segítség. Építsd fel a kifejezést két lépésben (FSUB, majd FMUL), és gondold át x = 1.0f-re, hogy a (x - 2.5f) * 0.5f eredménye negatív lesz-e, és mit tesz ilyenkor a nullához csonkító konverzió.

2. megoldás — Teljes megoldás. Egy lehetséges kódrészlet. Bemenete S0 = x, kimenete S0 = y és W1 = (int)y; a belépési pont és a hívó nem része a részletnek:

    .section .rodata
    .align 3
lab_c25:    .word 0x40200000      // 2.5f
lab_c05:    .word 0x3F000000      // 0.5f

    .text
    // Kódrészlet: a hívó előzőleg s0-ba helyezte az x bemenetet.
    ldr     s1, lab_c25           // s1 = 2.5f
    ldr     s2, lab_c05           // s2 = 0.5f
    // feltételezés: s0 = x
    fsub    s0, s0, s1            // s0 = x - 2.5f
    fmul    s0, s0, s2            // s0 = (x - 2.5f) * 0.5f
    fcvtzs  w1, s0                // w1 = (int)y, csonkítva

x = 5.0f esetén 5.0 - 2.5 = 2.5, 2.5 * 0.5 = 1.25, csonkítva 1 — a várt eredmény teljesül. x = 1.0f esetén 1.0 - 2.5 = -1.5, -1.5 * 0.5 = -0.75, amit a nullához csonkítás 0-ra visz (nem -1-re): ez pontosan a Z kerekítési mód következménye, és FCVTZS-t kellett használni FCVTZU helyett, hogy negatív értékre is értelmes eredményt kapjunk. Az FMOV S1, #2.5 és FMOV S2, #0.5 valószínűleg szintén assemble-elhető (mindkettő a szokásos 8 bites konstanskészletbe esik), de ez assembler-verziófüggő; a .word + LDR változat mindig működik, és a Load/store címzési módok anyagát is gyakorolja.

3. megoldás — Segítség. Az objdump-ban keress olyan mnemonicot, amely bitmintát másol ott, ahol a forrás konverziót ígér — a kettő bitmintája ugyanarra a kiinduló egészre egészen más float-ot ad.

3. megoldás — Teljes megoldás. (a) Az objdump-kimenetben fmov mnemonicot keresünk ott, ahol scvtf-nek kellene lennie: aarch64-linux-gnu-objdump -d lab-a045-x | grep -E 'fmov|scvtf|fcvtzu'. (b) GDB-ben töréspontot teszünk az SCVTF után, és megnézzük $s0 lebegőpontos nézetét (p $s0, várt: 3), majd egy fmov w9, s0 után p/x $w9 — a várt bitminta 3.0f-re 0x40400000. Ha itt 0x00000000-t látunk, akkor a mozgatás nem történt meg vagy hibás forrásból indult. (c) Egyetlen változtatás: ha az objdump fmov-ot mutat, cseréljük scvtf-re, újraépítünk, és echo $?-lel ellenőrizzük a 3-at. 1.0f bitmintája 0x3F800000 — érdemes ezt is fejben tartani, mert a lebegőpontos konstansok azonosítása disassembly-ben leggyakrabban ezzel a két értékkel kezdődik.

12.2.11 Rövid összefoglaló

A skaláris lebegőpontos utasítások pontosságát a regiszternév (Sn/Dn) kódolja, nem a mnemonic, és a két pontosság nem keverhető. Az FADD/FSUB/FMUL/FDIV helyesen kerekít az IEEE 754 szerint, kivételeiket az FPSR halmozza, nem az NZCV. Az FMOV bitmintát másol, nem konvertál; a határátlépésre az SCVTF/UCVTF és az FCVTZS/FCVTZU szolgál, utóbbi nullához csonkít és telít. Az FCMP négy kimenetele közül a rendezetlen (NaN) eset miatt a >-hez GT, a <=-hez LS kell, nem HI/LE.

Továbbhasznált definíciók: skaláris lebegőpontos utasítás; FMOV (bit szintű másolás); FCVTZS/FCVTZU (csonkító, telítő konverzió).

12.2.12 Források és ellenőrzési hiányok

A linkek és a dokumentumverziók közös állapota az Olvasói útmutató forrásállapot-táblájában van; itt csak az alfejezet saját megállapításai szerepelnek.

Ellenőrzési hiányok, amelyek a fenti ellenőrzés után is fennmaradnak, és amelyeket a hallgatónak a saját laborjában kell lezárnia:

  1. Az FCVTZS/FCVTZU NaN- és túlcsordulás-viselkedésének (NaN → 0, telítés a szélsőértékre, IOC beállítás) pontos ARM ARM megfogalmazása — itt specifikáció szerinti viselkedésként áll, de a konkrét szöveghely a fenti JS-renderelési korlát miatt nem volt automatikusan kinyerhető.
  2. Az FMOV Sd, #imm által ténylegesen elfogadott konstanskészlet az adott as verzióban — az assembler hibája a mérvadó.
  3. Az FADD/FDIV latenciák táblázata illusztratív, nem mért; a QEMU TCG egy funkcionális, utasításszintű emulátor, nem cikluspontos szimulátor, ezért abszolút latenciát csak fizikai AArch64 hoston vagy a gyártó technikai leírásából szabad idézni.
  4. A qemu-aarch64 és a fizikai AArch64 CPU közötti esetleges eltérés az FPSR flagjeinek jelentésében és a NaN-propagációban jelölendő, ha a laborban eltérést tapasztalsz.