Obsah
1. Zpracování hodnot typu half float (fp16) na platformě AArch64
2. Podpora technologie NEON v ARMovských jádrech AArch64
3. Pracovní registry používané instrukcemi NEON
4. Podporované formáty prvků vektorů
5. Vrácení hodnoty prvního resp. druhého parametru typu half float
6. Překlad operace součtu dvou hodnot typu half float
7. Překlad využívající instrukce pro přímé operace s hodnotami typu half float
8. Způsob překladu všech čtyř základních aritmetických operací nad hodnotami typu half float
9. Překlad operací pro porovnání dvou hodnot typu half float
10. NEON a operace nad celými vektory
11. Přičtení konstanty ke všem prvkům pole
12. Součet odpovídajících si prvků polí typu half float
13. Pomáháme překladači: klíčové slovo restrict
14. Překlad součtu polí s menším počtem prvků
15. Intrinsic pro provádění operací s hodnotami typu half float
16. Realizace součtu dvou hodnot typu half float přes intrinsic funkci
17. Základní aritmetické operace s hodnotami typu half float realizované přes intrinsic
18. Porovnání operandů typu half float realizované přes intrinsic
19. Repositář s demonstračními příklady
1. Zpracování hodnot typu half float (fp16) na platformě AArch64
Jak již bylo napsáno v perexu dnešního článku, mohou se programátoři, kteří se zabývají vývojem v oblasti generativní AI, zpracování přirozeného jazyka, embeddingu a embedding modelů atd., setkat s nutností zpracování skalárních hodnot či celých vektorů s prvky typu half float (FP16). Důvody jsou zřejmé: pro mnoho účelů rozsah i přesnost hodnot uložených jako half float plně vyhovuje požadavkům řešeného algoritmu a oproti hodnotám single jsou paměťové požadavky poloviční (a v praxi je velký finanční rozdíl v tom, jestli servery vyžadují například 16MB RAM nebo 32MB RAM). V tomto článku si vysvětlíme, jak se s těmito hodnotami pracuje na platformě AArch64, s níž se dnes velmi často setkáme (a to právě i v oblasti GENAI). Zajímavé je, že pro zpracování skalárů i vektorů typu half float je platforma AArch64 vybavena lépe, než klasická x86–64.
V úvodní kapitole si připomeneme základní vlastnosti numerického datového typu half float.
Formát half float, jenž je dnes standardizován v IEEE 754–2008, používá pro ukládání hodnot s plovoucí řádovou čárkou pouhých šestnáct bitů, tj. dva byty. Maximální hodnota je rovna 65504, minimální hodnota (větší než nula) přibližně 5,9×10-8. Předností tohoto formátu je malá bitová šířka (umožňuje paralelní přenos po interních sběrnicích GPU) a také větší rychlost zpracování základních operací, protože pro tak malou bitovou šířku mantisy je možné některé operace „zadrátovat“ a nepočítat pomocí ALU. Také některé iterativní výpočty (sin, cos, sqrt) mohou být provedeny rychleji, než v případě plnohodnotných typů float a single.
| Celkový počet bitů (bytů): | 16 (2) |
| Bitů pro znaménko: | 1 |
| Bitů pro exponent: | 5 |
| Bitů pro mantisu: | 10 |
| BIAS (offset exponentu): | 15 |
| Přesnost: | 3–4 číslice |
| Maximální hodnota: | 65504 |
| Minimální hodnota: | –65504 |
| Nejmenší kladná nenulová hodnota: | 5,96×10-8 |
| Nejmenší kladná normalizovaná hodnota: | 6,104×10-5 |
| Podpora záporné nuly: | ano |
| Podpora +∞: | ano |
| Podpora -∞: | ano |
| Podpora NaN: | ano |
Typ half float je v současnosti mikroprocesory ARM do značné míry podporován, přičemž jsou povoleny i některé vektorové operace, tj. například součet dvou vektorů atd. Ty jsou realizovány technologií NEON.
Pokud se podíváme na historii mikroprocesorů ARM, zjistíme, že cesta k technologii NEON na jádrech AArch64 vlastně nebyla vůbec přímočará. První implementace „vektorových“ operací pro procesory ARM používaly rozhraní pro koprocesory, takže se vlastně používala paralelní/doplňková instrukční sada. Konkrétně se jednalo o technologii nazvanou VFP neboli Vector Floating Point. Touto technologií, která je stále na některých ARMovských jádrech podporována, jsme se již v seriálu o architekturách počítačů zabývali.
Na tomto místě je vhodné zdůraznit, že i přesto, že se v názvu VFP používá termín „vector“, nejednalo se o implementaci skutečných SIMD operací, protože se prvky vektorů zpracovávaly postupně, tedy sekvenčně (stále se však jednalo o vylepšení, protože se ušetřilo načtení instrukce a její dekódování). Z tohoto důvodu byl „vektorový režim“ VFP poměrně rychle nahrazen novou technologií nazvanou NEON označovanou též Advanced SIMD. Dnes se s VFP můžeme na některých ARMovských jádrech setkat, další jádra pak podporují jen VFPLite, kde však každá operace trvá zhruba deset strojových cyklů!
Technologie NEON již podporovala plnohodnotné SIMD operace, při použití vektorů s nejmenšími prvky o velikosti jednoho bajtu až šestnáct operací (například součtu) paralelně. Díky tomu bylo možné implementovat například dekodér pro známý formát MP3 na mikroprocesoru s taktem pouhých 10 MHz popř. implementovat AMR kodek na podobném čipu, ovšem s hodinovým taktem 13 MHz. Při použití klasických výpočtů se skalárními hodnotami by bylo nutné použít čip s vyšší hodinovou frekvencí či naopak – tento výkonný čip by již neměl dostatek výkonu pro provádění dalších činností.
Pro ukázku: ještě mikroprocesory 486DX2 s hodinovou frekvencí 66 MHz měly s dekódováním MP3 velké problémy a zvládaly jen menší bitrate a monofonní výstup). Právě díky NEONu se začaly čipy ARM používat i v některých oblastech, které byly dříve vyhrazeny digitálním signálovým procesorům.
Mimo VFP a NEON bylo pro některá (dnes již notně stará) jádra ARM vyvinuto rozšíření pro DSP operace. Konkrétně se jednalo o jádra ARMv5TE. Nové instrukce byly využitelné například při kódování a dekódování videa, při zpracování zvukového signálu (včetně zvukové syntézy), práci s rastrovými obrazy (image processing) atd. Instrukce typu Load & Store mohly pracovat s registrovými páry, zavedeny byly nové adresovací režimy, aritmetika se saturací (tj. bez přetečení) a taktéž instrukce typu „multiply and accumulate (MAC)“ 16×16 bitů a 32×16 bitů, které mohly být v instrukční pipeline vykonány v rozmezí jednoho taktu (v jejich průběhu se tedy mohly začít zpracovávat další instrukce). Uvádí se, že při zpracování signálů byla výkonnost nových instrukcí v jádrech ARMv5TE dvakrát až třikrát vyšší, než při použití „běžných“ jader ARMv5 (samozřejmě za předpokladu ruční optimalizace kódu, což ostatně až na některé výjimky platí dodnes, protože intrinsic nejsou samospasitelné).
Další odbočkou byla technologie SIMD extensions for multimedia pro jádra ARMv6, která byla později nahrazena Advanced SIMD (NEONem).
Poznámka: skutečná míra paralelnosti SIMD operací se na různých ARM jádrech lišila. Například na Cortex-A8 se sice stále daly provádět operace se 128bitovými registry obsahujícími prvky vektorů (viz navazující kapitoly), ovšem ve skutečnosti se v daný okamžik zpracovávalo jen 64 bitů, tj. každá operace se musela provádět dvakrát. Naproti tomu na Cortex-15 se již zpracovávaly celé 128bitové registry.
2. Podpora technologie NEON v ARMovských jádrech AArch64
Na základě specifikace by všechny standardní implementace jader ARMv8 (s instrukční sadou A64 a s možností přepnutí do režimu AArch64) měly technologii NEON podporovat, stejně jako operace s FP hodnotami. Jinými slovy – na těchto jádrech už typicky není nutné řešit například problém „hardfp versus softfp“. Ovšem v budoucnosti se pravděpodobně setkáme i se specializovanými jádry nakonfigurovanými odlišným způsobem, například:
- NEON nebude vůbec podporován.
- FP operace nebudou vůbec podporovány.
- NEON+FP budou podporovány, ale bez zpracování výjimek u FP operací.
V tomto článku nás ovšem nejvíce zajímá podpora pro half float. Ta existuje ve dvou verzích:
- Pouze konverzní operace mezi hodnotami uloženými v registrech H. (viz další kapitolu)
- Podpora pro základní aritmetické operace prováděné přímo s hodnotami typu half float
3. Pracovní registry používané instrukcemi NEON
Původní technologie NEON, která byla implementovaná na 32bitových jádrech ARM, používala sadu třiceti dvou nových pracovních registrů, přičemž každý takový registr měl šířku 64 bitů. Tato sada byla oddělena od klasických celočíselných pracovních registrů, což samozřejmě zvýšilo možnosti překladače při optimalizacích kódu (a teoreticky to mohlo umožnit souběžné provádění výpočtů). Nové registry byly pojmenovány D0 až D31 (písmeno D v tomto případě značí double), popř. mohly být vždy dva sousední registry spojeny do jednoho 128bitového registru (ty byly pojmenovány Q0 až Q15, písmeno Q v tomto případě znamená quad). Pokud jádro kromě NEONu podporovalo i výše zmíněnou VFP (konkrétně VFPv3 nebo VFPv4), byly registry D0 až D31 sdíleny mezi oběma jednotkami.
U jader ARMv8-A s novou instrukční sadou A64 došlo v této oblasti k poměrně důležitému vylepšení, protože programátoři mají nově k dispozici 32 pracovních registrů, ovšem nyní se jedná o plnohodnotné 128bitové registry. Tyto registry jsou současně používány i při běžných matematických operacích s hodnotami uloženými v systému plovoucí řádové čárky a taktéž kryptografickým modulem. Došlo ještě k další změně – již nedochází k rozdělení jednoho registru pro typ double do dvou registrů pro hodnotu typu single/float, jako tomu bylo u VFP. Nově je možné každý registr použít jako 128bitový vektor, pro uložení hodnoty double (spodních 64 bitů), uložení hodnoty typu single/float (spodních 32 bitů) popř. (což nás dnes zajímá nejvíce) pro uložení hodnoty typu half float (spodních 16 bitů).
Horní bity jsou při čtení ignorovány, při zápisu nulovány:
Používá se následující pojmenování registrů:
| Jméno | Význam |
|---|---|
| v0..v31 | 128bitové registry (využívá se celá jejich bitová šířka) |
| d0..d31 | spodních 64 bitů registrů v0..v31, použito pro hodnoty typu double |
| s0..s31 | spodních 32 bitů registrů v0..v31, použito pro hodnoty typu single/float |
| h0..h31 | spodních 16 bitů registrů v0..v31, použito pro hodnoty typu half float |
4. Podporované formáty prvků vektorů
Ještě před popisem toho, jakým způsobem se registry používají ve funkci vektorů, se zmiňme o použitých výrazech. U technologie NEON se používá následující terminologie:
- Vector vždy značí 64bitovou či 128bitovou část pracovního registru Vn, která je rozdělena na prvky různých typů (s obecně menším počtem bitů, než má celý vektor).
- Element je prvek vektoru.
- Lane označuje index prvku vektoru. U mnoha operací se kombinují prvky z různých vektorů, které mají shodný index. Příkladem je součet vektorů, který je (logicky) prováděn po odpovídajících si prvcích.
Indexy jednotlivých lanes se zvyšují směrem od nejnižšího bitu k bitu nejvyššímu. Pro pracovní registry V0 až V31 a použitý typ prvků mohou indexy nabývat těchto hodnot:
| Vektor | Indexy jednotlivých lanes |
|---|---|
| 128bitový registr | × |
| 2×64 bitů | 1, 0 |
| 4×32 bitů | 3, 2, 1, 0 |
| 8×16 bitů | 7, 6, ..0 |
| 16×8 bitů | 15..0 |
Víme již, že je možné namísto 128bitových vektorů používat i vektory 64bitové, tj. registrové aliasy D0 až D31. Zde samozřejmě bude k dispozici jen polovina indexů, resp. jen polovina prvků vektorů:
| Vektor | Indexy jednotlivých lanes |
|---|---|
| 64bitový registr | × |
| 1×64 bitů | 0 |
| 2×32 bitů | 1, 0 |
| 4×16 bitů | 3, 2, 1, 0 |
| 8×8 bitů | 7, 6, ..0 |
5. Vrácení hodnoty prvního resp. druhého parametru typu half float
Nejprve si ukážeme naprosté základy práce s hodnotami typu half float, které jsou v jazyku C popsány jako typ _Float16 (můžete se ovšem setkat i s použitím __fp16). Hodnoty typu half float jsou předávány přes registry Vxx popsané výše: první parametr v registru V0, druhý v registru V1 atd. Návratová hodnota typu half float (pokud funkce hodnotu tohoto typu vrací) je uložena v registru V0:
_Float16 fp16_first(_Float16 x, _Float16 y) {
return x;
}
_Float16 fp16_second(_Float16 x, _Float16 y) {
return y;
}
První z těchto funkcí je přeložena do jediné instrukce RET, protože jak první parametr, tak i výsledek funkce je předáván přes stejný registr V0:
fp16_first:
ret
Druhá funkce obsahuje pouze operaci přesunu jedné hodnoty typu half float z registru V1 (druhý parametr) do registru V0 (návratová hodnota):
fp16_second:
mov v0.h[0], v1.h[0]
ret
6. Překlad operace součtu dvou hodnot typu half float
Pokusme se nyní přeložit velmi jednoduchou funkci, která provede součet dvojice hodnot typu half float. Tento typ se v GCC jmenuje _Float16. Realizace takové funkce je triviální:
_Float16 fp16_add(_Float16 x, _Float16 y) {
_Float16 z = x + y;
return z;
}
Překlad pro platformu AArch64 může dopadnout následovně:
fp16_add:
fcvt s1, h1
fcvt s0, h0
fadd s0, s0, s1
fcvt h0, s0
ret
Povšimněte si, že se nejdříve instrukcemi FCVT provede převod obou operandů předaných v registrech H0 a H1 na typ single, následně se instrukci FADD provede součet obou hodnot typu single a posléze se provede převod zpět na typ half float, a to opět instrukcí FCVT.
7. Překlad využívající instrukce pro přímé operace s hodnotami typu half float
Výše uvedený výsledek překladu pochopitelně není ideální, protože se provádí konverze z typu half float na single a zpět. Ovšem pokud překladači naznačíme, že cílová platforma umožňuje přímé provádění (některých) operací s hodnotami typu half float, bude situace odlišná. Podpora pro half float byla přidána do ARMv8.2, kterou mnohá existující ARM jádra splňují. Upravíme tedy příkaz pro překlad – předáme následující přepínače:
-O9 -ffast-math -march=armv8.2-a+fp16
Nyní se ve vygenerovaném strojovém kódu nebudou žádné konverze provádět a pouze se přímo sečtou obě předané hodnoty typu half float s tím, že výsledkem je opět hodnota stejného typu:
fp16_add(_Float16, _Float16):
fadd h0, h0, h1
ret
8. Způsob překladu všech čtyř základních aritmetických operací nad hodnotami typu half float
Pro úplnost se ještě podívejme na to, jakým způsobem se přeloží všechny čtyři základní aritmetické operace, tj. nikoli pouze operace součtu, ale i rozdílu, součinu a podílu (modulo není pro typy s plovoucí řádovou čárkou definována):
_Float16 fp16_add(_Float16 x, _Float16 y) {
return x+y;
}
_Float16 fp16_sub(_Float16 x, _Float16 y) {
return x-y;
}
_Float16 fp16_mul(_Float16 x, _Float16 y) {
return x*y;
}
_Float16 fp16_div(_Float16 x, _Float16 y) {
return x/y;
}
Překlad v případě, že nebudeme přímo a explicitně vyžadovat použití instrukcí half float, bude opět používat převody mezi typy half float a single:
fp16_add:
fcvt s1, h1
fcvt s0, h0
fadd s0, s0, s1
fcvt h0, s0
ret
fp16_sub:
fcvt s1, h1
fcvt s0, h0
fsub s0, s0, s1
fcvt h0, s0
ret
fp16_mul:
fcvt s1, h1
fcvt s0, h0
fmul s0, s0, s1
fcvt h0, s0
ret
fp16_div:
fcvt s1, h1
fcvt s0, h0
fdiv s0, s0, s1
fcvt h0, s0
ret
Pokud naopak použijeme přepínač -march=armv8.2-a+fp16 (nebo i vyšší verzi architektury), bude výsledkem překladu skutečně optimalizovaný strojový kód:
fp16_add(_Float16, _Float16):
fadd h0, h0, h1
ret
fp16_sub(_Float16, _Float16):
fsub h0, h0, h1
ret
fp16_mul(_Float16, _Float16):
fmul h0, h0, h1
ret
fp16_div(_Float16, _Float16):
fdiv h0, h0, h1
ret
9. Překlad operací pro porovnání dvou hodnot typu half float
Kromě provádění základních aritmetických operací se v praxi velmi často porovnávají dvě hodnoty na relaci „je rovno“, „nerovno“, „větší než“, „větší nebo rovno“, „menší než“ a „menší nebo rovno“. Programovací jazyk C má pro tyto operace vyhrazenu šestici operátorů a v případě GCC je možné tyto operace provádět i s hodnotami typu half float (v GCC _Float16).
Ostatně si můžeme tyto operace jednoduše otestovat:
bool fp16_eq(_Float16 x, _Float16 y) {
return x == y;
}
bool fp16_ne(_Float16 x, _Float16 y) {
return x != y;
}
bool fp16_gt(_Float16 x, _Float16 y) {
return x > y;
}
bool fp16_ge(_Float16 x, _Float16 y) {
return x >= y;
}
bool fp16_lt(_Float16 x, _Float16 y) {
return x < y;
}
bool fp16_le(_Float16 x, _Float16 y) {
return x <= y;
}
Překlad bez povolení přímých manipulací s hodnotami typu half float bude opět založen na konverzi těchto hodnot na typy single. Následně je provedena instrukce FCMP nebo FCMPE a do registru W0 je uložen výsledek porovnání (získaný z příznakových bitů):
fp16_eq(_Float16, _Float16):
fcvt s0, h0
fcvt s1, h1
fcmp s0, s1
cset w0, eq
ret
fp16_ne(_Float16, _Float16):
fcvt s0, h0
fcvt s1, h1
fcmp s0, s1
cset w0, ne
ret
fp16_gt(_Float16, _Float16):
fcvt s0, h0
fcvt s1, h1
fcmpe s0, s1
cset w0, gt
ret
fp16_ge(_Float16, _Float16):
fcvt s0, h0
fcvt s1, h1
fcmpe s0, s1
cset w0, ge
ret
fp16_lt(_Float16, _Float16):
fcvt s0, h0
fcvt s1, h1
fcmpe s0, s1
cset w0, mi
ret
fp16_le(_Float16, _Float16):
fcvt s0, h0
fcvt s1, h1
fcmpe s0, s1
cset w0, ls
ret
Mohlo by se zdát, že pokud povolíme přímé manipulace s typem half float, bude výsledný strojový kód kratší (a tím pádem i rychlejší), tj. vynechají se konverzní instrukce FCVT atd. Ovšem není tomu tak, o čemž se můžeme snadno přesvědčit:
fp16_eq(_Float16, _Float16):
fcvt s0, h0
fcvt s1, h1
fcmp s0, s1
cset w0, eq
ret
fp16_ne(_Float16, _Float16):
fcvt s0, h0
fcvt s1, h1
fcmp s0, s1
cset w0, ne
ret
fp16_gt(_Float16, _Float16):
fcvt s0, h0
fcvt s1, h1
fcmpe s0, s1
cset w0, gt
ret
fp16_ge(_Float16, _Float16):
fcvt s0, h0
fcvt s1, h1
fcmpe s0, s1
cset w0, ge
ret
fp16_lt(_Float16, _Float16):
fcvt s0, h0
fcvt s1, h1
fcmpe s0, s1
cset w0, mi
ret
fp16_le(_Float16, _Float16):
fcvt s0, h0
fcvt s1, h1
fcmpe s0, s1
cset w0, ls
ret
10. NEON a operace nad celými vektory
Instrukce NEON na 64bitových mikroprocesorech AArch64 podporují vektory s elementy (prvky) těchto typů:
| Typ | Šířka | Poznámka |
|---|---|---|
| float | 32 bitů | hodnota s plovoucí řádovou čárkou, jednoduchá přesnost |
| double | 64 bitů | hodnota s plovoucí řádovou čárkou, dvojitá přesnost |
| half | 32 bitů | hodnota s plovoucí řádovou čárkou, poloviční přesnost (jen převody) |
| int8 | 8 bitů | osmibitové celé číslo se znaménkem, obecné použití |
| int16 | 16 bitů | 16bitové celé číslo se znaménkem, obecné použití |
| int32 | 32 bitů | 32bitové celé číslo se znaménkem, obecné použití |
| int64 | 64 bitů | 64bitové celé číslo se znaménkem, obecné použití |
| uint8 | 8 bitů | osmibitové celé číslo bez znaménka, typicky barvová složka pixelu |
| uint16 | 16 bitů | 16bitové celé číslo bez znaménka, typicky zvukový vzorek |
| uint32 | 32 bitů | 32bitové celé číslo bez znaménka, obecné použití |
| uint64 | 64 bitů | 64bitové celé číslo bez znaménka, obecné použití |
| polynomial | 8 bitů | používán pro výpočty korekcí chyb atd. |
| polynomial | 16 bitů | používán pro výpočty korekcí chyb atd. |
Nás nyní budou zajímat především „vektorizace“ operací prováděných na poli obsahujícími prvky typu half float.
11. Přičtení konstanty ke všem prvkům pole
V dalším ukázkovém příkladu je realizována funkce, která ke všem prvkům pole s prvky typu half float o známé délce přičítá konstantu předanou formou argumentu. Celý příklad je naprogramován naivním způsobem, bez jakékoli snahy o optimalizace na úrovni céčkovského zdrojového kódu:
#define LENGTH 32
void fp16_delta(_Float16 values[LENGTH], _Float16 delta) {
int i;
for (i=0; i<LENGTH; i++) {
values[i] += delta;
}
}
V případě, že není povoleno rozšíření instrukční sady o fp16, bude výsledkem překladu explicitně vygenerovaná programová smyčka, ve které se (v každé iteraci) provádí dvojice konverzí instrukcí FCVT: z half float na single a naopak ze single na half float. Výsledek tedy nebude příliš rychlý:
fp16_delta:
fcvt s0, h0
add x1, x0, 64
.L2:
ldr h31, [x0]
fcvt s31, h31
fadd s31, s31, s0
fcvt h31, s31
str h31, [x0], 2
cmp x0, x1
bne .L2
ret
Pokud jsou operace fp16 povoleny, bude překlad proveden odlišným způsobem. Součet 32 prvků pole je rozdělen do čtyř vektorových instrukcí FADD. Povšimněte si, jak se „role“ registrů mění pro různé operace – někdy se pracuje s celými 128bitovými registry Qx, jindy se stejným registrem, ovšem tak, že jeho obsah bude považován za vektor osmi 16bitových hodnot typu half float (Vx.8h):
fp16_delta(_Float16*, _Float16):
dup v0.8h, v0.h[0]
ldp q3, q2, [x0]
ldp q1, q31, [x0, 32]
fadd v3.8h, v3.8h, v0.8h
fadd v2.8h, v2.8h, v0.8h
fadd v1.8h, v1.8h, v0.8h
fadd v31.8h, v31.8h, v0.8h
stp q3, q2, [x0]
stp q1, q31, [x0, 32]
ret
12. Součet odpovídajících si prvků polí typu half float
Zajímavé bude taktéž zjistit, jakým způsobem bude přeložena funkce, která provede součet odpovídajících si prvků polí, tedy vlastně klasický vektorový součet. Pokud si tento problém značně zjednodušíme tím, že nebudeme předávat délku polí (a kontrolovat, zda mají obě pole stejnou délku atd.) ani používat přístup k prvkům přes ukazatele, je možné součet napsat následovně:
#define SIZE 32
void add_arrays(_Float16 *a, _Float16 *b) {
int i;
for (i=0; i<SIZE; i++) {
a[i] += b[i];
}
}
Pokud nebude povolena přímá manipulace s hodnotami typu half float, bude překlad proveden do formy počítané programové smyčky, zde konkrétně s počitadlem uloženým v registru X2, které se zvyšuje o dvojku, protože je tento registr současně použitý pro výpočet offsetu:
mov x2, 0
.L2:
...
...
...
add x2, x2, 2
cmp x2, 64
bne .L2
Uvnitř těla smyčky se oba prvky načtou z adres X0+X2 a X1+X2, převedou na hodnoty typu single, sečtou a uloží na adresu X0+X2:
ldr h31, [x0, x2]
ldr h30, [x1, x2]
fcvt s31, h31
fcvt s30, h30
fadd s30, s31, s30
fcvt h30, s30
str h30, [x0, x2]
Výsledek vypadá následovně:
add_arrays(_Float16*, _Float16*):
mov x2, 0
.L2:
ldr h31, [x0, x2]
ldr h30, [x1, x2]
fcvt s31, h31
fcvt s30, h30
fadd s30, s31, s30
fcvt h30, s30
str h30, [x0, x2]
add x2, x2, 2
cmp x2, 64
bne .L2
ret
Při povolení přímé manipulace s hodnotami typu half float překladač zjistí, jestli se pole nepřekrývají. Pokud ano, skočí do běžné smyčky, ve které se již neprovádí převody hodnot:
mov x2, 0
.L2:
ldr h23, [x0, x2]
ldr h22, [x1, x2]
fadd h22, h23, h22
str h22, [x0, x2]
add x2, x2, 2
cmp x2, 64
bne .L2
Pokud se smyčky nepřekrývají, je celý výpočet rozbalen a proveden po vektorech se čtyřmi prvky:
ldr q24, [x1]
ldp q25, q27, [x0]
ldp q29, q31, [x0, 32]
fadd v24.8h, v25.8h, v24.8h
str q24, [x0]
ldr q26, [x1, 16]
fadd v26.8h, v27.8h, v26.8h
str q26, [x0, 16]
ldr q28, [x1, 32]
fadd v28.8h, v29.8h, v28.8h
str q28, [x0, 32]
ldr q30, [x1, 48]
fadd v30.8h, v31.8h, v30.8h
str q30, [x0, 48]
Celkový výsledek vypadá takto:
add_arrays(_Float16*, _Float16*):
sub x2, x0, x1
sub x2, x2, #2
cmp x2, 12
bls .L4
ldr q24, [x1]
ldp q25, q27, [x0]
ldp q29, q31, [x0, 32]
fadd v24.8h, v25.8h, v24.8h
str q24, [x0]
ldr q26, [x1, 16]
fadd v26.8h, v27.8h, v26.8h
str q26, [x0, 16]
ldr q28, [x1, 32]
fadd v28.8h, v29.8h, v28.8h
str q28, [x0, 32]
ldr q30, [x1, 48]
fadd v30.8h, v31.8h, v30.8h
str q30, [x0, 48]
ret
.L4:
mov x2, 0
.L2:
ldr h23, [x0, x2]
ldr h22, [x1, x2]
fadd h22, h23, h22
str h22, [x0, x2]
add x2, x2, 2
cmp x2, 64
bne .L2
ret
13. Pomáháme překladači: klíčové slovo restrict
V případě, že sčítáme dvě rozdílná pole, by bylo vhodné překladači nějakým způsobem napovědět, že nemusí do výsledného kódu vkládat i variantu s překrývajícími se poli. Pokud by byla pole odlišného typu (resp. pokud by ukazatele byly odlišného typu), je řešení snadné – podle normy je chování překladače nedefinované :-). Ovšem naše pole mají shodný typ (přesněji řečeno oba ukazatele mají totožný typ _Float16 *), takže překladač musí předpokládat, že se může jednat o stejné ukazatele nebo že se budou oblasti překrývat. Jak překladači napovědět, že k této situaci nedojde a nemusí se jí zabývat? V jazyku C, konkrétně ve verzi C99 a vyšší, je pro tento účel rezervováno slovo restrict, které se používá následujícím způsobem (pro ANSI C lze v GCC použít i __restrict__):
#define SIZE 32
void add_arrays(_Float16 * restrict a, _Float16 * restrict b) {
int i;
for (i=0; i<SIZE; i++) {
a[i] += b[i];
}
}
Překladač v tomto případě může provést optimalizace předpokládající, že se sčítají odlišná pole a výsledek bude mnohem kratší a pro nás i čitelnější – jen čtyři „vektorové“ součty:
add_arrays:
ldp q24, q26, [x1]
ldp q28, q30, [x1, 32]
ldp q25, q27, [x0]
ldp q29, q31, [x0, 32]
fadd v24.8h, v25.8h, v24.8h
fadd v26.8h, v27.8h, v26.8h
fadd v28.8h, v29.8h, v28.8h
fadd v30.8h, v31.8h, v30.8h
stp q24, q26, [x0]
stp q28, q30, [x0, 32]
ret
14. Překlad součtu polí s menším počtem prvků
V případě, že je počet prvků sčítaných polí relativně malý a pokud jsou povoleny maximální optimalizace, může se překladač rozhodnout, že celou smyčku s výpočtem rozbalí. Opět si to pochopitelně otestujeme:
#define SIZE 16
void add_arrays(_Float16 *a, _Float16 *b) {
int i;
for (i=0; i<SIZE; i++) {
a[i] += b[i];
}
}
Překlad za předpokladu, že nejsou povoleny přímé manipulace s prvky typu half float. Došlo k rozbalení smyčky, ovšem stále můžeme vidět stejný vzor: FCVT+FCVF+FADD+FCVT:
add_arrays(_Float16*, _Float16*):
ldr h24, [x1]
ldr h25, [x0]
ldr h27, [x0, 2]
fcvt s24, h24
ldr h29, [x0, 4]
fcvt s25, h25
ldr h31, [x0, 6]
fcvt s27, h27
ldr h1, [x0, 8]
fcvt s29, h29
ldr h3, [x0, 10]
fcvt s31, h31
ldr h5, [x0, 12]
fadd s24, s25, s24
fcvt s1, h1
fcvt s3, h3
ldr h7, [x0, 14]
fcvt s5, h5
ldr h17, [x0, 16]
ldr h19, [x0, 18]
fcvt h24, s24
fcvt s7, h7
fcvt s17, h17
ldr h21, [x0, 20]
fcvt s19, h19
str h24, [x0]
fcvt s21, h21
ldr h26, [x1, 2]
fcvt s26, h26
fadd s26, s27, s26
fcvt h26, s26
str h26, [x0, 2]
ldr h28, [x1, 4]
fcvt s28, h28
fadd s28, s29, s28
fcvt h28, s28
str h28, [x0, 4]
ldr h30, [x1, 6]
fcvt s30, h30
fadd s30, s31, s30
fcvt h30, s30
str h30, [x0, 6]
ldr h0, [x1, 8]
fcvt s0, h0
fadd s0, s1, s0
fcvt h0, s0
str h0, [x0, 8]
ldr h2, [x1, 10]
fcvt s2, h2
fadd s2, s3, s2
fcvt h2, s2
str h2, [x0, 10]
ldr h4, [x1, 12]
fcvt s4, h4
fadd s4, s5, s4
fcvt h4, s4
str h4, [x0, 12]
ldr h6, [x1, 14]
fcvt s6, h6
fadd s6, s7, s6
fcvt h6, s6
str h6, [x0, 14]
ldr h16, [x1, 16]
fcvt s16, h16
fadd s16, s17, s16
fcvt h16, s16
str h16, [x0, 16]
ldr h18, [x1, 18]
fcvt s18, h18
fadd s18, s19, s18
fcvt h18, s18
str h18, [x0, 18]
ldr h20, [x1, 20]
fcvt s20, h20
fadd s20, s21, s20
fcvt h20, s20
str h20, [x0, 20]
ldr h22, [x1, 22]
ldr h23, [x0, 22]
ldr h25, [x0, 24]
fcvt s22, h22
ldr h27, [x0, 26]
fcvt s23, h23
ldr h29, [x0, 28]
fcvt s25, h25
ldr h31, [x0, 30]
fcvt s27, h27
fcvt s29, h29
fadd s22, s23, s22
fcvt s31, h31
fcvt h22, s22
str h22, [x0, 22]
ldr h24, [x1, 24]
fcvt s24, h24
fadd s24, s25, s24
fcvt h24, s24
str h24, [x0, 24]
ldr h26, [x1, 26]
fcvt s26, h26
fadd s26, s27, s26
fcvt h26, s26
str h26, [x0, 26]
ldr h28, [x1, 28]
fcvt s28, h28
fadd s28, s29, s28
fcvt h28, s28
str h28, [x0, 28]
ldr h30, [x1, 30]
fcvt s30, h30
fadd s30, s31, s30
fcvt h30, s30
str h30, [x0, 30]
ret
Definice, že se pole nepřekrývají + povolení práce s hodnotami typu half float vede k mnohem kratšímu a pochopitelně i rychlejšímu kódu:
add_arrays:
ldp q29, q31, [x0]
ldp q28, q30, [x1]
fadd v28.8h, v29.8h, v28.8h
fadd v30.8h, v31.8h, v30.8h
stp q28, q30, [x0]
ret
15. Intrinsic pro provádění operací s hodnotami typu half float
Intrinsic, které se taktéž v některých dokumentech označují možná přiléhavějším slovem built-ins, jsou z pohledu vývojáře (a taktéž z pohledu syntaxe jazyka C) funkce, které jsou rozeznávány a implementovány přímo překladačem (v našem konkrétním případě překladačem programovacího jazyka C), aniž by musely být deklarovány ve vyvíjeném programu nebo aniž by byly součástí nějakých knihoven. Překladač tedy nemusí generovat kód pro načtení runtime knihovny s těmito pseudofunkcemi, řešit volání těchto pseudofunkcí, ale naopak může využít všechny v dané situaci dostupné optimalizační strategie (typicky se intrinsic do kódu vkládá jako sekvence strojových instrukcí).
V předchozím odstavci je sice napsáno, že intrinsic (built-ins) vypadají z pohledu syntaxe programovacího jazyka C jako běžné funkce (a tak je tedy bude používat vývojář popř. vývojové prostředí), ovšem ve skutečnosti se o plnohodnotné funkce nejedná, protože strojový kód pro ně generuje přímo překladač na základě interních pravidel. Do jisté míry se intrinsic podobají inline funkcím, protože i u nich lze zcela odstranit problematické předávání parametrů a pamatování návratových hodnot.
Proč však vlastně intrinsic vznikly a proč se používají zrovna v programovacím jazyku C, který je v ostatních ohledech navržen takovým způsobem, aby byl prakticky zcela abstrahován (na rozdíl od například Pascalu nebo i Go) od konkrétních knihoven a funkcí? V některých případech je nutné umožnit programátorům přístup ke specializovaným instrukcím, jejichž sémantiku není možné dobře vyjádřit přímo v C. A přesně toto je případ SIMD instrukcí, ať již na platformě x86–64, tak i na ARMech či na RISC-V. Z tohoto důvodu se budeme intrinsic zabývat v dalším textu; zaměříme se přitom opět na GCC, jehož vlastnosti jsou postupně přebírány i do Clangu.
Intrinsic pro architekturu ARM32 i AArch64 naleznete na adrese https://developer.arm.com/architectures/instruction-sets/intrinsics/. V GCC sice mohou některé z popsaných intrinsic chybět, ovšem ty základní lze použít i v tomto překladači jazyka C.
16. Realizace součtu dvou hodnot typu half float přes intrinsic funkci
Podívejme se na velmi jednoduchý příklad použití intrinic funkce. Konkrétně se bude jednat o funkci vaddh_f16 pro provedení součtu hodnot typu half float:
#include <arm_fp16.h>
_Float16 fp16_add(_Float16 x, _Float16 y) {
return vaddh_f16(x, y);
}
Výsledek překladu by měl vypadat následovně:
fp16_add(_Float16, _Float16):
fadd h0, h0, h1
ret
include/arm_fp16.h: In function 'add_arrays':
include/arm_fp16.h:421:1: error: inlining failed in call to 'always_inline' 'vaddh_f16': target specific option mismatch
421 | vaddh_f16 (float16_t __a, float16_t __b)
| ^~~~~~~~~
:6:12: note: called from here
6 | return vaddh_f16(a, b);
| ^~~~~~~~~~~~~~~
17. Základní aritmetické operace s hodnotami typu half float realizované přes intrinsic
Instrinsic pochopitelně existují i pro další tři základní aritmetické operace, takže je můžeme snadno použít:
#include <arm_fp16.h>
_Float16 fp16_add(_Float16 x, _Float16 y) {
return vaddh_f16(x, y);
}
_Float16 fp16_sub(_Float16 x, _Float16 y) {
return vsubh_f16(x, y);
}
_Float16 fp16_mul(_Float16 x, _Float16 y) {
return vmulh_f16(x, y);
}
_Float16 fp16_div(_Float16 x, _Float16 y) {
return vdivh_f16(x, y);
}
Výsledky ukazují, že jsou vygenerovány základní instrukce pro zpracování hodnot typu half float:
fp16_add(_Float16, _Float16):
fadd h0, h0, h1
ret
fp16_sub(_Float16, _Float16):
fsub h0, h0, h1
ret
fp16_mul(_Float16, _Float16):
fmul h0, h0, h1
ret
fp16_div(_Float16, _Float16):
fdiv h0, h0, h1
ret
18. Porovnání operandů typu half float realizované přes intrinsic
Mezi další intrinsic, které je možné přímo zavolat, patří vestavěné funkce určené pro porovnání dvou hodnot typu half float. V jejich názvu je vždy zapsána testovaná relace (EQ=equal atd.) a mají příponu _f16. Opět si vyzkoušejme základní způsob jejich použití:
#include <arm_fp16.h>
bool fp16_eq(_Float16 x, _Float16 y) {
return vceqh_f16(x, y);
}
bool fp16_ne(_Float16 x, _Float16 y) {
return !vceqh_f16(x, y);
}
bool fp16_gt(_Float16 x, _Float16 y) {
return vcgth_f16(x, y);
}
bool fp16_ge(_Float16 x, _Float16 y) {
return vcgeh_f16(x, y);
}
bool fp16_lt(_Float16 x, _Float16 y) {
return vclth_f16(x, y);
}
bool fp16_le(_Float16 x, _Float16 y) {
return vcleh_f16(x, y);
}
Z výsledků je patrné, že překlad je proveden odlišně od překladu z deváté kapitoly. Nejprve je zavolána instrukce FCMEQ, FCMGT nebo FCMGE, které provedou porovnání a v případě, že je testovaná podmínka splněna, nastaví se všech šestnáct registrů cílového registru na jedničky (pokud podmínka splněna naopak není, jsou všechny bity vynulovány). Následně je oněch šestnáct bitů přeneseno do celočíselného registru a je proveden test jeho hodnoty (popř. se výsledek zneguje, protože jen třemi typy porovnání musíme realizovat všech šest testů na relaci):
fp16_eq(_Float16, _Float16):
fcmeq h31, h0, h1
umov w0, v31.h[0]
tst w0, 65535
cset w0, ne
ret
fp16_ne(_Float16, _Float16):
fcmeq h31, h0, h1
umov w0, v31.h[0]
tst w0, 65535
cset w0, eq
ret
fp16_gt(_Float16, _Float16):
fcmgt h31, h0, h1
umov w0, v31.h[0]
tst w0, 65535
cset w0, ne
ret
fp16_ge(_Float16, _Float16):
fcmge h31, h0, h1
umov w0, v31.h[0]
tst w0, 65535
cset w0, ne
ret
fp16_lt(_Float16, _Float16):
fcmgt h31, h1, h0
umov w0, v31.h[0]
tst w0, 65535
cset w0, ne
ret
fp16_le(_Float16, _Float16):
fcmge h31, h1, h0
umov w0, v31.h[0]
tst w0, 65535
cset w0, ne
ret
19. Repositář s demonstračními příklady
Demonstrační příklady napsané v jazyku C, které jsou určené pro překlad s využitím překladače gcc pro platformu AArch64, byly uloženy do Git repositáře, který je dostupný na adrese https://github.com/tisnik/8bit-fame. Jednotlivé demonstrační příklady si můžete v případě potřeby stáhnout i jednotlivě bez nutnosti klonovat celý (dnes již poměrně rozsáhlý) repositář:
| # | Příklad | Stručný popis | Adresa |
|---|---|---|---|
| 1 | fp16_add.c | operace součtu dvou hodnot typu half float | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_add.c |
| 2 | fp16_add.asm | překlad operace součtu dvou hodnot typu half float | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_add.asm |
| 3 | fp16_add_fp16.asm | překlad využívající instrukce pro přímé operace s hodnotami typu half float | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_add_fp16.asm |
| 4 | fp16_arith.c | všechny čtyři základní aritmetické operace nad typy half float | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_arith.c |
| 5 | fp16_arith.asm | překlad všech čtyř základních aritmetických operací | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_arith.asm |
| 6 | fp16_arith_fp16.asm | překlad všech čtyř základních aritmetických operací s přímými instrukcemi pro half float | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_arith_fp16.asm |
| 7 | fp16_comparison.c | realizace všech šesti operací pro porovnání dvou hodnot typu half float | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_comparison.c |
| 8 | fp16_comparison.asm | překlad operací pro porovnání dvou hodnot typu half float | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_comparison.asm |
| 9 | fp16_comparison_fp16.asm | překlad operací pro porovnání dvou hodnot typu half float s přímými instrukcemi | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_comparison_fp16.asm |
| 10 | fp16_add_delta.c | přičtení konstanty ke všem prvkům pole | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_add_delta.c |
| 11 | fp16_add_delta.asm | překlad do strojového kódu bez přímého povolení manipulace s typy half float | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_add_delta.asm |
| 12 | fp16_add_delta_fp16.asm | překlad do strojového kódu s přímým povolení manipulace s typy half float | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_add_delta_fp16.asm |
| 13 | fp16_add_arrays32.c | součet odpovídajících si prvků polí typu half float se známou délkou | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_add_arrays32.c |
| 14 | fp16_add_arrays32.asm | překlad do strojového kódu bez přímého povolení manipulace s typy half float | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_add_arrays32.asm |
| 15 | fp16_add_arrays32_fp16.asm | překlad do strojového kódu s přímým povolení manipulace s typy half float | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_add_arrays32_fp16.asm |
| 16 | fp16_add_arrays32_restrict.c | součet odpovídajících si prvků polí typu half float, zajištění, že se pole nepřekrývají | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_add_arrays32_restrict.c |
| 17 | fp16_add_arrays32_restrict.asm | překlad do strojového kódu s přímým povolení manipulace s typy half float | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_add_arrays32_restrict.asm |
| 18 | fp16_add_arrays16.c | součet odpovídajících si prvků polí typu half float se známou délkou | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_add_arrays16.c |
| 19 | fp16_add_arrays16.asm | překlad do strojového kódu bez přímého povolení manipulace s typy half float | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_add_arrays16.asm |
| 20 | fp16_add_arrays16_fp16.asm | překlad do strojového kódu s přímým povolení manipulace s typy half float | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_add_arrays16_fp16.asm |
| 21 | fp16_add_arrays16_restrict.c | součet odpovídajících si prvků polí typu half float, zajištění, že se pole nepřekrývají | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_add_arrays16_restrict.c |
| 22 | fp16_add_arrays16_restrict.asm | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_add_arrays16_restrict.asm | |
| 23 | fp16_first_second.c | funkce vracející svůj první nebo naopak druhý parametr | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_first_second.c |
| 24 | fp16_first_second.asm | překlad do strojového kódu s přímým povolení manipulace s typy half float | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_first_second.asm |
| 25 | fp16_add_intrinsic.c | realizace součtu dvou hodnot typu half float přes intrinsic funkci, zdrojový kód v jazyku C | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_add_intrinsic.c |
| 26 | fp16_add_intrinsic.asm | realizace součtu dvou hodnot typu half float přes intrinsic funkci, výsledek překladu | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_add_intrinsic.asm |
| 27 | fp16_arith_intrinsic.c | základní aritmetické operace s hodnotami typu half float realizované přes intrinsic, zdrojový kód v jazyku C | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_arith_intrinsic.c |
| 28 | fp16_arith_intrinsic.asm | základní aritmetické operace s hodnotami typu half float realizované přes intrinsic, výsledek překladu | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_arith_intrinsic.asm |
| 29 | fp16_comparison_intrinsic.c | porovnání operandů typu half float realizované přes intrinsic, zdrojový kód v jazyku C | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_comparison_intrinsic.c |
| 30 | fp16_comparison_intrinsic.asm | porovnání operandů typu half float realizované přes intrinsic, výsledek překladu | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_comparison_intrinsic.asm |
20. Odkazy na Internetu
- NEON Technology (stránky ARM)
https://developer.arm.com/technologies/neon - SIMD Assembly Tutorial: ARM NEON – Xiph.org
https://people.xiph.org/~tterribe/daala/neon_tutorial.pdf - Ne10
http://projectne10.github.io/Ne10/ - NEON and Floating-Point architecture
http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.den0024a/BABIGHEB.html - An Introduction to ARM NEON
http://peterdn.com/post/an-introduction-to-ARM-NEON.aspx - ARM NEON Intrinsics Reference
http://infocenter.arm.com/help/topic/com.arm.doc.ihi0073a/IHI0073A_arm_neon_intrinsics_ref.pdf - Arm Neon Intrinsics vs hand assembly
https://stackoverflow.com/questions/9828567/arm-neon-intrinsics-vs-hand-assembly - ARM NEON Optimization. An Example
http://hilbert-space.de/?p=22 - AArch64 NEON instruction format
https://developer.arm.com/docs/den0024/latest/7-aarch64-floating-point-and-neon/73-aarch64-neon-instruction-format - Vektorové procesory aneb další pokus o zvýšení výpočetního výkonu počítačů
http://www.root.cz/clanky/vektorove-procesory-aneb-dalsi-pokus-o-zvyseni-vypocetniho-vykonu-pocitacu/ - SIMD instrukce využívané v moderních mikroprocesorech řady x86
http://www.root.cz/clanky/simd-instrukce-vyuzivane-v-modernich-mikroprocesorech-rady-x86/ - SIMD instrukce v moderních mikroprocesorech řady x86 (2.část: SSE)
http://www.root.cz/clanky/simd-instrukce-v-modernich-mikroprocesorech-rady-x86–2-cast-sse/ - SIMD instrukce v moderních mikroprocesorech řady x86 (3.část: SSE2)
http://www.root.cz/clanky/simd-instrukce-v-modernich-mikroprocesorech-rady-x86–3-cast-sse2/ - Instrukce typu SIMD na mikroprocesorech RISC
http://www.root.cz/clanky/instrukce-typu-simd-na-mikroprocesorech-risc/ - Instrukce typu SIMD na mikroprocesorech RISC (2. část)
http://www.root.cz/clanky/instrukce-typu-simd-na-mikroprocesorech-risc-2-cast/ - Instrukce typu SIMD na mikroprocesorech RISC (3.část – MIPS-3D a VIS)
http://www.root.cz/clanky/instrukce-typu-simd-na-mikroprocesorech-risc-3-cast-mips-3d-a-vis/ - Trasování a ladění nativních aplikací v Linuxu
https://www.root.cz/clanky/trasovani-a-ladeni-nativnich-aplikaci-v-linuxu/ - Trasování a ladění nativních aplikací v Linuxu: použití GDB a jeho nadstaveb
https://www.root.cz/clanky/trasovani-a-ladeni-nativnich-aplikaci-v-linuxu-pouziti-gdb-a-jeho-nadstaveb/ - Debuggery a jejich nadstavby v Linuxu (3): Nemiver
http://mojefedora.cz/debuggery-a-jejich-nadstavby-v-linuxu-3-nemiver/ - Debuggery a jejich nadstavby v Linuxu (4): KDbg
http://mojefedora.cz/debuggery-a-jejich-nadstavby-v-linuxu-4-kdbg/ - Debuggery a jejich nadstavby v Linuxu (5): ladění aplikací v editorech Emacs a Vim
http://mojefedora.cz/debuggery-a-jejich-nadstavby-v-linuxu-5-ladeni-aplikaci-v-editorech-emacs-a-vim/ - Tracing (software)
https://en.wikipedia.org/wiki/Tracing_%28software%29 - cgdb: the curses debugger
https://cgdb.github.io/ - cgdb: dokumentace
https://cgdb.github.io/docs/cgdb-split.html - strace(1) – Linux man page
http://linux.die.net/man/1/strace - strace (stránka projektu na SourceForge)
https://sourceforge.net/projects/strace/ - strace (Wikipedia)
https://en.wikipedia.org/wiki/Strace - GDB – Dokumentace
http://sourceware.org/gdb/current/onlinedocs/gdb/ - GDB – Supported Languages
http://sourceware.org/gdb/current/onlinedocs/gdb/Supported-Languages.html#Supported-Languages - GNU Debugger (Wikipedia)
https://en.wikipedia.org/wiki/GNU_Debugger - The LLDB Debugger
http://lldb.llvm.org/ - Debugger (Wikipedia)
https://en.wikipedia.org/wiki/Debugger - Comparison of ARMv8-A cores
https://en.wikipedia.org/wiki/Comparison_of_ARMv8-A_cores - A64 General Instructions
http://www.keil.com/support/man/docs/armclang_asm/armclang_asm_pge1427898258836.htm - ARMv8 (AArch64) Instruction Encoding
http://kitoslab-eng.blogspot.cz/2012/10/armv8-aarch64-instruction-encoding.html - Cortex-A32 Processor
https://www.arm.com/products/processors/cortex-a/cortex-a32-processor.php - Cortex-A35 Processor
https://www.arm.com/products/processors/cortex-a/cortex-a35-processor.php - Cortex-A53 Processor
https://www.arm.com/products/processors/cortex-a/cortex-a53-processor.php - Cortex-A57 Processor
https://www.arm.com/products/processors/cortex-a/cortex-a57-processor.php - Cortex-A72 Processor
https://www.arm.com/products/processors/cortex-a/cortex-a72-processor.php - Cortex-A73 Processor
https://www.arm.com/products/processors/cortex-a/cortex-a73-processor.php - Apple A7 (SoC založen na CPU Cyclone)
https://en.wikipedia.org/wiki/Apple_A7 - System cally pro AArch64 na Linuxu
https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/unistd.h - Architectures/AArch64 (FedoraProject.org)
https://fedoraproject.org/wiki/Architectures/AArch64 - SIG pro AArch64 (CentOS)
https://wiki.centos.org/SpecialInterestGroup/AltArch/AArch64 - The ARMv8 instruction sets
http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.den0024a/ch05s01.html - A64 Instruction Set
https://developer.arm.com/products/architecture/instruction-sets/a64-instruction-set - Switching between the instruction sets
http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.den0024a/ch05s01.html - The A64 instruction set
http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.den0024a/ch05s01.html - Introduction to ARMv8 64-bit Architecture
https://quequero.org/2014/04/introduction-to-arm-architecture/ - MCU market turns to 32-bits and ARM
http://www.eetimes.com/document.asp?doc_id=1280803 - Cortex-M0 Processor (ARM Holdings)
http://www.arm.com/products/processors/cortex-m/cortex-m0.php - Cortex-M0+ Processor (ARM Holdings)
http://www.arm.com/products/processors/cortex-m/cortex-m0plus.php - ARM Processors in a Mixed Signal World
http://www.eeweb.com/blog/arm/arm-processors-in-a-mixed-signal-world - ARM Architecture (Wikipedia)
https://en.wikipedia.org/wiki/ARM_architecture - DSP for Cortex-M
https://developer.arm.com/technologies/dsp/dsp-for-cortex-m - Cortex-M processors in DSP applications? Why not?!
https://community.arm.com/processors/b/blog/posts/cortex-m-processors-in-dsp-applications-why-not - White Paper – DSP capabilities of Cortex-M4 and Cortex-M7
https://community.arm.com/processors/b/blog/posts/white-paper-dsp-capabilities-of-cortex-m4-and-cortex-m7 - Q (number format)
https://en.wikipedia.org/wiki/Q_%28number_format%29 - TriCore Architecture & Core
http://www.infineon.com/cms/en/product/microcontroller/32-bit-tricore-tm-microcontroller/tricore-tm-architecture-and-core/channel.html?channel=ff80808112ab681d0112ab6b73d40837 - TriCoreTM V1.6 Instruction Set: 32-bit Unified Processor Core
http://www.infineon.com/dgdl/tc_v131_instructionset_v138.pdf?fileId=db3a304412b407950112b409b6dd0352 - TriCore v2.2 C Compiler, Assembler, Linker Reference Manual
http://tasking.com/support/tricore/tc_reference_guide_v2.2.pdf - Infineon TriCore (Wikipedia)
https://en.wikipedia.org/wiki/Infineon_TriCore - C166®S V2 Architecture & Core
http://www.infineon.com/cms/en/product/microcontroller/16-bit-c166-microcontroller/c166-s-v2-architecture-and-core/channel.html?channel=db3a304312bef5660112c3011c7d01ae - Comparing four 32-bit soft processor cores
http://www.eetimes.com/author.asp?section_id=14&doc_id=1286116 - RISC-V Instruction Set
http://riscv.org/download.html#spec_compressed_isa - RISC-V Spike (ISA Simulator)
http://riscv.org/download.html#isa-sim - RISC-V (Wikipedia)
https://en.wikipedia.org/wiki/RISC-V - David Patterson (Wikipedia)
https://en.wikipedia.org/wiki/David_Patterson_(computer_scientist) - OpenRISC (oficiální stránky projektu)
http://openrisc.io/ - OpenRISC architecture
http://openrisc.io/architecture.html - Emulátor OpenRISC CPU v JavaScriptu
http://s-macke.github.io/jor1k/demos/main.html - OpenRISC (Wikipedia)
https://en.wikipedia.org/wiki/OpenRISC - OpenRISC – instrukce
http://sourceware.org/cgen/gen-doc/openrisc-insn.html - OpenRISC – slajdy z přednášky o projektu
https://iis.ee.ethz.ch/~gmichi/asocd/lecturenotes/Lecture6.pdf - Berkeley RISC
http://en.wikipedia.org/wiki/Berkeley_RISC - Great moments in microprocessor history
http://www.ibm.com/developerworks/library/pa-microhist.html - Microprogram-Based Processors
http://research.microsoft.com/en-us/um/people/gbell/Computer_Structures_Principles_and_Examples/csp0167.htm - Great Microprocessors of the Past and Present
http://www.cpushack.com/CPU/cpu1.html - A Brief History of Microprogramming
http://www.cs.clemson.edu/~mark/uprog.html - What is RISC?
http://www-cs-faculty.stanford.edu/~eroberts/courses/soco/projects/2000–01/risc/whatis/ - RISC vs. CISC
http://www-cs-faculty.stanford.edu/~eroberts/courses/soco/projects/2000–01/risc/risccisc/ - RISC and CISC definitions:
http://www.cpushack.com/CPU/cpuAppendA.html - FPGA
https://cs.wikipedia.org/wiki/Programovateln%C3%A9_hradlov%C3%A9_pole - The Evolution of RISC
http://www.ibm.com/developerworks/library/pa-microhist.html#sidebar1 - List of ARM instructions implementing half-precision floating-point arithmetic
https://stackoverflow.com/questions/76255632/list-of-arm-instructions-implementing-half-precision-floating-point-arithmetic - Half-Precision Floating Point (GCC)
https://gcc.gnu.org/onlinedocs/gcc/Half-Precision.html - Additional Floating Types (GCC)
https://gcc.gnu.org/onlinedocs/gcc/Floating-Types.html - Advanced SIMD (Neon)
https://en.wikipedia.org/wiki/ARM_architecture_family#Advanced_SIMD_(NEON) - GCC: ARM options
https://gcc.gnu.org/onlinedocs/gcc/ARM-Options.html - Compile ARM Neon intrinsics on macos (M3 chipsets) using clang
https://stackoverflow.com/questions/79056335/compile-arm-neon-intrinsics-on-macos-m3-chipsets-using-clang - Intrinsics – Arm Developer
https://developer.arm.com/architectures/instruction-sets/intrinsics/ - FCMEQ (register)
https://www.scs.stanford.edu/~zyedidia/arm64/fcmeq_advsimd_reg.html - FCMGE (register)
https://www.scs.stanford.edu/~zyedidia/arm64/fcmge_advsimd_reg.html - FCMGT (register)
https://www.scs.stanford.edu/~zyedidia/arm64/fcmgt_advsimd_reg.html