15.1 Projektspecifikáció: feladat, interfész és mérési terv

15.1.1 Motiváció: a specifikáció mint mérnöki szerződés

A záróprojekt első lépése nem a kódírás, hanem a feladat rögzítése. Ez nem bürokrácia: a teljesítménykritikus assembly modulnál a specifikáció az a pont, ahol a szándék és a mérhető követelmény szétválik a megvalósítástól. Ha a feladat, az interfész és az elfogadási feltétel nincs rögzítve az első sor assembly megírása előtt, akkor később nem lehet eldönteni, hogy a modul jó-e. A „gyorsnak tűnik” nem elfogadási kritérium; a „medián idő ≤ a referencia medián idejének fele n≥64n \ge 64 esetén, és minden korrektségi teszt zöld” már az.

Ez az alfejezet a biztonsági interop-szerződés fogalmait ülteti át egy konkrét projekt tervébe. Az ott bevezetett három dimenzió – értékhatomány, pointer-érvényesség, tulajdonjog és hibajelzési konvenció – itt kap konkrét értékeket. A NEON-mérési módszertan adja a statisztikai keretet, a fordítói optimalizálási tapasztalat pedig figyelmeztet arra, hogy a baseline nem lehet „szándékosan lassú C”: a referencia ugyanolyan fordítói kapcsolókkal kell fusson, mint amilyeneket egy valódi projekt használna.

A fejezet célja nem az, hogy egy konkrét modult implementáljunk – az a megvalósítás alfejezet feladata –, hanem hogy olyan specifikációs dokumentumot állítsunk össze, amelyből egy másik fejlesztő (vagy a kurzus auditálója) egyértelműen meg tudja ítélni a kész modult. A specifikáció önmagában is tesztelhető: ha a korrektségi terv nem fedi le a határeseteket, az a specifikáció hibája, nem az implementációé.

15.1.2 Definíció és scope: mi tartozik a projekt-specifikációba

Projekt-specifikáció (project specification): olyan rögzített dokumentum, amely egy szoftvermodul feladatát, külső interfészét, korrektségi követelményeit, teljesítmény-követelményeit és biztonsági szerződését írja le annyira pontosan, hogy a modul implementációjától függetlenül eldönthető legyen annak elfogadása vagy elutasítása.

A specifikációnak öt része van ebben a projektben:

  1. Feladat és indoklás. Mit számol a modul, és miért assembly-optimalizálásra érdemes.
  2. C-interfész. A függvényszignatúra, a visszatérési érték, a hibajelzési konvenció, az argumentumok értékhatárai.
  3. Korrektségi terv. Referenciaimplementáció, összehasonlítási metrika, határesetek listája.
  4. Teljesítmény-mérési terv. Bemenetméretek, ismétlésszám, statisztika, elfogadási küszöb.
  5. Biztonsági önellenőrzési terv. Argumentum-ellenőrzés, túlcsordulás, memóriabiztonság, mentési szimmetria.

A scope-ból kizárt: a C alapszintaxis újratanítása, a konkrét assembly utasítássor megírása (az a058), a mérés tényleges végrehajtása (az a059), valamint az auditmódszer részletes kidolgozása (az a060). Ez az alfejezet a terv, nem a végrehajtás.

15.1.3 Feladatválasztás és indoklás

Három jelölt merül fel a könyv korábbi fejezetei alapján. Mindegyik szerepelt már valamilyen formában, és mindegyik rendelkezik az assembly-optimalizáláshoz szükséges tulajdonságokkal – de nem egyformán.

1. jelölt: CRC32 bitek fölött. A bitmanipulációs fejezet mintái közvetlenül alkalmazhatók: táblás megoldás, táblamentes (bitenkénti) megoldás, illetve az ARMv8.1 CRC extension utasításai. Erőssége, hogy az interfész triviális (uint32_t crc32(const uint8_t *data, size_t len)), és a korrekció bitre pontosan ellenőrizhető. Gyengesége, hogy a bázis A64 ISA-ban nincs dedikált CRC-utasítás (az az ARMv8.1 opcionális kiterjesztése), ezért a kézi optimalizálás gyorsulása behatárolt; a táblás változat memóriafüggő, a táblamentes változat pedig jól vektorizálható ugyan, de a gyorsulás mértéke erősen függ a bemenet hosszától.

2. jelölt: memcpy-szerű másolás. Az interfész szintén egyszerű (void lab_memcpy(void *dst, const void *src, size_t n)), és a NEON vektorizáció természetes. Problémája, hogy nagy nn esetén a memóriasávszélesség a szűk keresztmetszet, nem a számítási kapacitás; az Amdahl-korlát itt nem a nem vektorizálható hányadból, hanem a memóriából ered. Kis és közepes méreteknél viszont látványos a gyorsulás, és a cache-viselkedés (cache-hierarchia) jól demonstrálható.

3. jelölt: sávos (blokkolt) mátrixszorzás. A feladat: C=A⋅BC = A \cdot B, ahol AA, BB, CC mind n×nn \times n-es, sor-fő (row-major) elrendezésű float tömbök. A számítási igény O(n3)O(n^3), a memóriaigény O(n2)O(n^2) – vagyis nagy nn-nél a számítás dominál, és a memóriasávszélesség nem feltétlenül szűk keresztmetszet. A NEON vektorizáció mindhárom ciklusban alkalmazható, a blokkolás (cache-tudatos kód) pedig a cache-találati arányt javítja. A teljesítmény iparági standard metrikával, GFLOP/s-ban mérhető. A referenciaimplementáció (naiv három egymásba ágyazott ciklus) triviálisan megírható, és a lebegőpontos összehasonlítás módszertana is tanítható rajta.

A választás a 3. jelölt: sávos mátrixszorzás. Indoklás táblázatban:

Szempont CRC32 memcpy sávos mátrixszorzás
Számítási dominancia közepes alacsony (memóriakötött) magas (O(n3)O(n^3))
NEON-vektorizálhatóság részleges magas magas (mindhárom ciklus)
Cache-tudatosság szerepe alacsony magas magas (blokkolás)
Referenciaimplementáció egyszerűsége magas magas magas
Iparági mérőszám átviteli sebesség sávszélesség GFLOP/s
Gyorsulás a modell szerint korlátozott memóriakötött Amdahl-modell s=4s = 4 feltevéssel: legfeljebb 4× (modellérték, nem hardvergarancia)
Korrektség ellenőrzése bitre pontos bitre pontos toleranciás

A mátrixszorzás választását az is indokolja, hogy a vektorizálható hányad fogalma itt kézzelfogható: a belső szorzat-összegzés szinte teljesen vektorizálható, a maradék (setup, ciklusvezérlés, blokkhatárok) pedig mérhetően elválik a vektorizált magtól.

15.1.4 A C-interfész rögzítése

Munkakönyvtár: ~/arm-labor/a64/zaro1/ — az alfejezet minden laborfájlja ide kerül, és minden parancs innen futtatandó (a könyvtárrendet a Labor-alapkörnyezet alfejezet írja le). Ez az I. záróprojekt közös könyvtára: mind a négy alfejezete (a057–a060) itt dolgozik, és egymás fájljaira épít.

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

Az interfész a C hívó és az assembly implementáció közötti interop-szerződés. A szignatúra:

/* lab_sgemm.h */
#ifndef LAB_SGEMM_H
#define LAB_SGEMM_H

#include <stddef.h>

#define LAB_SGEMM_OK      0
#define LAB_SGEMM_EINVAL -1

/*
 * C = A * B, mindharom matrix n x n-es, row-major, float32.
 * A, B, C teruletei nem fedhetik at egymast (restrict-szerzodes).
 *
 * Visszateres:
 *   LAB_SGEMM_OK    (0)  sikeres vegrehajtas
 *   LAB_SGEMM_EINVAL(-1) ervenytelen argumentum (NULL pointer vagy
 *                        n * n * sizeof(float) tulcsordulas)
 *
 * n == 0 eseten a fuggveny nem ir semmit, es LAB_SGEMM_OK-t ad.
 */
int lab_sgemm(size_t n, const float *restrict A,
              const float *restrict B, float *restrict C);

#endif

Az int visszatérési érték tudatos döntés. A biztonsági interop-szerződés szerint a hívó és a hívott közötti hibajelzés konvencióját rögzíteni kell. A void visszatérés ezt a csatornát megszüntetné, és a hibát a hívóra terhelné, aki viszont nem feltétlenül tudja ellenőrizni a pointerek érvényességét. Az int visszatérés egyetlen regiszterben (W0) elfér, az AAPCS64 szerint ez a standard visszatérési regiszter, és az assembly implementációban egyetlen feltételes ág elég hozzá.

Az argumentumok az AAPCS64 szerint az X0–X3 regiszterekben érkeznek: n az X0-ban, A az X1-ben, B az X2-ben, C az X3-ban. A restrict minősítő nem változtat az ABI-n, de szerződéses ígéret: az A, B, C által mutatott területek nem fednek át. Ez az assembly implementációt mentesíti az alias-analízis alól, és a hívót terheli a felelősség a megsértéséért.

Az értékhatomány szerződése:

Argumentum Érvényes tartomány Viselkedés érvénytelen esetén
n 0≤n≤⌊SIZE_MAX/sizeof(float)⌋0 \le n \le \lfloor \sqrt{SIZE\_MAX / sizeof(float)} \rfloor LAB_SGEMM_EINVAL
A, B, C nem NULL (ellenőrzött); n2n^2 elemre érvényes (nem ellenőrizhető, a hívó felelőssége) NULL esetén LAB_SGEMM_EINVAL, egyébként meghatározatlan
A, B, C átfedés nincs meghatározatlan (szerződésszegés)

A NULL pointer és a túlcsordulás ellenőrzése O(1)O(1) költségű, szemben a mátrixszorzás O(n3)O(n^3) költségével. Ezért a biztonsági ellenőrzések beépítése nem rontja érdemben a teljesítményt – ezt az alábbi biztonsági önellenőrzési terv számszerűsíti.

15.1.5 Korrektségi teszt-terv

A korrektségi terv célja: az assembly implementáció eredménye minden releváns bemeneten egy előre rögzített tolerancián belül egyezzen a referenciaeredménnyel, és a visszatérési érték minden esetben a szerződésnek megfelelő legyen.

Referenciaimplementáció. A referencia nem a naiv float-os három ciklus, mert az maga is kerekítési hibát halmoz fel, és nem tekinthető „igazságnak”. A referencia double pontossággal számol, float bemenetből, és double kimenetet ad:

/* lab_sgemm_ref.c */
#include "lab_sgemm.h"

void lab_sgemm_ref(size_t n, const float *A, const float *B, double *C)
{
    for (size_t i = 0; i < n; ++i) {
        for (size_t j = 0; j < n; ++j) {
            double acc = 0.0;
            for (size_t k = 0; k < n; ++k) {
                acc += (double)A[i * n + k] * (double)B[k * n + j];
            }
            C[i * n + j] = acc;
        }
    }
}

Numerikus szerződés. A projekt egyetlen numerikus szerződést használ; a tesztkészlet, a mérés előtti ellenőrzés és az audit is erre hivatkozik.

  1. Lebegőpontos környezet. FPCR: RMode = 00 (legközelebbi, páros felé kerekítés), FZ = 0 (nincs flush-to-zero), DN = 0 (nincs alapértelmezett NaN), és az AH/FIZ/NEP bitek is 0. Ez a Linux alapállapota; a teszthajtó induláskor kiolvassa az FPCR-t, és eltérés esetén nem fut le.
  2. Bemeneti tartomány. A szerződés véges, túlcsordulásmentes bemenetekre szól: minden A- és B-elem véges, és minden kimeneti elemre sij=∑k|aik||bkj|≤FLT_MAX/2s_{ij} = \sum_k |a_{ik}|\,|b_{kj}| \le \text{FLT\_MAX}/2. A felezés a kerekítési tartalék, indoklását lásd lent. Ezen kívül (NaN, Inf vagy túlcsorduló bemenet) a modul viselkedése nem része az elfogadásnak; a túlcsordulást a tesztkészlet külön, dokumentált elvárással vizsgálja.
  3. A modul számítási modellje. Minden cijc_{ij} a 0-ról indul, és k=0,1,…,n−1k = 0, 1, \dots, n-1 sorrendben float pontosságú fúzionált szorzás-összeadással frissül (c←fma(aik,bkj,c)c \leftarrow \text{fma}(a_{ik}, b_{kj}, c), minden lépés után egy kerekítés). A vektoros FMLA sávonként és a skalár farok FMADD-je ugyanezt a műveletet végzi, ezért a sáv és a farok között nincs numerikus különbség.
  4. Referencia. A lab_sgemm_ref ugyanebben a kk-sorrendben, double pontossággal összegez. Két float szorzata double-ban pontosan ábrázolható (24 + 24 bit ≤ 53 bit), ezért a referenciában a fordítói kontrakció (-ffp-contract) sem változtat az eredményen.
  5. Elfogadás. Minden elemre: cijc_{ij} véges, és

|cijasm−cijref|≤atol+rtol⋅sij,rtol=10−4,atol=10−30.|c_{ij}^{\text{asm}} - c_{ij}^{\text{ref}}| \le \text{atol} + \text{rtol} \cdot s_{ij}, \qquad \text{rtol} = 10^{-4},\ \text{atol} = 10^{-30}.

A nem véges kimeneti elem véges referencia mellett mindig FAIL, és az összehasonlítás !(eltérés <= korlát) alakú, így a NaN nem csúszhat át rajta.

Miért nem bitre pontos, és miért ez a korlát? A kk-sorrend az i-k-j átírásban nem változik: egy adott cijc_{ij}-hez a modul és a referencia ugyanabban a sorrendben adja hozzá a tagokat. A különbség forrása a pontosság: a modul minden lépés után 24 bites mantisszára kerekít, a referencia 53 bitesre. A fúzionált művelet ezen belül csak annyit számít, hogy lépésenként egy kerekítés van, nem kettő. Az ilyen összegzés ismert hibakorlátja |c−r|≤γn∑k|aikbkj||c - r| \le \gamma_n \sum_k |a_{ik} b_{kj}|, ahol γn≈n⋅2−24\gamma_n \approx n \cdot 2^{-24}; ez n≤256n \le 256 esetén legfeljebb 1,6⋅10−51{,}6 \cdot 10^{-5}, tehát az rtol=10−4\text{rtol} = 10^{-4} biztonsági tartalékkal fedi. A korlátot ezért sijs_{ij}-hez és nem |cijref||c_{ij}^{\text{ref}}|-hez mérjük: kioltásnál (pozitív és negatív tagok összegénél) a referencia lehet közel nulla, miközben a kerekítési hiba a tagok nagyságával arányos. Az atol\text{atol} csak az alulcsordulási tartományt nyeli el; n>256n > 256 esetén az rtol\text{rtol} értékét újra kell számolni.

Miért FLT_MAX/2\text{FLT\_MAX}/2, és miért nem elég az sij<FLT_MAXs_{ij} < \text{FLT\_MAX}? A szerződés véges kimenetet vállal, de a float FMA-lánc részösszegei a kerekítés miatt nagyobbak lehetnek a pontos abszolút összegnél. Ellenpélda n=4n = 4-re: az A első sora {𝟶𝚡𝟷.𝚏𝚏𝚏𝚏𝚏𝚊𝚙𝟷𝟸𝟽,𝟶𝚡𝟷.𝟶𝟾𝚙𝟷𝟶𝟹,𝟶𝚡𝟷.𝟶𝟾𝚙𝟷𝟶𝟹,𝟶𝚡𝟷.𝟶𝟾𝚙𝟷𝟶𝟹}\{\texttt{0x1.fffffap127},\ \texttt{0x1.08p103},\ \texttt{0x1.08p103},\ \texttt{0x1.08p103}\}, a többi sora nulla, B minden eleme 1. A pontos összeg s00=𝟶𝚡𝟷.𝚏𝚏𝚏𝚏𝚏𝚍𝟷𝟾𝚙𝟷𝟸𝟽<FLT_MAX=𝟶𝚡𝟷.𝚏𝚏𝚏𝚏𝚏𝚎𝚙𝟷𝟸𝟽s_{00} = \texttt{0x1.fffffd18p127} < \text{FLT\_MAX} = \texttt{0x1.fffffep127}. A kk-sorrendű float FMA részösszegei mégis 𝟶𝚡𝟷.𝚏𝚏𝚏𝚏𝚏𝚌𝚙𝟷𝟸𝟽\texttt{0x1.fffffcp127}, 𝟶𝚡𝟷.𝚏𝚏𝚏𝚏𝚏𝚎𝚙𝟷𝟸𝟽\texttt{0x1.fffffep127}, majd +∞+\infty: minden 21032^{103} nagyságú tag egy teljes ulp-ot (21042^{104}) ad hozzá, mert a pontos érték a két szomszédos float közötti felezőpont fölé esik. A tartalék levezetése. Legyen u=2−24u = 2^{-24} az egységkerekítés, η=2−149\eta = 2^{-149} a legkisebb pozitív szubnormális float, és xkx_k a kk-adik FMA pontos, kerekítés előtti értéke. Az RNE-kerekítésre a normális tartományban a relatív korlát |fl(z)|≤(1+u)|z||\mathrm{fl}(z)| \le (1+u)|z| érvényes, de FZ = 0 mellett a szerződés a szubnormális eredményeket is megengedi, ahol a hiba abszolút is lehet, legfeljebb η/2\eta/2. Ezért a mindkét tartományban érvényes modell: |fl(z)|≤(1+u)|z|+η/2.|\mathrm{fl}(z)| \le (1+u)\,|z| + \eta/2. Egy tisztán relatív korlát itt nem elég. Ellenpélda: az első szorzat a=𝟶𝚡𝟷.𝟶𝟶𝟶𝟶𝟶𝟸𝚙-𝟷𝟸𝟼a = \texttt{0x1.000002p-126}, b=𝟶𝚡𝟷𝚙-𝟸𝟺b = \texttt{0x1p-24}, a második szorzat 0. Ekkor sij=𝟶𝚡𝟷.𝟶𝟶𝟶𝟶𝟶𝟸𝚙-𝟷𝟻𝟶s_{ij} = \texttt{0x1.000002p-150}, az első FMA a pontos értéket 𝟶𝚡𝟷𝚙-𝟷𝟺𝟿\texttt{0x1p-149}-re kerekíti, így a második lépés pontos értéke x2=𝟶𝚡𝟷𝚙-𝟷𝟺𝟿x_2 = \texttt{0x1p-149}. Ez nagyobb a relatív korlát (1+u)sij=𝟶𝚡𝟷.𝟶𝟶𝟶𝟶𝟶𝟹𝚙-𝟷𝟻𝟶(1+u)\,s_{ij} = \texttt{0x1.000003p-150} értékénél, de belefér az abszolút taggal bővített (1+u)sij+η/2(1+u)\,s_{ij} + \eta/2 korlátba (natív C fmaf-fal ellenőrizve). A modellt lépésenként alkalmazva |xk+1|≤|ĉk|+|ai,kbk,j|≤(1+u)|xk|+η/2+|ai,kbk,j||x_{k+1}| \le |\hat c_k| + |a_{i,k}b_{k,j}| \le (1+u)\,|x_k| + \eta/2 + |a_{i,k}b_{k,j}|, amiből indukcióval |xk|≤(1+u)k−1sij+η2∑t=0k−2(1+u)t≤(1+γn)(sij+nη2).|x_k| \le (1+u)^{k-1} s_{ij} + \frac{\eta}{2}\sum_{t=0}^{k-2}(1+u)^t \le (1+\gamma_n)\left(s_{ij} + \frac{n\,\eta}{2}\right). sij≤FLT_MAX/2s_{ij} \le \text{FLT\_MAX}/2 és γn≤1\gamma_n \le 1 (azaz n≲223n \lesssim 2^{23}) mellett ez FLT_MAX\text{FLT\_MAX} alatt marad: az abszolút tag nη/2≤223⋅2−150n\,\eta/2 \le 2^{23} \cdot 2^{-150} elhanyagolható a FLT_MAX/2\text{FLT\_MAX}/2 nagyságrendű tartalékhoz képest. Egyetlen lépés sem kerekíthet tehát +∞+\infty-re. A tankönyvi n≤256n \le 256 körben γn≤1,6⋅10−5\gamma_n \le 1{,}6 \cdot 10^{-5}, tehát a felezés bőkezű. Egy szorosabb határ FLT_MAX/(1+γn)−nη/2\text{FLT\_MAX}/(1+\gamma_n) - n\,\eta/2 lenne (a második tag gyakorlatilag nulla), de a kerek, könnyen ellenőrizhető FLT_MAX/2\text{FLT\_MAX}/2 a szerződés része; a near-max és az fma-edge teszteset besorolása ettől nem változik. A tagságot a tesztkészlet lab_sgemm_in_contract függvénye egy bizonyítottan elégséges, konzervatív szűrővel vizsgálja: a double-ben számított összeget a kerekítési hibájával felfelé becsli, így 1-et csak biztosan belső esetre ad, a határ közvetlen közelében pedig egy belső esetet is elutasíthat.

Határesetek. A tesztterv minden alábbi bemenetre lefut, és a referenciaeredményhez hasonlít:

Eset n érték Cél
nulla méret 0 LAB_SGEMM_OK, nincs írás
egységelem 1 egyetlen szorzat
blokkméret alatt 2, 3 vektorizációs határ alatt
blokkméret felett 5, 7 maradékkezelés
blokkméret többszörös 8, 16 teljes blokk
blokkhatár átlépés 17, 63, 65 részleges blokk
közepes 64, 128 tipikus mérési méret
nagy 256 stressz, cache-hatás
további farokesetek 4, 6, 12 4-gyel osztható és nem osztható kis méretek

A fenti méretek mindegyike determinisztikus, mintázott tartalommal fut.

Speciális mátrixtartalmak. A tényleges tesztmátrix (a tesztkészlet pontosan ezt hajtja végre):

Tartalom n értékek Cél
csupa nulla, csupa egyes, csupa mínusz egyes, identitás (B=IB = I) 32, 64 strukturált hibák (sávkitöltés, előjel, indexelés)
véletlen, egyenletes [−1,1)[-1, 1), rögzített maggal 32, 64, 128, 256 kioltás, általános eset, nagy méret
nagyérték-teszt (±106\pm 10^6) 32, 64 nagy, de a szerződésen belüli értékek: 64⋅1012≈6,4⋅101364 \cdot 10^{12} \approx 6{,}4 \cdot 10^{13}, messze a float határa (≈3,4⋅1038\approx 3{,}4 \cdot 10^{38}) alatt
határközeli (near-max): minden A-elem 𝟶𝚡𝟷.𝚏𝚏𝚏𝚏𝚏𝚌𝚙𝟷𝟸𝟼/n\texttt{0x1.fffffcp126}/n, B csupa 1 32, 64 szerződésen belül, egynemű előjellel: sij=𝟶𝚡𝟷.𝚏𝚏𝚏𝚏𝚏𝚌𝚙𝟷𝟸𝟼≤FLT_MAX/2s_{ij} = \texttt{0x1.fffffcp126} \le \text{FLT\_MAX}/2; a kimenetnek végesnek és tűrésen belülinek kell lennie
túlcsordulás (102010^{20}, szerződésen kívül) 8 a szerződésnek el kell utasítania; dokumentált elvárás a kk-sorrendű float FMA-modell eredménye: minden szorzat 1040>10^{40} > FLT_MAX, ezért minden kimeneti elem +∞+\infty
kerekítési határeset (half-edge, szerződésen kívül): A első sora {FLT_MAX/2,1}\{\text{FLT\_MAX}/2, 1\}, B első oszlopa {1,1}\{1, 1\} 2 a pontos s00=FLT_MAX/2+1s_{00} = \text{FLT\_MAX}/2 + 1 a határ fölött van, double-ben mégis egyenlő vele; a szűrőnek a kerekítési hibát is figyelembe véve el kell utasítania
FMA-határ-ellenpélda (fma-edge, a fenti 4×4-es eset, szerződésen kívül) 4 a régi sij<FLT_MAXs_{ij} < \text{FLT\_MAX} feltétel átengedné, az új elutasítja; dokumentált elvárás az FMA-modell: a 0. sor +∞+\infty, a többi 0

A nagyérték-teszt tehát nem túlcsordulás-teszt; a szerződés felső határát a near-max eset terheli, a valódi túlcsordulást pedig a két külön, szerződésen kívüli eset vizsgálja, saját elvárással. A tesztkészlet minden esetnél azt is ellenőrzi, hogy a lab_sgemm_in_contract a várt osztályba sorolja-e a bemenetet. NaN- és Inf-bemenetet a szerződés kizár. A nem véges kimenetet viszont az ellenőrző minden esetben FAIL-nek minősíti, és ezt egy önellenőrzés igazolja: a teszthajtó egy helyes eredménybe szándékosan NaN-t, ±∞\pm\infty-t és tűrésen kívüli eltérést ír, és mindegyiknek le kell buknia.

Memóriaszerződés. A kimeneten csak a C n2n^2 eleme változhat. A teszthajtó a bemeneti A és B másolatát a hívás után bájtra összeveti, a kimeneti puffer előtt őrbájtokat tart, utána pedig védőlap (PROT_NONE) áll. Ez a túlindexelt olvasást és írást is laphibává teszi. A C oldali AddressSanitizer erre nem elég, mert a kézzel írt assembly memóriaeléréseit nem instrumentálja.

15.1.6 Teljesítmény-mérési terv

A teljesítmény-mérés célja: eldönteni, hogy az assembly implementáció a rögzített elfogadási küszöböt teljesíti-e, és dokumentálni a mért értékeket. A mérés nem az assembly utasítások számát méri, hanem a falióra-időt egy rögzített munkamennyiségre.

Bemenetméretek. n∈{16,32,64,128,256}n \in \{16, 32, 64, 128, 256\} (a mérőprogram alapértelmezése). Az alsó határ a vektorizációs overhead dominanciáját mutatja, a felső a cache-hatást. Kis nn-nél az assembly setup-költsége látszik, nagy nn-nél a memória és a cache.

Melegítés és ismétlésszám. Minden mérethez W=3W = 3 melegítő futás (a cache és az elágazás-előrejelző felmelegítése), majd R=11R = 11 mért futás. A páratlan ismétlésszám lehetővé teszi a medián egyszerű kiszámítását. A melegítő futások eredménye nem kerül a statisztikába.

Statisztika. Az elsődleges metrika a medián idő. Mellette a szóródás mérésére a medián abszolút eltérés (MAD, median absolute deviation) szolgál:

MAD=median(|ti−median(t)|)\text{MAD} = \text{median}\left(|t_i - \text{median}(t)|\right)

A mediánt azért használjuk, mert a falióra-idő eloszlása gyakran ferde (egy-egy lassabb futás az ütemező vagy a cache miatt), és a medián robusztusabb az átlagnál. Az átlag és a szórás is közölhető, de nem ez az elsődleges metrika.

Származtatott metrika. A mátrixszorzás műveletigénye 2n32n^3 lebegőpontos művelet (szorzás és összeadás páronként), ezért a teljesítmény:

GFLOP/s=2n3t⋅109\text{GFLOP/s} = \frac{2 n^3}{t \cdot 10^9}

ahol tt a medián idő másodpercben. Ez a metrika függetleníti a mérést a bemenetmérettől, és lehetővé teszi a különböző nn-ek összehasonlítását.

Összehasonlítás. A kézzel írt .S fájlt a -O2 nem optimalizálja: az assembler azt fordítja le, amit leírtunk. A C-oldali kapcsolók tehát csak a C-változatokat befolyásolják, és a „ugyanazzal a szinttel fordítva” önmagában nem tesz igazságossá egy összehasonlítást. Az is fontos, hogy a -march=armv8-a már alapból tartalmazza az FP- és SIMD-támogatást, ezért a +simd utótag nem kapcsol be egy addig tiltott autovektorizálást. Az, hogy a fordító vektorizál-e, az optimalizálási kapcsolókon (-ftree-vectorize, -O3) és a ciklus alakján múlik.

Az assembly modul a naiv változathoz képest három dolgot változtat egyszerre: a ciklussorrendet (i-j-k helyett i-k-j, így B oszlopai helyett folytonos B- és C-sorokat jár be), a vektorizálást (4 sávos FMLA) és a fúzionált szorzás-összeadást. A teljes nyereség ezért nem nevezhető „assembly/NEON-gyorsulásnak”. A mérés négy változatot hasonlít össze, hogy a hatások szétváljanak:

Változat Forrás Kapcsolók (GCC) Mit választ le
1. naiv C, i-j-k, skalár lab_sgemm_ijk_scalar -O2 -fno-tree-vectorize kiindulópont
2. átrendezett C, i-k-j, skalár lab_sgemm_ikj_scalar -O2 -fno-tree-vectorize a ciklussorrend hatása (1 → 2)
3. átrendezett C, i-k-j, vektorizált lab_sgemm_ikj_vec -O3 -fopt-info-vec-optimized a fordítói vektorizálás hatása (2 → 3)
4. assembly lab_sgemm – (a .S fájlra nem hat) a kézi kód többlete a vektorizált C-hez képest (3 → 4)

Minden C-változat azonos célra és FP-modellre fordul: -std=gnu17 -march=armv8-a -ffp-contract=fast, -ffast-math nélkül. A kontrakció engedélyezése azt jelenti, hogy a fordító is használhat FMADD/FMLA-t, mint a modul. A -ffast-math hiánya miatt viszont a fordító nem rendezheti át az összeadásokat, ezért az i-j-k redukciós ciklust nem tudja vektorizálni, az i-k-j belső ciklusát igen. A jegyzőkönyvbe kerül a GCC pontos verziója (aarch64-linux-gnu-gcc --version) és a vektorizációs riport. A végső binárison objdump -d-vel ellenőrizzük, hogy az 1–2. változatban nincs vektoros fmla, a 3.-ban van.

Elfogadási küszöb. Az assembly implementáció elfogadott, ha

  1. minden korrektségi teszt zöld;
  2. a jegyzőkönyvben rögzített környezetben (host vagy QEMU-verzió és CPU-modell, fordítóverzió, kapcsolók) n≥64n \ge 64 esetén az assembly medián ideje legfeljebb a naiv i-j-k skalár változat medián idejének a fele;
  3. a jegyzőkönyv tartalmazza mind a négy változat mediánját és MAD-ját, a három részgyorsulást (sorrend, C-vektorizálás, kézi assembly) és az nn, WW, RR, atol, rtol értékeket.

A 2. pont kétszeres küszöbe egy rögzített környezetre vonatkozik. Nem állítjuk, hogy minden Arm-processzoron vagy QEMU-verzión teljesül, sem azt, hogy ott megengedő volna: egy másik mag vagy emulátor más arányt adhat, ezért minden új környezetben újra kell mérni. A küszöb teljesülése önmagában azt sem igazolja, hogy a kézi assembly megérte. Erről a 3 → 4 részgyorsulás dönt. Ha a modul nem gyorsabb a vektorizált C-nél, az is érvényes, dokumentálandó eredmény. Abszolút GFLOP/s-csúcs előírása hardver- és QEMU-függő lenne, és sértené a „ne találj ki benchmarkot” elvet. A NEON-gyorsulás mérése szerint QEMU-emulációban a mért érték emulációs idő, nem a célprocesszor ciklusszáma, és ezt a jegyzőkönyvben jelölni kell.

Illusztratív modell, nem garancia. Ha a skaláris referencia futásidejének pp hányadát ss-szeresére gyorsítjuk, és a többi rész ideje változatlan, akkor S=1/((1−p)+p/s)S = 1/((1-p) + p/s) (Amdahl-modell). A négysávos NEON egyszerre négy float-elemet dolgozhat fel, de ebből önmagában nem következik, hogy s=4s = 4, sem az, hogy 4× általános plafon volna: a memóriaelérés, a fúzionált műveletek, a kibontás és a skalár farok mind eltolhatja a tényleges ss-t. A pp értékét sem a forráskódból olvassuk ki, hanem a skaláris referencia időhányadaként mérjük (profilozással vagy a vektorizálható szakasz külön időmérésével). Az alábbi ábra egy s=4s = 4 feltevésű modell számított görbéje pp függvényében, nem a processzor garantált teljesítménye. Például p=0,95p = 0{,}95 és s=4s = 4 mellett S≈3,48S \approx 3{,}48.

Illusztratív, szamitott Amdahl-modell s=4 feltevessel - Szamitott ertek az S=1/((1-p)+p/s) kepletbol s=4 feltevessel; nem meresi adat es nem hardvergarancia, csak tervezesi modell.

A mérési terv ezzel szemben a tényleges medián időt rögzíti; a modell és a mérés viszonya az a059 validációs lépés tárgya.

15.1.7 Biztonsági önellenőrzési terv

A biztonsági terv a biztonsági interop-szerződés közvetlen alkalmazása. Négy ellenőrzési pont:

  1. Argumentum-ellenőrzés. A függvény belépéskor ellenőrzi, hogy A, B, C nem NULL, és hogy n nem nulla. Ha n == 0, a függvény azonnal visszatér LAB_SGEMM_OK-val, anélkül hogy bármit írna a C pointerre. Ez a kapacitáskötött határellenőrzés mintája: a hosszt a fogadó fél saját kapacitásához kötjük, és a konverzió után, az első írás előtt döntünk.
  2. Túlcsordulás-ellenőrzés. Az n2n^2 elem mérete size_t-ben túlcsordulhat. Az ellenőrzés: ha n>⌊SIZE_MAX/sizeof(float)⌋n > \lfloor \sqrt{SIZE\_MAX / sizeof(float)} \rfloor, akkor LAB_SGEMM_EINVAL. Ez megakadályozza, hogy egy hibás n érték miatt a függvény a memórián kívülre írjon.
  3. Mentési szimmetria. Az assembly implementációban minden STP (store pair) utasításhoz tartozó LDP (load pair) a visszatérési úton, minden kimeneti ágon (sikeres és hibás visszatérésnél is) végrehajtódik. Ez a mentési szimmetria követelménye; megsértése a hívó regisztereinek csendes módosulását okozza.
  4. Memóriaátfedés. A restrict szerződés szerint az A, B, C területek nem fednek át. Ezt a függvény nem ellenőrzi (nem is tudná általánosan eldönteni), de a szerződés a hívó felelőssége. A tesztelés során külön tesztesettel kell ellenőrizni, hogy a hívó ezt betartja-e.

A biztonsági ellenőrzések költsége O(1)O(1), a műveleté O(n3)O(n^3), ezért n≥16n \ge 16 esetén az ellenőrzés futásideje elhanyagolható. Ez indokolja, hogy az ellenőrzéseket nem érdemes feltételes fordítási kapcsoló mögé rejteni – a záróprojektben mindig aktívak.

15.1.8 Kidolgozott példa: a specifikációs dokumentum minta

Az alábbi példa egy teljes specifikációs vázlat, amely a fenti döntéseket egy dokumentumba rendezi. A kódblokkok szemléltető példák, nem végrehajtandó utasítások.

/* lab_sgemm_spec.c — a mérési terv vaza (szemlelteto) */
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <math.h>
#include "lab_sgemm.h"

static double now_seconds(void)
{
    struct timespec ts;
    if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) { perror("clock_gettime"); exit(2); }
    return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9;
}

static double median(double *v, size_t n)
{
    /* n paratlan; egyszeru buborek-rendezes a vazlatban */
    for (size_t i = 0; i < n; ++i)
        for (size_t j = i + 1; j < n; ++j)
            if (v[j] < v[i]) { double t = v[i]; v[i] = v[j]; v[j] = t; }
    return v[n / 2];
}

int main(void)
{
    const size_t sizes[] = { 16, 32, 64, 128, 256 };
    const size_t W = 3, R = 11;

    for (size_t s = 0; s < sizeof(sizes) / sizeof(sizes[0]); ++s) {
        size_t n = sizes[s];
        size_t nelem = n * n;
        float *A = malloc(nelem * sizeof(float));
        float *B = malloc(nelem * sizeof(float));
        float *C = malloc(nelem * sizeof(float));
        double *Rref = malloc(nelem * sizeof(double));
        if (!A || !B || !C || !Rref) { free(A); free(B); free(C); free(Rref); return 1; }

        /* bemenet feltoltese: illusztrativ, determinisztikus */
        for (size_t i = 0; i < nelem; ++i) {
            A[i] = (float)((int)(i % 17) - 8) / 8.0f;
            B[i] = (float)((int)(i % 13) - 6) / 6.0f;
        }

        /* melegites */
        for (size_t w = 0; w < W; ++w)
            if (lab_sgemm(n, A, B, C)) { fputs("kernel error\n", stderr); return 1; }

        /* meres */
        double t[R];
        for (size_t r = 0; r < R; ++r) {
            double t0 = now_seconds();
            if (lab_sgemm(n, A, B, C)) { fputs("kernel error\n", stderr); return 1; }
            t[r] = now_seconds() - t0;
        }
        double med = median(t, R);
        if (med <= 0) { fputs("timer resolution\n", stderr); return 2; }
        double gflops = (2.0 * (double)n * (double)n * (double)n)
                        / (med * 1e9);
        printf("n=%zu median=%.6f s GFLOP/s=%.3f\n", n, med, gflops);

        free(A); free(B); free(C); free(Rref);
    }
    return 0;
}

A példa három dolgot mutat: a melegítés és a mért ismétlések szétválasztását, a medián mint elsődleges statisztikát, és a GFLOP/s származtatott metrikát. A tényleges korrektségi összehasonlítás (lab_sgemm kontra lab_sgemm_ref) ettől külön függvényben történik, a toleranciás összehasonlítással.

Előretekintés: a projekt fájllistája. Ebben az alfejezetben még nincs futtatható build. A specifikáció a szerződést rögzíti, a forrásokat a következő alfejezetek készítik el. Az alábbi táblázat a projekt egyetlen hiteles fájllistája: minden fájlnév, a hiteles változatát közlő alfejezet és az, hogy melyik buildbe kerül. A későbbi alfejezetek buildparancsai pontosan ezeket a fájlokat linkelik, a ~/arm-labor/a64/zaro1/ közös könyvtárban.

Fájl Tartalom Hiteles változat Build
lab_sgemm.h a lab_sgemm C-interfésze, LAB_SGEMM_OK/LAB_SGEMM_EINVAL a058 (az itt közölt változattal deklarációra azonos) mindegyik
lab_sgemm.S az assembly modul a058 mindegyik
lab_sgemm_main.c, Makefile egyszerű hívó program a058 lab_sgemm
lab_sgemm_abi.c (+ lab-abi-call.h, lab-abi-call.S az a029-ből) ABI-ellenőrzés a közös teszthívóval a058 lab_sgemm_abi
lab_sgemm_ref.c double referencia a059 (az itt közölt változattal kódra azonos) lab_sgemm_test, lab_sgemm_bench
lab_sgemm_check.h, lab_sgemm_check.c a numerikus szerződés ellenőrzője (lab_fpcr_ok, lab_sgemm_in_contract, lab_sgemm_check) a059 lab_sgemm_test, lab_sgemm_bench
lab_sgemm_test.c korrektségi teszthajtó a059 lab_sgemm_test
lab_sgemm_c.c C összehasonlító változatok a059 lab_sgemm_bench
lab_sgemm_bench.c mérőprogram a059 lab_sgemm_bench
lab_sgemm_spec.c a fenti mérési vázlat ez az alfejezet; csak szemléltetés nincs

A korrektségi teszt tényleges buildje és futtatása tehát A korrektségi tesztkészlet kidolgozott példájában van. Ott a teszthajtó mellé az ellenőrző modul (lab_sgemm_check.c) is linkelődik, és a hívott lab_sgemm az assembly modul. A projektben nincs C nyelvű lab_sgemm implementáció. A C összehasonlító változatok a mérési baseline szerepét töltik be, saját, eltérő nevekkel, és csak a mérőprogramba kerülnek. A teszthajtó ezért hoston nem fordítható natív gcc-vel, mert a hívott függvény AArch64 assembly. A futtatás qemu-aarch64 alatt vagy natív AArch64-en történik.

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

Tünet: a korrektségi teszt n=17n = 17 esetén elbukik, de n=16n = 16-nál zöld. Hipotézis: a blokkméret-határ kezelése hibás; a maradék blokk túlcsordul a következő sorra. Adatgyűjtés: a teszt kiírja az első eltérő (i,j)(i, j) indexet; ez megmutatja, melyik blokkhatáron csúszik el. Egy változtatás: a maradékkezelő ág feltételének módosítása. Ellenőrzés: a teszt újrafuttatása mind a nyolc határesetre.

Tünet: a mért gyorsulás n=16n = 16-nál negatív (az assembly lassabb, mint a C). Hipotézis: a setup-költség (regisztermentés, blokk-bejáratás) nagyobb, mint a nyereség. Adatgyűjtés: az assembly disassemblyjében a függvény prologusának hossza; a perf-szerű ciklusszámlálás hiányában a medián idők összehasonlítása. Egy változtatás: a blokkméret csökkentése vagy a setup egyszerűsítése. Ellenőrzés: n=16n = 16 és n=64n = 64 újramérése.

Tünet: a mérés szórása nagy (MAD > a medián 10%-a). Hipotézis: a host más folyamatai zavarnak, vagy a QEMU nem determinisztikus. Adatgyűjtés: a 11 mért idő kiírása; ha egy kiugró érték van, az külső zavar. Egy változtatás: az ismétlésszám növelése vagy a mérés más időpontban ismétlése. Ellenőrzés: a MAD újraszámítása.

Tünet: a korrektségi teszt minden nn-re „zöld”, de a modul a valós bemeneten hibás. Hipotézis: a teszt bemenete nem fedi le a valós értéktartományt (pl. csak kis értékek). Adatgyűjtés: a teszt bemeneteinek statisztikája. Egy változtatás: nagy értékek és előjeles értékek bevonása. Ellenőrzés: a toleranciás összehasonlítás újrafuttatása.

15.1.10 Feladatok

1. feladat (magyarázó, alapszint — cél: megértés-ellenőrzés). Hasonlítsd össze a három jelölt feladatot (CRC32, memcpy, sávos mátrixszorzás) az alábbi szempontok szerint: számítási dominancia, vektorizálhatóság, cache-érzékenység, referenciaimplementáció egyszerűsége. Melyik esetben lenne az Amdahl-korlát a legszűkebb, és miért? Készíts táblázatot, és indokold, melyik feladatot választanád záróprojektnek, ha a cél a legnagyobb mért gyorsulás elérése.

2. feladat (implementációs, középszint — cél: rutinépítés). Írd meg a lab_sgemm_ref függvényt double pontossággal, valamint a korrektségi teszt keretrendszer vázát, amely a határeset-táblázatot és a speciális tartalmak tesztmátrixát lefedi. A keretrendszer a numerikus szerződés szerinti összehasonlítást (atol + rtol * s_ij, nem véges elem = FAIL) alkalmazza, és minden eltérésnél kiírja az (i,j)(i, j) indexet és a három értéket (asm, ref, különbség).

3. feladat (hibakeresési/mérési, középszint — cél: rutinépítés). Tervezd meg a mérési jegyzőkönyv sablonját: milyen mezőket tartalmaz (host, QEMU-verzió, fordítói verzió, optimalizálási szint, nn, WW, RR, atol, rtol, mért medián, MAD, GFLOP/s, jelölés hogy emulációs idő). Egészítsd ki egy olyan eljárással, amely a mérési zajt csökkenti (melegítés, medián, ismétlés), és indokold, miért nem használnál clock()-ot CLOCK_MONOTONIC helyett.

15.1.11 Megoldások

1. megoldás — Segítség. Melyik feladatnál dominál a memóriasávszélesség a számítás helyett, és ez hogyan hat az Amdahl-modell érvényességére (amely eredetileg számítási párhuzamosításra vonatkozik)?

1. megoldás — Teljes megoldás. A CRC32 esetén a vektorizálható hányad alacsony (a bitenkénti feldolgozás nehezen párhuzamosítható, a táblás változat memóriafüggő), ezért az Amdahl-korlát szűk. A memcpy esetén a memóriasávszélesség a szűk keresztmetszet, nem a nem vektorizálható hányad; az Amdahl-korlát formálisan kevésbé releváns, mert a modell nem a számítási kapacitást írja le. A mátrixszorzásnál a belső ciklus szinte teljesen vektorizálható, a memóriaigény O(n2)O(n^2), a számítási igény O(n3)O(n^3), ezért az Amdahl-korlát a legkevésbé szűk. Egy s=4s = 4 feltevésű modell szerint a gyorsulás legfeljebb 4× lehet; ez a feltevés következménye, nem a hardver garanciája, és a tényleges ss-t mérni kell. A választás indoklása: legnagyobb mért gyorsulás és legjobb mérési standard (GFLOP/s) a mátrixszorzásnál várható.

2. megoldás — Segítség. A referencia double akkumulátorral dolgozik, a bemenet float marad — nézd meg a fejezet korábbi lab_sgemm_ref-re vonatkozó definícióját, és a nyolc határeset + hat tartalomtípus listáját a specifikáció szerint.

2. megoldás — Teljes megoldás. A lab_sgemm_ref a fenti double-os verzió. A teszt keretrendszer vázlata: minden n-re és minden tartalomra generálja a bemenetet, lefuttatja a referenciát és az assemblyt, majd minden elemre ellenőrzi a toleranciát. A határesetek listája a táblázat n értékei; a speciális tartalmak a tesztmátrix szerinti n-eken futnak. Az eltérés kiírása segít lokalizálni a hibát: ha az első eltérés a (0,0)(0, 0) indexnél van, a hiba a teljes mátrixra hat; ha egy blokkhatárnál, a blokkméret-kezelés a hibás. Az összehasonlítás !(eltérés <= korlát) alakú, és a nem véges elemet külön is FAIL-nek minősíti, így a NaN nem adhat „0 maximális hibát”. Hogy ez valóban működik, azt a szándékosan beírt NaN/Inf hibák lebukása igazolja. A szerződésen kívüli túlcsordulási eset saját, dokumentált elvárást kap (minden elem +∞+\infty).

3. megoldás — Segítség. Gondold át, mit mér a clock() valójában (CPU-idő vs. falióra-idő), és miért probléma ez egy olyan mérésnél, ahol más folyamatok vagy I/O is futhatnak a háttérben.

3. megoldás — Teljes megoldás. A jegyzőkönyv sablonja táblázatos formában rögzíti a környezetet és a paramétereket. A clock() a CPU-időt méri, amely több szálon vagy blokkoló I/O esetén félrevezető; a CLOCK_MONOTONIC a falióra-időt méri, amely a valós felhasználói élményt tükrözi. A melegítés a cache és az elágazás-előrejelző feltöltését szolgálja; a medián a kiugró értékeket kiszűri; az ismétlés a véletlen zaj hatását csökkenti. A jegyzőkönyvben jelölni kell, hogy QEMU-emulációban a mérés emulációs idő, nem target-ciklusszám.

15.1.12 Rövid összefoglaló

A projekt-specifikáció öt részből áll: feladat és indoklás, C-interfész, korrektségi terv, teljesítmény-mérési terv és biztonsági önellenőrzési terv. A választott feladat a sávos mátrixszorzás, mert számításigényes, jól vektorizálható, cache-tudatos és iparági standard metrikával mérhető. Az interfész int lab_sgemm(size_t n, const float *restrict A, const float *restrict B, float *restrict C), hibajelzése LAB_SGEMM_OK/LAB_SGEMM_EINVAL. A korrektségi terv egyetlen numerikus szerződést használ (rögzített FPCR, véges bemenet, double referencia, sijs_{ij}-hez mért tűrés); a mérési terv négy változatot (naiv, átrendezett, vektorizált C és assembly) hasonlít össze mediánnal, MAD-dal és GFLOP/s-sal; a biztonsági terv argumentum-ellenőrzést, túlcsordulás-védelmet és mentési szimmetriát ír elő.

Továbbhasznált definíciók: - projekt-specifikáció: a feladat, interfész, korrektségi, teljesítmény- és biztonsági követelmények rögzített dokumentuma. - elfogadási küszöb: a mérési tervben rögzített, számszerű feltétel, amelynek teljesülése esetén a modul elfogadható. - numerikus szerződés: rögzített FPCR, véges bemeneti tartomány (sij≤FLT_MAX/2s_{ij} \le \text{FLT\_MAX}/2, kerekítési tartalékkal), a modul és a referencia számítási modellje, valamint az atol + rtol * s_ij elfogadási feltétel, ahol s_ij = sum_k |a_ik||b_kj|.

15.1.13 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 ténylegesen fennmaradnak: a CLOCK_MONOTONIC felbontása és a QEMU emulációs idő viselkedése az adott hoston és QEMU-verzióban nem ellenőrzött ebben a folyamatban; a konkrét atol/rtol értékek az első mérési ciklusban validálandók. A négy mérési változatot és a tesztkészletet a helyettesítő eszközlánccal (zig cc/Clang + musl, Unicorn-alapú emulátor) lefordítottuk és lefuttattuk; Clang -O3 mellett az i-k-j belső ciklusa vektorizálódott (4 sáv, kétszeres kibontás), az i-j-k redukció nem, a -fno-vectorize változatokban nincs vektoros fmla. A GCC vektorizációs riportját és az emulátoron kívüli időket nem mértük; az emulátorbeli időket nem közöljük.