Vegyünk egy tipikus mérnöki feladatot: négy float
értéket kell egy for ciklusban s skalárral
szorozni. A C fordító erre a ciklusra – kapcsolókkal – néhány vektoros
utasítást generálhat, de ahhoz, hogy ezt kézzel is meg tudjuk írni,
először azt kell tisztázni, hol vannak a négy érték. A
skaláris világban minden érték egy külön regiszterben (S0,
S1, …) lakik, és az FADD/FMUL
egyetlen elemet dolgoz fel. A NEON (Arm Advanced SIMD) világában viszont
egy regiszter több elemet tárol párhuzamosan, ezért az
első kérdés nem az aritmetika, hanem a regiszterkészlet és az
adatelrendezés: hány elem fér egy regiszterbe, milyen széles
egy elem, és hogyan kerülnek az értékek a memóriából a regiszter
megfelelő „rekeszeibe”. Ha ezt nem rögzítjük, a vektoros utasítások
szemantikája értelmezhetetlen.
Ez az alfejezet a skaláris lebegőpontos utasítások (A lebegőpontos regiszterkészlet és az IEEE 754
ábrázolás, Skaláris lebegőpontos utasítások)
után a NEON keretét adja meg: a
V0–V31 regiszterek nézeteit, a lane-jelölést,
valamint az LD1/ST1 betöltő/tároló
utasításokat. A tényleges vektoros aritmetikát a Vektorizált összeadás/szorzás: egy skaláris ciklus
NEON-átirata tárgyalja; itt csak annyit használunk belőle, amennyi a
nézetek bemutatásához kell.
Az A64 ISA 32 darab 128 bites vektorregisztert
definiál: V0–V31. Minden Vn
regiszter egy 128 bites tár, amelyet az utasítás arrangement-je
(elrendezés-jelölése) bont fel azonos méretű lane-ekre
(sávokra). Egy Vn regisztert egyetlen utasítás egyszerre
128, 64, 32, 16 vagy 8 bites nézetben ér el, de egyszerre csak
egy nézetben.
A jelölés: Vn.<darabszám><típus>, ahol a
típusbetű a lane méretét adja meg:
| Típusbetű | Lane méret | Lehetséges nézetek (128 bit) | 64 bites nézet |
|---|---|---|---|
B |
8 bit (byte) | Vn.16B (16 sáv) |
Vn.8B (8 sáv) |
H |
16 bit (halfword) | Vn.8H (8 sáv) |
Vn.4H (4 sáv) |
S |
32 bit (single) | Vn.4S (4 sáv) |
Vn.2S (2 sáv) |
D |
64 bit (double) | Vn.2D (2 sáv) |
Vn.1D (1 sáv) |
A szabály kötelező: a darabszám × lane szélesség = 128 vagy
64 bit. Ezért V0.4S, V0.8H és
V0.16B ugyanannak a 128 bites tárnak a három különböző
felbontása; bármelyik használható, de az utasításnak és az
elrendezés-jelölésnek összhangban kell lenniük.
A lane-ek indexelhetők is:
Vn.<T>[i], ahol i a 0 és a darabszám-1
közötti sávszám. A 0. lane a legkisebb helyértékű
biteket foglalja el. Ez azért fontos, mert little-endian tárolásnál az
LD1 betöltés során a memória legkisebb címén lévő
elem kerül a 0. lane-be, és így tovább növekvő címekkel növekvő
lane-indexbe. Ha az értelmezés mégis fordítottnak tűnik, az szinte
mindig endianness- vagy lane-számozási félreértés (Bájtsorrend: endianness és miért releváns a
határon).
Definíció. NEON-lane: egy
Vnvektorregiszter egyetlen,B/H/S/Dméretű, a megadott nézet szerinti sávja; az utasítások lane-enként (SIMD-módon) párhuzamosan dolgoznak fel egyVn.<darabszám><T>elrendezésben.
Megjegyzés: a Qn a 128 bites egész (Vn.16B
megfelelője), a Dn a 64 bites alsó rész
(Vn.8B), az Sn/Hn/Bn
pedig a legalacsonyabb 32/16/8 bit skaláris nézete. NEON-utasításoknál a
Vn.<T> alakot írjuk.
A LD1 (load single structure) egy vagy több
Vn regiszterbe tölt összefüggő memóriából,
a ST1 (store single structure) ugyanígy tárol.
Alapszintaxis (A64):
ld1 {v0.4s}, [x1] // 16 bájtot tölt v0-ba, 4 db 32 bites lane
ld1 {v0.16b}, [x1] // ugyanaz, byte-nézetben
ld1 {v0.2d}, [x1] // 16 bájt, 2 db 64 bites lane
ld1 {v0.2s}, [x1] // CSAK 8 bájtot tölt (2 db 32 bites lane)
st1 {v0.4s}, [x0] // 16 bájtot tárol
ld1 {v0.4s}, [x1], #16 // post-index: x1 += 16 a betöltés után
ld1 {v0.4s, v1.4s}, [x1] // 32 bájtot tölt (v0-ba az első 16, v1-be a második 16)A kulcsészrevétel: az LD1 által átvitt bájtszám
a nézetből következik, nem külön paraméter. A
{v0.4s} és a {v0.16b} ugyanazt a 16 bájtot
tölti be, csak az értelmezést adja meg. A {v0.2s} ezzel
szemben csak 8 bájtot mozgat – az assembler nem kifogásolja, de ha 16
bájtot szándékoztunk olvasni, hibás programot kapunk, ami
adatszegmens-túllépésbe futhat.
Az LD1 „single structure” változat; a rokon
LD2/LD3/LD4 ugyanabból a
memóriából összefésült (interleaved) adatot bont szét több
regiszterbe. Ezt a Struktúrák, igazítás és
mezőelérés mintázatai után érdemes használni; jelen alfejezetben
maradunk az egyregiszteres formánál.
Három egyszerű eszköz arra, hogy egy lane-t értékkel lássunk el:
dup Vd.<T>, Rn – egy általános célú regiszter
bitmintáját mindegyik lane-be másolja
(dup v1.4s, w2).dup Vd.<T>, Vn.<T>[i] – egy meglévő lane
értékét terjeszti minden lane-be (broadcast).ins Vd.<T>[i], Rn – egy általános célú regisztert
ír a megadott lane-be.ins Vd.<T>[i], Vn.<T>[j] – egy lane-t másol
egy másik regiszter megadott lane-jébe.mov Vd.16b, Vn.16b – teljes 128 bites
regisztermásolás (nem lane-másolás!).Visszafelé, egy lane-t általános célú regiszterbe a
umov/smov visz
(umov w3, v0.s[2]).
Az ins célregisztert nem inicializálja:
a nem érintett lane-ek értéke definiálatlan, amíg oda nem írunk. Ezért
lane-enkénti összeállítás előtt rendszerint egy
mov v1.16b, v0.16b másolással indulunk.
A64-en az általános betöltők (köztük a NEON
LD1/ST1) alapértelmezés szerint
megengedik a nem igazított címeket, amennyiben a rendszer
SCTLR.A = 0 (ez a szokásos user-mode Linux és a QEMU
user-mode konfigurációja). Ez azt jelenti, hogy az igazítás nem
korrektségi, hanem teljesítménykérdés:
{v0.4s}
betöltés jellemzően egyetlen cache-sor-hozzáféréssel kiszolgálható.Ezért a vektoros adatszerkezeteket érdemes _Alignas(16)
(C) vagy .balign 16 (assembly) direktívával 16 bájtos
határra igazítani. Azt, hogy egy konkrét mikroarchitektúrán mennyi a
többletköltség, az adott mag dokumentációjából vagy mérésből
kell igazolni, nem az ISA garantálja.
A C-interoperabilitás miatt jegyezzük meg: az AAPCS64 szerint a
V0–V7 és a V16–V31
caller-saved; a V8–V15-nek
csak az alsó 64 bitje callee-saved. Ez azt jelenti,
hogy egy híváson át egyetlen V-regiszter teljes 128
bitjében sem bízhatunk. A V0–V7 és
V16–V31 egészét, a
V8–V15 felső 64 bitjét a hívott szabadon
felülírhatja. Ha a hívónak egy teljes 128 bites értékre a hívás után is
szüksége van, azt a hívás előtt 16 bájtra igazított veremhelyre kell
mentenie, vagy a hívás után újra kell számolnia. Ezt gyakran elfelejtik.
Részletek a Az AAPCS64
regiszterszerződés-ben.
Munkakönyvtár: ~/arm-labor/a64/a046/ —
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/a046
cd ~/arm-labor/a64/a046Írjunk egy assembly függvényt, amely betölt négy
float-ot LD1-gyel,
MOV+INS segítségével megcseréli a két szélső
lane-t, majd ST1-gyel visszaírja. Ez tisztán a
regiszter/lane kezelést demonstrálja, aritmetika nélkül.
// lab-swapends.S
// void lab_swapends(float *dst, const float *src);
// dst[0..3] = { src[3], src[1], src[2], src[0] }
.text
.global lab_swapends
.type lab_swapends, %function
lab_swapends:
ld1 {v0.4s}, [x1] // v0 = {s0, s1, s2, s3} (lane 0..3)
mov v1.16b, v0.16b // v1 = teljes másolat
ins v1.s[0], v0.s[3] // v1.s[0] = s3
ins v1.s[3], v0.s[0] // v1.s[3] = s0
st1 {v1.4s}, [x0] // dst[0..3] = {s3, s1, s2, s0}
ret
.size lab_swapends, .-lab_swapends
.section .note.GNU-stack,"",%progbitsA C-oldali driver és a build (cross-toolchain + QEMU user-mode):
/* lab-swapends-main.c */
#include <stdio.h>
extern void lab_swapends(float *dst, const float *src);
int main(void)
{
_Alignas(16) float src[4] = {1.0f, 2.0f, 3.0f, 4.0f};
_Alignas(16) float dst[4] = {0};
lab_swapends(dst, src);
for (int i = 0; i < 4; ++i)
printf("%g\n", (double)dst[i]);
return 0;
}aarch64-linux-gnu-gcc -static -O0 -g -o lab-swapends lab-swapends.S lab-swapends-main.c
# elvárt: GNU_STACK RW
readelf -lW lab-swapends \
| awk '$1=="GNU_STACK"{f=$7} END{print "GNU_STACK", f; exit !(f=="RW")}'
qemu-aarch64 ./lab-swapendsVárt kimenet (hypotetikus, a hallgató laborjában ellenőrizendő):
4
2
3
1
Regiszterállapot ellenőrzése GDB-vel (a lényeg a lane-szemlélet):
qemu-aarch64 -g 1234 ./lab-swapends # az egyik terminálban
# A másik terminálban:
gdb-multiarch ./lab-swapends
(gdb) target remote localhost:1234
(gdb) break lab_swapends
(gdb) continue
(gdb) info registers v0 v1
(gdb) si # ld1 után: v0.4s = {1,2,3,4}
(gdb) p/x $v0 # a teljes 128 bites bitminta
A 0. lane 0x3F800000 (1.0f), a 3. lane
0x40800000 (4.0f) bitmintája igazolja, hogy a memória kis
címe a 0. lane-be került (little-endian). A GDB
vektorregiszter-nézetének mezőnevei verziónként eltérhetnek.
Nézet-darabszám tévesztés. V0.4S és
V0.8H ugyanaz a 128 bit, de FADD v0.4S, …
helyett FADD v0.8H, … már értelmezhetetlen vagy hibás
eredményt ad. Tünet: assembler-hiba vagy furcsa numerikus
érték. Ellenpróba: objdump -d és a mnemonic
visszaellenőrzése.
LD1 méret tévesztés.
ld1 {v0.2s}, [x1] csak 8 bájtot olvas; a felső két lane
nullázódik, így négy float helyett csak kettőt töltöttünk
be. Adatgyűjtés: gdb-ben x/4fw $x1
összevetése $v0.s-sel.
Post-index elhagyása. [x1, #16]
csak eltolás, nem írja vissza x1-et. Ciklusban a mutató nem
halad. Ellenpróba: ciklusváltozó és $x1 nyomon
követése.
Lane-indexelés iránya. little-endian környezetben a 0. lane a legalacsonyabb című elem; big-endian csak jelölt, generáláskor jelölt profil (Bájtsorrend: endianness és miért releváns a határon).
MOV vs INS.
mov v1.16b, v0.16b teljes regisztert másol, nem egy lane-t;
aki lane-másolást vár, definiálatlan eredményt kap, mert a „másik”
regiszter többi lane-je véletlenszerű.
Teljes vektor egy híváson át. A
V8–V15 felső 64 bitje a híváson át elveszhet,
ha csak a callee-saved szerződés alsó felére támaszkodunk. A
V0–V7 (és a V16–V31)
ebben nem segít, mert ezek teljes 128 bitje caller-saved: a hívások
között szabadon használható ideiglenes regiszterek, de
a hívás után egyikük tartalmában sem bízhatunk.
Javítás: a híváson át szükséges teljes vektort a hívás előtt 16
bájtra igazított veremhelyre mentjük, vagy a hívás után
újraszámoljuk:
str q16, [sp, #-16]! // a teljes 128 bit a verembe; az SP 16-ra igazított marad
bl lab_helper
ldr q16, [sp], #16 // a hívás után pontosan ugyanaz a 128 bitEllenpróba: egy tesztsegéd szándékosan felülír mindent, amit
az AAPCS64 megenged neki: a V0–V7 és a
V16–V31 teljes tartalmát, valamint a
V8–V15 felső 64 bitjét. A helyettesítő
eszközlánccal (Clang + musl, Unicorn) mérve a V0-ban és a
V8-ban tartott 128 bites érték a segéd hívása után
elveszett, a fenti veremmentéssel megőrzött érték bájtra pontosan
megmaradt.
1. (magyarázó, alapszint — cél: megértés-ellenőrzés)
Adott egy V0 nézet V0.4S, ahol
V0 = 0x40490FDB_3F800000_00000000_C0490FDB (hex). Add meg
V0.S[0], V0.S[1], V0.S[3] IEEE
754 szerinti közelítő értékét, és indokold, hogy a bájt-nézetben
V0.B[0] és V0.B[4] miért különböző lane-ekhez
tartozik.
2. (implementációs, alapszint — cél: rutinépítés)
Írj egy lab_bcast4(float *dst, float s) assembly függvényt,
amely az s skalárt DUP-pal mind a négy lane-be
terjeszti (fmov w1, s0 → dup v0.4s, w1), majd
ST1-gyel dst[0..3]-ba írja. Készíts hozzá C
drivert és futtasd qemu-aarch64-gyel.
3. (hibakeresési/mérési, középszint — cél:
rutinépítés) Az alábbi szándékosan hibás kódrészlet csak két
forráselemet tölt be, és a várt megfordítás helyett a
dst[3] nulla marad. Add meg a hiba okát, és javítsd úgy,
hogy dst = {src[3], src[2], src[1], src[0]} legyen.
Használj gdb-t a regisztertartalom ellenőrzéséhez.
lab_rev4:
ld1 {v0.2s}, [x1] // FELTÉTELEZETTEN hibás
mov v1.16b, v0.16b
ins v1.s[0], v0.s[1]
ins v1.s[1], v0.s[0]
st1 {v1.4s}, [x0]
ret1. megoldás — Segítség. Bontsd a 128 bitet négy darab 32 bites lane-re a hexadecimális mintából, és minden lane bitmintáját olvasd IEEE 754 egyszeres pontosságú kódolásként — a bájt-indexelés little-endian, ez dönti el, melyik bájt melyik lane-hez tartozik.
1. megoldás — Teljes megoldás. Bontsuk 32 bites
lane-ekre: S0 = 0xC0490FDB, S1 = 0x00000000,
S2 = 0x3F800000, S3 = 0x40490FDB. IEEE 754
szerint: S0 ≈ −3.1415927 (0xC0490FDB a
−π bitmintája), S1 = +0.0, S3 ≈
+3.1415927. A V0.B[0] a S0 legalacsonyabb
bájtját jelenti (little-endian: a legalacsonyabb helyértékű bájt),
V0.B[4] pedig a S1 legalacsonyabb bájtját;
ugyanabban a 128 bites tárban más lane-hez tartoznak, csak a bontás
más.
2. megoldás — Segítség. A dup a skalár
bitmintáját egy általános célú regiszteren keresztül terjeszti szét — a
lépéssorrend: skalár → GP-regiszter → DUP → ST1.
2. megoldás — Teljes megoldás.
// void lab_bcast4(float *dst, float s)
.text
.global lab_bcast4
.type lab_bcast4, %function
lab_bcast4:
fmov w1, s0 // s bitmintája GP regiszterbe
dup v0.4s, w1 // minden lane = s
st1 {v0.4s}, [x0] // 16 bájt kiírása
ret
.size lab_bcast4, .-lab_bcast4
.section .note.GNU-stack,"",%progbitsDriver és build hasonló az előző példához; a várt kimenet
s négyszer. A dup v0.4s, w1 a bitmintát
négyszer másolja, ami a lebegőpontos s-re pontosan
s-t ad minden lane-ben, mert a forrás bitminta már helyes
IEEE 754 kódolás.
3. megoldás — Segítség. Nézd meg pontosan, hány
bájtot tölt be az ld1 {v0.2s} forma a kétregiszteres
v0.4s nézethez képest — a hiányzó lane-ek innen
erednek.
3. megoldás — Teljes megoldás. Az
ld1 {v0.2s} csak 8 bájtot tölt be; az
utasítás a felső 64 bitet nullázza, nem a másik két forráselemet tölti
oda. Emellett a két INS csak az első két elemet cseréli
fel. A teljes megfordításhoz mind a négy forráselem kell. A
REV64 a két 64 bites félen belül cserél, az
EXT #8 pedig felcseréli a feleket:
lab_rev4:
ld1 {v0.4s}, [x1]
rev64 v0.4s, v0.4s
ext v0.16b, v0.16b, v0.16b, #8
st1 {v0.4s}, [x0]
retEllenőrzés: {1,2,3,4} bemenetre {4,3,2,1}
az eredmény. A feladat szándékosan hibás kódja fent megmarad.
A NEON 32 db 128 bites Vn regisztere ugyanazt a tárat
több nézetben mutatja: Vn.16B, Vn.8H,
Vn.4S, Vn.2D (és feleakkora 64 bites
változataik). A darabszám × lane szélesség = 128 vagy 64 bit, a 0. lane
a legalacsonyabb helyértékű – little-endian memóriában ez a
legalacsonyabb cím. Az LD1/ST1 a nézetből
vezeti le az átvitt bájtszámot; a DUP, INS és
MOV a lane-ek közötti mozgatás eszközei. Az igazítás nem
korrektségi, de teljesítményt érintő tényező. AAPCS64 szerint
V0–V7 és V16–V31
caller-saved, V8–V15 csak alsó 64 bitjében
callee-saved.
Továbbhasznált definíciók: NEON-lane;
arrangement (elrendezés-jelölés:
Vn.<darabszám><T>); LD1/ST1
(egyregiszteres vektoros betöltés/tárolás).
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.
LD1/ST1 és
a lane-jelölés (Vn.<T>[i]) pontos specifikációja.
Ellenőrizve: a link él, de a fejezetszám és a kiadásbetű az
Arm-oldalak közös korlátja miatt nem ellenőrzött.V0–V31 megőrzési szabályai.
Ellenőrizve: a jelenlegi kiadás 2025Q4 (2026-01-23), Release
minőségű (csak az SME-részek Beta).qemu-aarch64 user-mode és a -static linkelés
viselkedése. Ellenőrizve: a master ághoz tartozó
dokumentáció, nem egy konkrét kiadott verzióhoz kötött; a ténylegesen
telepített QEMU-verzió dokumentációja ettől eltérhet.Ellenőrzési hiányok, amelyek a fenti ellenőrzés után is
fennmaradnak: a GNU eszközlánccal és
qemu-aarch64-gyel a kódot itt nem futtattuk. A helyettesítő
eszközlánccal (Clang + musl, statikus build, Unicorn-alapú emulátor) a
lab-swapends a 4 2 3 1 kimenetet adta, a kész
ELF GNU_STACK jelzője RW; a teljes vektor
veremmentésének ellenpróbáját (lásd Tipikus hibák) ugyanígy futtattuk. A
GDB-menet levezetett, várt állapot. Az Ubuntu 24.04-re telepített
GCC/binutils/QEMU pontos verzióját a laborban kell rögzíteni.
Nyitott ellenőrzési igény: az
SCTLR.A = 0 alapértelmezés és az igazítatlan
LD1 viselkedése a konkrét QEMU-verzióban és a fizikai
AArch64 hoston; a cache-sor határán átnyúló hozzáférés ciklusköltsége az
adott mikroarchitektúrán. A példák várt kimenete hipotetikus/levezetett,
a tényleges ellenőrzés a hallgató laborjában történik. Az
LD2/LD3/LD4 és a TBL
részletes tárgyalása a Vektorizált
összeadás/szorzás: egy skaláris ciklus NEON-átirata és a NEON-gyorsulás mérése és korlátai témája.