Képfeldolgozó vagy jelfeldolgozó kódban gyakran ilyen ciklus áll: két
int32_t tömböt elemenként összeadunk egy harmadikba. A
skaláris változat minden elemre külön
ldr/ldr/add/str
sorozatot hajt végre. Egy modern AArch64 mag egy órajel alatt akár 128
bitet is be tud tölteni, ha a kód erre alkalmas alakban kéri – a
skaláris ciklus viszont elemekre bontja azt a szélességet, és a
betöltési sáv szélességét nem használja ki.
A feladat: ugyanazt a szemantikát („c[i] = a[i] + b[i]
minden i-re”) úgy megfogalmazni, hogy egyetlen utasítás
egyszerre négy int32_t-n dolgozzon, miközben a tömb hossza
tetszőleges – nem kell 4 többszörösének lennie, és 0 is lehet. Ez a
NEON-átirat (NEON rewrite) magja, és innentől a skaláris ciklus csak
referencia-implementációként szolgál a korrektség ellenőrzéséhez.
Vektorizálás (vectorization): egy skaláris művelet
újrafogalmazása úgy, hogy egy utasítás egyszerre
darab
bites adatelemen hajtsa végre ugyanazt a műveletet, ahol
a lane-ek száma. int32_t esetén
,
tehát
.
A Vn.4S elrendezés pontosan ezt a négy 32 bites sávot
jelenti (lásd A NEON regiszterkészlet és
adatelrendezés).
A vektorizálhatóság feltétele az adatforma-függetlenség
(element-wise independence): az eredmény
-edik
eleme kizárólag a bemenetek
-edik
elemétől függ. Az add ilyen úgynevezett map minta;
a prefix-összeg vagy a futó maximum viszont nem, mert ott a szomszédos
elemekre is szükség van. Ez a megkülönböztetés később, a
szorzás-összeadás (multiply-accumulate) mintánál is fontos marad.
A fő ciklus / maradékciklus szerkezet. Jelölje az elemek számát. Ha nem osztható -sel, a ciklust ketté kell bontani:
A fő ciklus elemet dolgoz fel, elemes lépésekkel, vektoros utasításokkal; a maradékciklus (remainder loop) a maradék elemet skalárisan. Pszeudokód:
i = 0
while i < n_fő: # vektoros ág
c[i..i+3] = a[i..i+3] + b[i..i+3]
i += 4
while i < n: # maradék ág
c[i] = a[i] + b[i]
i += 1Fontos megfigyelés:
,
azaz int32_t-re
,
ami kettes komplemensben
– bitmanipulációval (n >> 2) << 2 alakban is
megkapható (lásd Shift és rotate: LSL/LSR/ASR/ROR
gyakorlati mintázatok). Az alábbi táblázat néhány
-re
mutatja a felosztást; az értékek számítottak, nem mérték:
| (elem) | |||
|---|---|---|---|
| 0 | 0 | 0 | 0 |
| 7 | 1 | 4 | 3 |
| 8 | 2 | 8 | 0 |
| 17 | 4 | 16 | 1 |
Két tervezési döntés van, amelyet érdemes tudatosan meghozni. Az
egyik az indexelés egysége: számolhatunk elemben (és a
címzéshez lsl #2 skálázást használunk), vagy bájtban (ekkor
a lépés #16 a fő és #4 a maradék ciklusban).
Elemszám-alapú indexelésnél a fő ciklus határa közvetlenül
,
bájtalapúnál
.
A másik, hogy a határt előre kiszámítjuk-e, vagy minden iterációban
újraszámoljuk – az előbbi rövidebb és gyorsabb ciklusmagot ad.
Munkakönyvtár: ~/arm-labor/a64/a047/ —
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/a047
cd ~/arm-labor/a64/a047Az alábbi kód A64 ISA (AArch64, ARMv8-A alapprofil), az AAPCS64
szerint x0 = a, x1 = b, x2 = c,
x3 = n. A rutinok void visszatérésűek, csak a
c tömböt írják.
Nevek és interfész. A függvénynevekben az
i32 az elemtípust jelöli. A következő alfejezet
float tömbökkel és más paramétersorrenddel
(dst, a, b, n) dolgozik, és ott a függvények
add_f32_* nevet kapnak. Azonos név esetén egy régebbi, más
interfészű objektum észrevétlenül belinkelődhetne. A deklarációk közös
fejlécbe kerülnek, lab-a047-add-i32.h néven:
/* lab-a047-add-i32.h */
#ifndef LAB_A047_ADD_I32_H
#define LAB_A047_ADD_I32_H
#include <stddef.h>
#include <stdint.h>
/* c[i] = a[i] + b[i], 0 <= i < n.
* Előfeltétel: a, b, c legalább n elemű, érvényes tömbök (n = 0 esetén
* nem dereferáljuk őket); c nem fedi át részlegesen a-t vagy b-t; minden
* i-re INT32_MIN <= a[i] + b[i] <= INT32_MAX (a C-referencia csak így definiált). */
void add_i32_scalar(const int32_t *a, const int32_t *b, int32_t *c, size_t n);
void add_i32_neon (const int32_t *a, const int32_t *b, int32_t *c, size_t n);
#endifA túlcsordulási tartomány. Az assembly
ADD W5, W5, W6 és a vektoros ADD V0.4S, …
minden bemeneten modulo
számol, a két változat tehát a teljes tartományon bitre azonos eredményt
ad. A C-referencia a[i] + b[i] kifejezése viszont
int32_t operandusokkal (amelyek int-té
léptetődnek elő) előjeles túlcsordulásnál definiálatlan viselkedés. A
„C-vel egyenértékű” állítás ezért csak a fejlécben rögzített tartományon
igaz; a teszt bemenetei ezen belül maradnak.
Az elvárt viselkedést a fejléc C-szerződése rögzíti:
c[i] = a[i] + b[i]. A teszt ezt a C-kifejezést elemenként
maga számolja ki, és orákulumként használja. Mindkét
assembly rutint külön-külön ehhez hasonlítja, nem egymáshoz. Először a
skaláris változat assemblyben:
// lab-a047-scalar.s
.text
.global add_i32_scalar
.type add_i32_scalar, %function
add_i32_scalar:
cbz x3, .Lsc_done
mov x4, #0
.Lsc_loop:
ldr w5, [x0, x4, lsl #2]
ldr w6, [x1, x4, lsl #2]
add w5, w5, w6
str w5, [x2, x4, lsl #2]
add x4, x4, #1
cmp x4, x3
b.lo .Lsc_loop
.Lsc_done:
ret
.size add_i32_scalar, . - add_i32_scalar
.section .note.GNU-stack,"",%progbitsA NEON-változat ugyanezt a szerkezetet követi, csak a fő ciklusban vektorosan:
// lab-a047-neon.s
.text
.global add_i32_neon
.type add_i32_neon, %function
add_i32_neon:
mov x4, #0 // i = 0 (elemben)
lsr x5, x3, #2 // n / 4 (teljes csoportok)
lsl x5, x5, #2 // n_fő = 4 * (n/4) elemben
cbz x5, .Lnv_rem // n < 4 -> nincs fő ciklus
.Lnv_main:
lsl x9, x4, #2 // bájteltolás = i*4; q-címzéshez külön képezve
ldr q0, [x0, x9] // a[i..i+3]
ldr q1, [x1, x9] // b[i..i+3]
add v0.4s, v0.4s, v1.4s // 4 párhuzamos 32 bites összeadás
str q0, [x2, x9] // c[i..i+3]
add x4, x4, #4 // 4 elemmel lépünk
cmp x4, x5
b.lo .Lnv_main
.Lnv_rem:
cmp x4, x3
b.hs .Lnv_done // nincs maradék
.Lnv_rloop:
ldr w6, [x0, x4, lsl #2]
ldr w7, [x1, x4, lsl #2]
add w6, w6, w7
str w6, [x2, x4, lsl #2]
add x4, x4, #1
cmp x4, x3
b.lo .Lnv_rloop
.Lnv_done:
ret
.size add_i32_neon, . - add_i32_neon
.section .note.GNU-stack,"",%progbitsA ldr q0, [x0, x4, lsl #2] alak nem
assemblálható: 128 bites (q) regiszteres
átvitelnél a regiszter-offset lsl mezője a kódolásban csak
#0 vagy #4 (log2(16)) lehet, mert a hardver
csak a tényleges, 16 bájtos transzferméretnek megfelelő vagy nulla
skálázást enged meg — #2 (4 bájtos skálázás) ennél az
utasításformánál nem létező kódolás. Mivel x4 itt elemindex
(int32_t, 4 bájt/elem), a bájteltolást (i*4)
külön lsl x9, x4, #2 utasítással kell kiszámítani, és azt
shift nélkül, [Xn, X9] alakban kell a
q-címzésben felhasználni. A skaláris (w, 4
bájtos) hozzáférésnél ugyanez a lsl #2 közvetlenül a
címzésben érvényes, mert ott a transzferméret pontosan a shift által
jelzett 4 bájt.
Regiszterhasználat: x0–x2 a paraméterek,
x3 a hossz, x4 a futó index, x5 a
fő ciklus elemszám-határa, x6/x7 skaláris
temporálisok, x9 a q-címzéshez külön képezett
bájteltolás, q0/q1 a vektoros operandusok.
Mind x4–x9, mind
v0/v1 caller-saved, ezért nem kell menteni
őket (lásd Az AAPCS64 regiszterszerződés).
A korrektségi teszt minden
-re
lefut, így mind a négy lehetséges maradékosztály (0, 1, 2, 3) lefedésre
kerül. A két rutint külön-külön egy független
C-orákulumhoz méri. A két rutin egymáshoz hasonlítása csak az
eltérésüket mutatná meg: egy mindkettőben azonos paramétersorrend-,
művelet- vagy indexhiba így észrevétlen maradna. A kimeneti tömb előtt
és után négy-négy őrszó áll, és a c minden eleme előre
ugyanezzel az őrértékkel töltődik. Így a meg nem írt elem és a tömbön
kívüli írás is lebukik. n = 0-ra a teszt NULL
mutatókat ad át, tehát bármilyen memória-hozzáférés laphibát okozna.
/* lab-a047-main.c — mindkét rutint külön-külön egy független C-orákulumhoz méri */
#include <stdint.h>
#include <stdio.h>
#include "lab-a047-add-i32.h"
#define NMAX 17
#define GUARD 4 /* őrszavak c előtt és után */
#define GVAL ((int32_t)0x5A5A5A5A) /* őrérték, egyben a c előtöltése */
typedef void (*add_fn)(const int32_t *, const int32_t *, int32_t *, size_t);
/* 0, ha f(a, b, c, n) pontosan c[i] = a[i] + b[i]-t írt minden i < n-re, és
mást nem; a c-n kívüli őrszavak változatlanok. n = 0-ra NULL mutatókat kap:
bármilyen hozzáférés laphiba. */
static int check(const char *name, add_fn f, size_t n)
{
int32_t a[NMAX], b[NMAX], buf[GUARD + NMAX + GUARD];
int32_t *c = buf + GUARD;
for (size_t i = 0; i < n; i++) {
a[i] = (int32_t)(i * 3 + 1); /* minden elem különböző */
b[i] = (int32_t)i * 5 - 2; /* |a[i] + b[i]| < 200: nincs túlcsordulás */
}
for (size_t k = 0; k < GUARD + NMAX + GUARD; k++)
buf[k] = GVAL;
if (n == 0)
f(NULL, NULL, NULL, 0);
else
f(a, b, c, n);
for (size_t k = 0; k < GUARD + NMAX + GUARD; k++) {
int in_c = k >= GUARD && k - GUARD < n;
int32_t want = in_c ? a[k - GUARD] + b[k - GUARD] : GVAL; /* C-orákulum */
if (buf[k] != want) {
printf("%s n=%zu: %s, c-index %ld: %d (elvárt %d)\n", name, n,
in_c ? "hibás elem" : "írás a c-n kívül", (long)k - GUARD, buf[k], want);
return 1;
}
}
return 0;
}
int main(void)
{
int fails = 0;
for (size_t n = 0; n <= NMAX; n++) {
int bad = check("add_i32_scalar", add_i32_scalar, n)
| check("add_i32_neon", add_i32_neon, n);
if (!bad)
printf("n=%2zu OK\n", n);
fails += bad;
}
puts(fails ? "TEST: FAIL" : "TEST: PASS");
return fails != 0;
}Build és futtatás (target: qemu-aarch64, user-mode
emuláció; a statikus link azért kell, hogy a guest dinamikus linkere ne
hiányozzon):
aarch64-linux-gnu-as -o lab-a047-scalar.o lab-a047-scalar.s
aarch64-linux-gnu-as -o lab-a047-neon.o lab-a047-neon.s
aarch64-linux-gnu-gcc -O2 -c -o lab-a047-main.o lab-a047-main.c
aarch64-linux-gnu-gcc -static -o lab-a047-test \
lab-a047-main.o lab-a047-scalar.o lab-a047-neon.o
# elvárt: GNU_STACK RW
readelf -lW lab-a047-test \
| awk '$1=="GNU_STACK"{f=$7} END{print "GNU_STACK", f; exit !(f=="RW")}'
qemu-aarch64 ./lab-a047-test; echo "exit=$?"Várt kimenet: 18 sor n= 0 OK … n=17 OK,
majd TEST: PASS és exit=0. Bármely eltérésnél
a program megnevezi a rutint, az n-t és az első hibás
c-indexet, megkülönbözteti a hibás elemet a
c-n kívüli írástól, és 1-es kilépési kóddal áll le.
Ellenpróba: a teszt valóban elkapja a hibát. A helyettesítő eszközlánccal (Clang + musl, Unicorn-alapú emulátor) mérve:
| Beültetett hiba | Megfigyelt eredmény |
|---|---|
| nincs (helyes rutinok) | n= 0 OK … n=17 OK,
TEST: PASS, exit=0 |
add → sub mindkét
rutinban, ugyanúgy |
add_i32_scalar n=1: hibás elem, c-index 0: 3 (elvárt -1),
ugyanígy az add_i32_neon; TEST: FAIL |
a NEON-maradékciklus feltétele b.lo helyett
b.ls (egy elemmel túlír) |
add_i32_neon n=1: írás a c-n kívül, c-index 1;
TEST: FAIL |
Az első sor azt mutatja, miért kell független orákulum: egy korábbi
változat a két rutint egymáshoz hasonlította, és ugyanezzel a kétoldali
add → sub hibával hibátlanul lefutott.
Ellenőrzési lépés:
aarch64-linux-gnu-objdump -d lab-a047-neon.o kimenetében a
q0-ás betöltés, az add v0.4s, ... és a
str q0, ... sorok mellett a ciklusmag hossza vizuálisan is
látható; ha a fordító/assembler négy ldr w-t mutat
helyette, akkor nem a NEON-ág fordult le.
A maradékciklus hiánya. A leggyakoribb tünet:
hosszon minden helyes,
-nél
viszont a 16. elem (index 16) változatlan marad. Hipotézis: a fő ciklus
után nincs while i < n második ciklus. Adatgyűjtés:
gdb-multiarch alatt töréspont a .Lnv_done
előtt, majd p $x4, p $x3; ha
$x4 != $x3, maradék elem maradt. Ellenpróba: tegyük be a
hiányzó maradék ágat, és futtassuk újra a 0–17-es tesztet.
Mértékegység-keveredés a ciklushatárban. Ha a
lsl x5, x5, #2 kimarad, akkor x5 = n/4
csoport, miközben x4 elemben nő
#4-gyel.
-re
x5 = 4 lesz, és a fő ciklus az első iteráció után kilép. A
teljes vezérlési út azonban itt nem ér véget: a maradékciklus az
aktuális x4 = 4-től n-ig fut, és a 4–16.
elemet skalárisan elkészíti. Az eredmény tehát helyes,
a korrektségi teszt átmegy; 4 elem készül vektorosan és 13 skalárisan. A
hiba teljesítménybeli:
-re
a helyettesítő eszközlánccal megszámolt dinamikus utasításszám 46
helyett 105. Ez az egyik legtanulságosabb hiba, mert a korrektségi teszt
nem látja, csak a vezérlési út vizsgálata vagy az utasításszám. Ha egy
dimenzióhiba korrektségi hibát is okoz, az csak akkor jön elő, ha a
maradékág sem a futó indexből indul. Ezt is a teljes vezérlési úton kell
ellenőrizni, nem a fő ciklusban magában.
Skálázási hiba. Ha a lsl x9, x4, #2 sor
kimarad, és a q-betöltés/tárolás közvetlenül
x4-et (az elemindexet) használja bájteltolásként
([x0, x4]), akkor a tényleges cím iterációnként csak 4
bájttal (egy elemmel) mozdul a helyes 16 bájt (négy elem) helyett — a
négy lane nagyrészt ugyanazt, egymást átfedő bemenetet dolgozza fel
többször, és az eredmény az elemek döntő részénél hibás, miközben a
program nem áll le hibával. Ellenpróba: olyan mini-teszttel érdemes
kezdeni, ahol az a és b tömb elemei mind
különbözőek (ahogy a fenti harnessben) — ekkor az átfedő olvasás azonnal
hibás elem-ként látszik.
b.lo vs. b.lt.
x4, x3, x5 mind unsigned
összehasonlításban értelmezendő (indexek, nem előjeles mennyiségek),
ezért a helyes feltételkód lo/hs. Ha
véletlenül lt-t írunk, akkor
nagy (a 63. bitet elérő) értékeinél az elágazás rossz irányba megy;
degenerált tesztadatokkal ez nem derül ki.
Előjeles túlcsordulás a referenciában. A C skaláris
változat int32_t összeadása előjeles túlcsordulásnál
undefined behavior (UB), a NEON add v0.4s viszont
definiáltan modulo
működik. A korrektségi teszt ezért olyan értékeket használjon, ahol
nincs túlcsordulás, vagy uint32_t-et (illetve a NEON-ban
add v0.4s helyett add v0.4s-t azonos
bitmintán) – a szemantikai különbséget dokumentálni kell.
n = 0 eset. A
cbz x5, .Lnv_rem nélkül a cmp x4, x5 eleve
hamis lenne, de a str a cbz hiányában egyszer
még lefutna egy nulla hosszú tömbre. A nulla hossz explicit kezelése nem
stílus kérdése: a do-while szerkezetű assembly-ciklusok
tipikus hibája.
1. feladat (magyarázó, alapszint — cél:
megértés-ellenőrzés). Az add_i32_neon fő ciklusa
do-while szerkezetű: az index növelése a cmp
előtt történik. Fogalmazza meg szavakkal a ciklusinvariánst (mi
igaz x4-re minden iteráció elején), és mutassa meg, miért
b.lo a helyes feltétel, ha x4 és
x5 csoportszám helyett elemszámot jelöl. Mennyiben változna
az invariancia, ha az indexelést bájtban végeznénk?
2. feladat (implementációs, középszint — cél:
rutinépítés). Írjon mul_acc_neon rutint, amely
c[i] += a[i] * b[i]-t számol a NEON MLA
(multiply-accumulate) utasítással a fő ciklusban és skaláris
MADD-dal a maradékban. Tartsa meg a fő/maradék szerkezetet,
és bővítse a korrektségi tesztet úgy, hogy minden
-re
mindkét implementációt összevesse. Figyeljen rá, hogy c
most bemenet is.
3. feladat (hibakeresés és mérés, középszint — cél:
rutinépítés). Adott egy add_neon_buggy változat,
amelyben a fő ciklus határa lsr x5, x3, #2 (a
lsl kimaradt). Mutassa meg objdump és
gdb-multiarch segítségével, hogy
esetén hány elemet dolgoz fel a fő ciklus, és hányat a maradékciklus.
Indokolja a teljes vezérlési út alapján, miért megy át mégis a
korrektségi teszt, és mivel lehet a hibát kimutatni. Készítsen ezen
felül tételes utasításszámlálást: objdump -d alapján
számolja meg, hány utasítás fut le
elemre a skaláris és a helyes NEON-változatban. Számolja a címképzést,
az indexnövelést, az összehasonlítást és az elágazást is, és adja meg
külön a ciklusmagra és a teljes útvonalra (belépés, ciklus, maradékág,
ret) kapott értéket. Az eredményt dinamikus
utasításszám-arányként nevezze meg; a futásidejű gyorsulás külön
mérés tárgya (NEON-gyorsulás mérése és
korlátai), és QEMU alatt csak illusztratív.
1. megoldás — Segítség. Az index x4
minden iteráció ELEJÉN mit tud már biztosan a feldolgozott tartományról
— és mivel az add #4 a cmp ELŐTT fut, mi a
kapcsolat x4 és x5 közt a feltétel
kiértékelésekor?
1. megoldás — Teljes megoldás. Az invariancia: „az
[x0, x0+4*x4) intervallum és a hozzá tartozó
b, c szakasz már feldolgozott, x4
mindig néggyel osztható, és x4 <= x5”. Az index növelése
a feltételvizsgálat előtt történik, ezért a ciklusmag egyszer már
lefutott, amikor a cmp kiértékelődik – ez a
do-while minta. A b.lo azért helyes, mert
x4 és x5 elemszám, azaz nem negatív mennyiség;
b.lt előjeles olvasatot kérne, és
fölötti, irreális hosszakat leszámítva ugyanazt adná, de a szándékot nem
fejezi ki. Bájtalapú indexelésnél az invariancia x4
értékére „4 többszöröse, x4 <= 4*x5” lenne, a fő ciklus
lépése #16, a maradéké #4, és a címzésből
eltűnne az lsl #2 skálázás.
2. megoldás — Segítség. A szorzás-összeadás pontosan
ugyanazt a fő/maradék vázat követi, mint az add_i32_neon —
csak egy harmadik betöltés (c jelenlegi értéke) és egy
MLA/MADD kerül az ADD helyére.
Melyik regiszter szerepel KÉTSZER is (forrásként és célként)?
2. megoldás — Teljes megoldás. A fő ciklus lényege a
három betöltés és az egyetlen MLA:
// x0 = a, x1 = b, x2 = c, x3 = n
mov x4, #0
lsr x5, x3, #2
lsl x5, x5, #2
cbz x5, .Lma_rem
.Lma_main:
lsl x9, x4, #2 // bájteltolás = i*4; q-címzéshez külön képezve
ldr q0, [x0, x9]
ldr q1, [x1, x9]
ldr q2, [x2, x9]
mla v2.4s, v0.4s, v1.4s // v2 = v2 + v0*v1, lane-enként
str q2, [x2, x9]
add x4, x4, #4
cmp x4, x5
b.lo .Lma_main
.Lma_rem:
cmp x4, x3
b.hs .Lma_done
.Lma_rloop:
ldr w6, [x0, x4, lsl #2]
ldr w7, [x1, x4, lsl #2]
ldr w8, [x2, x4, lsl #2]
madd w8, w6, w7, w8 // w8 = w8 + w6*w7
str w8, [x2, x4, lsl #2]
add x4, x4, #1
cmp x4, x3
b.lo .Lma_rloop
.Lma_done:
retIndoklás: az MLA (Advanced SIMD, Vd.4S) és
a skaláris MADD ugyanazt a szemantikát adja lane-enként,
ezért a maradék ág eredménye bitre azonos a vektoros ágéval. Megjegyzés:
ez a 32 bites akkumulátor túlcsordulhat; nagy szorzók esetén szélesítő
(widening) változat kell, ami az NEON-gyorsulás
mérése és korlátai tárgya. A korrektségi tesztben c-t
előre fel kell tölteni (pl. c[i] = 7), hogy a
+= tényleg érvényesüljön.
3. megoldás — Segítség. Ha a
lsl x5, x5, #2 kimarad, x5 CSOPORTSZÁM marad
(n/4), de a ciklus x4-et ELEMSZÁMKÉNT, néggyel
lépteti — hány iteráció után lesz x4 >= x5, ha
x5 csak 4 (nem 16)? És honnan indul utána a
maradékciklus?
3. megoldás — Teljes megoldás. A hibás változatban
-re
x5 = n/4 = 4, miközben x4 elemenként
#4-gyel nő. x4 értékei 0, majd 4; ekkor
4 < 4 hamis, és a fő ciklus kilép. A fő ciklus tehát
pontosan 4 elemet (index 0–3) dolgoz fel. A vezérlés ezután a
.Lnv_rem ágra kerül, amely az aktuális
x4 = 4-től x3 = 17-ig skalárisan futtatja a
maradékot: ez 13 elem (index 4–16). Az eredmény minden elemre
helyes, ezért a korrektségi teszt átmegy, és első
eltérés sincs. A felosztás 4 + 13 a helyes 16 + 1 helyett. Általában
x4 a fő ciklus után sosem lépi túl n-t, így a
hiba minden
-re
csak teljesítménybeli. GDB-vel a .Lnv_rem címkére tett
töréspontnál p $x4, p $x5, p $x3
(4, 4, 17) azonnal megmutatja a dimenzió-keveredést. A hibát egy
utasításszám-mérés is kimutatja:
-re
a helyes változat 46, a hibás 105 utasítást hajt végre.
Tételes utasításszámlálás -ra (a helyettesítő eszközlánccal emulátorban, utasításonként megszámolva is ezt kaptuk):
| Változat | Belépés | Ciklusmag × iteráció | Maradékág | Kilépés | Összesen |
|---|---|---|---|---|---|
| skaláris | cbz, mov = 2 |
(ldr, ldr, add,
str, add, cmp, b.lo)
= 7 × 16 = 112 |
– | ret = 1 |
115 |
| NEON | mov, lsr, lsl,
cbz = 4 |
(lsl, ldr q, ldr q,
add v, str q, add,
cmp, b.lo) = 8 × 4 = 32 |
cmp, b.hs = 2 |
ret = 1 |
39 |
A ciklusmagok dinamikus utasításszám-aránya
,
a teljes útvonalé (belépéssel és kilépéssel)
.
Ez nem gyorsulási plafon. A négy sávból vagy ebből az arányból nem
vezethető le ISA-szintű felső korlát, mert az utasítások ideje nem
azonos: egy ldr q vagy str q más költségű,
mint egy skaláris ldr w, és a futásidőt a
memóriasávszélesség, a késleltetések és a mikroarchitektúra
párhuzamossága is alakítja. A futásidő egy megnevezett végrehajtási
modell vagy mérés tárgya (NEON-gyorsulás mérése és
korlátai).
A vektorizált tömb-összeadás két fázisra bomlik: fő
ciklusra, amely
elemes lépésekkel, ldr q / add v.Vd.T /
str q mintával dolgozik, és
maradékciklusra, amely a
elemet skalárisan fejezi be. A fő ciklus határa
,
elemszám- és bájtalapú indexelés közül tudatosan kell választani; a
kettő keverése a legjellemzőbb hiba. A korrektséget független
C-orákulummal, minden maradékosztályt lefedő hosszúsorozaton és a
tömbhatárok körüli őrszavakkal kell igazolni, mindkét változatra külön.
A szorzás-összeadás mintázat ugyanezt a vázat használja
MLA/MADD utasításokkal.
Továbbhasznált definíciók: fő ciklus (main loop); maradékciklus (remainder loop); lane-párhuzamos feldolgozás (element-wise map minta).
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.
ADD (vector), MLA (vector),
LDR (register) vektoros formáinak pontos kódolása, a
q-regiszteres betöltés igazítási követelményei és a
regiszter-offset skálázási korlátai. Ellenőrizve: a
link él (a developer.arm.com → support.arm.com
átirányítás megerősített); a fejezetszám és a kiadásbetű az Arm-oldalak
közös korlátja miatt nem ellenőrzött.vaddq_s32,
vmlaq_s32 intrinsic-ek szemantikája, ha a hallgató
C-intrinsic felől közelít. Ugyanaz a korlát.qemu-aarch64 futtatási opciók. Tartalmilag
ellenőrizve: a master ág dokumentációja (közös
verziókorlát). A hivatalos dokumentáció nem állít semmit a
cache-hierarchia vagy a memóriasávszélesség modellezéséről; ez a TCG
funkcionális, nem cikluspontos emulátor-tervezéséből következik, nem egy
explicit QEMU-dokumentált korlát.A helyettesítő eszközlánccal (Clang + musl, statikus build,
Unicorn-alapú emulátor) ténylegesen lefuttattuk a teszthajtót a helyes
rutinokkal és a táblázat beültetett hibáival; a 3. megoldás
utasításszámait emulátorban, utasításonként megszámolva is ellenőriztük
(skaláris
:
115, NEON
:
39, a lsl nélküli NEON
:
105 a helyes 46 helyett). A kész ELF GNU_STACK jelzője
RW; -z execstack-kel linkelve a közös
readelf-ellenőrzés RWE-t jelez és nem nulla
állapottal bukik.
Ellenőrzési hiányok, amelyek a fenti ellenőrzés után is
ténylegesen fennmaradnak: (1) A qemu-aarch64
verziója és a -static linkelés szükségessége Ubuntu 24.04
LTS csomagolt toolchain mellett nem ellenőrzött ebben a kérésben; a
pontos verziót és a -L sysroot-opció szükségességét a
hallgató laborjában kell validálni. (2) A fenti utasításszám-arány
levezetett érték, nem mért; QEMU user-mode nem ad hardverhű
ciklusszámot. (3) Az int32_t előjeles túlcsordulás és a
NEON
modulo-
viselkedés eltérésének gyakorlati kezelése (fordítóflag-ek, sanitizerek)
nincs ebben az alfejezetben specifikálva – ez az NEON-gyorsulás mérése és korlátai felé mutat.