Obsah
1. Zpracování hodnot typu half float (fp16) na platformě AArch64: operace s vektory
2. Dostupné intrinsic funkce pro manipulaci s hodnotami typu half float
3. Podpora pro manipulaci s celými vektory v GCC
4. Vektory obsahující prvky typu half float na platformě AArch64
5. Korektní a nekorektní velikost vektorů
6. Základní aritmetické operace s vektory
7. Způsob překladu aritmetických operací s prvky vektorů do strojového kódu A64
9. Základní aritmetické operace s vektory obsahujícími 64 prvků
10. Stejná operace prováděná se všemi prvky vektorů
11. Konverze všech prvků vektoru: z typu half float na typ float
12. Konverze všech prvků vektoru: z typu float na typ half float
13. Operace Multiply–accumulate (MAC) s vektory
14. Součet všech prvků vektoru
15. Alternativní realizace součtu všech prvků vektoru
16. Explicitní výpočet skalárního součinu
17. Alternativní výpočet skalárního součinu
18. Porovnání prvků vektorů s konstantou
19. Repositář s demonstračními příklady
1. Zpracování hodnot typu half float (fp16) na platformě AArch64: operace s vektory
Jak již bylo napsáno v perexu článku, dnes navážeme na předchozí článek, ve kterém jsme se věnovali zpracování skalárů i vektorů s prvky typu half float (neboli FP16) na platformě AArch64, tj. na mikroprocesorech s instrukční sadou A64 rozšířenou o technologii NEON. Zatímco minule jsme si ukázali, jak (a zda vůbec) překladač GCC provádí automatickou „vektorizaci“ výpočtů, popř. jakým způsobem se překládají nabízené intrinsic funkce, dnes se zaměříme na sice příbuznou, ale přece jen odlišnou technologii.
Překladač GCC (a nejenom ten – podobnou funkcionalitu totiž nalezneme u prakticky všech moderních překladačů jazyka C) totiž umožňuje definici nových datových typů „vektor“ a nabízí provedení základních aritmetických (i jiných) operací s odpovídajícími si prvky vektorů, aplikaci operace nad prvky vektorů a skalárem atd. Jedná se tedy o technologii, která stojí přibližně na půli cesty mezi nízkoúrovňovými intrinsic na straně jedné a zcela automatickou vektorizací kódu na straně druhé.
2. Dostupné intrinsic funkce pro manipulaci s hodnotami typu half float
V moderních překladačích programovacího jazyka C, přesněji řečeno v těch překladačích, které podporují platformu AArch64, jsou většinou definovány intrinsic funkce (či jen intrinsic), jejichž volání z céčkovského kódu se přímo překládá na instrukce cílové platformy (bez samotného volání funkce, předávání parametrů atd.). Některé příklady jsme si ukázali minule, takže si nyní ve stručnosti připomeneme, jakým způsobem se intrinsic překládají do strojového kódu. Pochopitelně zůstaneme u těch operací, které pracují s hodnotami typu half float
#include <arm_fp16.h>
#include <stdio.h>
void fp16_arithm(_Float16 x, _Float16 y) {
_Float16 a=vaddh_f16(x, y);
_Float16 b=vsubh_f16(x, y);
_Float16 c=vmulh_f16(x, y);
_Float16 d=vdivh_f16(x, y);
// donutíme překladač, aby předchozí intrinsic skutečně vygeneroval do kódu
printf("%f %f %f %f\n", (double)a, (double)b, (double)c, (double)d);
}
Ve vygenerovaném kódu se na prvních čtyřech řádcích objevuje přímé volání instrukcí, které provádí základní aritmetické operace s hodnotami typu half float; zbytek kódu se týká volání funkce printf (ovšem je zajímavé, jak „levný“ je převod hodnot half float na typ double):
fp16_arithm:
fdiv h3, h0, h1
fmul h2, h0, h1
fsub h31, h0, h1
fadd h0, h0, h1
adrp x0, .LC0
add x0, x0, :lo12:.LC0
fcvt d2, h2
fcvt d1, h31
fcvt d0, h0
fcvt d3, h3
b printf
Podobných intrinsic existuje celá řada. V následující tabulce jsou vypsány ty intrinsic pro zpracování hodnot typu half float, které lze volat v GCC:
| Návratová hodnota | Jméno funkce | Parametry |
|---|---|---|
| float16_t | vabsh_f16 | float16_t __a |
| float16_t | vaddh_f16 | float16_t __a, float16_t __b |
| int32_t | vcvtah_s32_f16 | float16_t __a |
| uint32_t | vcvtah_u32_f16 | float16_t __a |
| float16_t | vcvth_f16_s32 | int32_t __a |
| float16_t | vcvth_f16_u32 | uint32_t __a |
| float16_t | vcvthn_f16_s32 | int32_t __a, const int __b |
| float16_t | vcvthn_f16_u32 | uint32_t __a, const int __b |
| int32_t | vcvthn_s32_f16 | float16_t __a, const int __b |
| uint32_t | vcvthn_u32_f16 | float16_t __a, const int __b |
| int32_t | vcvth_s32_f16 | float16_t __a |
| uint32_t | vcvth_u32_f16 | float16_t __a |
| int32_t | vcvtmh_s32_f16 | float16_t __a |
| uint32_t | vcvtmh_u32_f16 | float16_t __a |
| int32_t | vcvtnh_s32_f16 | float16_t __a |
| uint32_t | vcvtnh_u32_f16 | float16_t __a |
| int32_t | vcvtph_s32_f16 | float16_t __a |
| uint32_t | vcvtph_u32_f16 | float16_t __a |
| float16_t | vdivh_f16 | float16_t __a, float16_t __b |
| float16_t | vfmah_f16 | float16_t __a, float16_t __b, float16_t __c |
| float16_t | vfmsh_f16 | float16_t __a, float16_t __b, float16_t __c |
| float16_t | vmaxnmh_f16 | float16_t __a, float16_t __b |
| float16_t | vminnmh_f16 | float16_t __a, float16_t __b |
| float16_t | vmulh_f16 | float16_t __a, float16_t __b |
| float16_t | vnegh_f16 | float16_t __a |
| float16_t | vrndah_f16 | float16_t __a |
| float16_t | vrndh_f16 | float16_t __a |
| float16_t | vrndih_f16 | float16_t __a |
| float16_t | vrndmh_f16 | float16_t __a |
| float16_t | vrndnh_f16 | float16_t __a |
| float16_t | vrndph_f16 | float16_t __a |
| float16_t | vrndxh_f16 | float16_t __a |
| float16_t | vsqrth_f16 | float16_t __a |
| float16_t | vsubh_f16 | float16_t __a, float16_t __b |
3. Podpora pro manipulaci s celými vektory v GCC
Intrinsic funkce, které byly zmíněny v předchozí kapitole, mají své specifické využití a vyplatí se alespoň vědět o jejich existenci. Ovšem jak již víme z předchozího článku, některé z nich lze nahradit běžnými operátory jazyka C (nad typy half float). Zajímavější situace ovšem nastane, pokud budeme vyžadovat práci s celými vektory prvků typu half float. I to je možné (do jisté míry) zajistit, protože GCC (ale i některé další překladače) nabízí vývojářům takzvané „vektorové rozšíření“, se kterým jsme se setkali v článcích o SIMD. Vzhledem k tomu, že se jedná o ústřední téma dnešního článku, připomeneme si základní vlastnosti tohoto vektorového rozšíření.
GCC umožňuje definici nových datových struktur typu „vektor prvků X“. Definice takového datového typu obecně vypadá takto:
typedef typ_prvku jméno_typu __attribute__((vector_size(velikost_vektoru_v_bajtech)));
Podívejme se na následující demonstrační příklad, ve kterém je definován nový datový typ nazvaný v16us (jméno může být pochopitelně jakékoli). Jedná se o vektor o délce šestnácti bajtů, který obsahuje prvky typu short int, což zde konkrétně může znamenat, že se do vektoru vejde celkem osm těchto prvků za předpokladu, že sizeof(unsigned short int)==2:
typedef unsigned short int v16us __attribute__((vector_size(16)));
Velikost jednoho prvku vektoru i velikost celého vektoru získáme operátorem sizeof, což si můžeme snadno ověřit:
#include <stdio.h>
typedef unsigned short int v16us __attribute__((vector_size(16)));
int main(void)
{
printf("scalar: %ld bytes\n", sizeof(unsigned short int));
printf("vector: %ld bytes\n", sizeof(v16us));
return 0;
}
Výsledek by měl vypadat následovně:
scalar: 2 bytes vector: 16 bytes
Vyzkoušet si můžeme i další vektory o celkové délce 16 bajtů, jejichž prvky budou různých typů a tudíž i délka vektoru měřená v počtu prvků bude odlišná:
#include <stdio.h>
typedef unsigned char v16ub __attribute__((vector_size(16)));
typedef unsigned short int v16us __attribute__((vector_size(16)));
typedef unsigned int v16ui __attribute__((vector_size(16)));
typedef unsigned long int v16ul __attribute__((vector_size(16)));
int main(void)
{
printf("unsigned char: %ld bytes\n", sizeof(unsigned char));
printf("unsigned short: %ld bytes\n", sizeof(unsigned short int));
printf("unsigned int: %ld bytes\n", sizeof(unsigned int));
printf("unsigned long: %ld bytes\n", sizeof(unsigned long int));
printf("vector unsigned char: %ld bytes\n", sizeof(v16ub));
printf("vector unsigned short: %ld bytes\n", sizeof(v16us));
printf("vector unsigned int: %ld bytes\n", sizeof(v16ui));
printf("vector unsigned long: %ld bytes\n", sizeof(v16ul));
return 0;
}
Výsledek:
unsigned char: 1 bytes unsigned short: 2 bytes unsigned int: 4 bytes unsigned long: 8 bytes vector unsigned char: 16 bytes vector unsigned short: 16 bytes vector unsigned int: 16 bytes vector unsigned long: 16 bytes
Totéž platí i pro vektory s prvky se znaménkem:
#include <stdio.h>
typedef signed char v16ub __attribute__((vector_size(16)));
typedef signed short int v16us __attribute__((vector_size(16)));
typedef signed int v16ui __attribute__((vector_size(16)));
typedef signed long int v16ul __attribute__((vector_size(16)));
int main(void)
{
printf("signed char: %ld bytes\n", sizeof(signed char));
printf("signed short: %ld bytes\n", sizeof(signed short int));
printf("signed int: %ld bytes\n", sizeof(signed int));
printf("signed long: %ld bytes\n", sizeof(signed long int));
printf("vector signed char: %ld bytes\n", sizeof(v16ub));
printf("vector signed short: %ld bytes\n", sizeof(v16us));
printf("vector signed int: %ld bytes\n", sizeof(v16ui));
printf("vector signed long: %ld bytes\n", sizeof(v16ul));
return 0;
}
Výsledky, které by se měly zobrazit na standardním výstupu po překladu a spuštění tohoto demonstračního příkladu:
signed char: 1 bytes signed short: 2 bytes signed int: 4 bytes signed long: 8 bytes vector signed char: 16 bytes vector signed short: 16 bytes vector signed int: 16 bytes vector signed long: 16 bytes
4. Vektory obsahující prvky typu half float na platformě AArch64
V dnešním článku nás nejvíce zajímá způsob práce s vektory, jejichž prvky jsou typu half float. Pokusme se tedy nyní definovat typ, který takový vektor popíše. Připomeňme si, že definice typu vektoru vypadá následovně:
typedef typ_prvku jméno_typu __attribute__((vector_size(velikost_vektoru_v_bajtech)));
Konkrétně vektor s osmi prvky typu half float (pro sizeof half_float==2) tedy bude vypadat následovně:
typedef _Float16 float16x8 __attribute__((vector_size(16)));
Opět si pochopitelně můžeme ověřit velikost prvku (musí být dva bajty) i velikost celého vektoru:
#include <stdio.h>
typedef _Float16 float16x8 __attribute__((vector_size(16)));
int main(void)
{
printf("scalar: %ld bytes\n", sizeof(_Float16));
printf("vector: %ld bytes\n", sizeof(float16x8));
return 0;
}
Po překladu a spuštění tohoto příkladu by se mělo vypsat:
scalar: 2 bytes vector: 16 bytes
5. Korektní a nekorektní velikost vektorů
V předchozím demonstračním příkladu byla velikost vektoru nastavena na šestnáct bajtů. Jedná se o korektní velikost, a to ze dvou důvodů:
- Tato velikost je celočíselným násobkem velikosti jednoho prvku (v bajtech). Konkrétně v tomto případě se jedná o celočíselný násobek dvojky, protože half float v paměti obsadí přesně dva bajty.
- Současně je tato velikost celočíselnou mocninou dvojky, což je obecný požadavek GCC, který nezávisí na typu prvků vektoru.
Ostatně můžeme si sami otestovat, jak bude překladač reagovat na pokus o definici vektoru s nekorektní velikostí. Nejprve se pokusíme o definici vektoru, jehož velikost není celočíselným násobkem sizeof(_Float16):
#include <stdio.h>
typedef _Float16 float16x8 __attribute__((vector_size(17)));
int main(void)
{
printf("scalar: %ld bytes\n", sizeof(_Float16));
printf("vector: %ld bytes\n", sizeof(float16x8));
return 0;
}
Překladač tento problém snadno odhalí:
$ gcc -Wall fp16_vector_incorrect_size_1.c
fp16_vector_incorrect_size_1.c:3:1: error: vector size not an integral multiple of component size
3 | typedef _Float16 float16x8 __attribute__((vector_size(17)));
| ^~~~~~~
Ve druhém demonstračním příkladu sice bude velikost vektoru dělitelná dvojkou, ale nebude celočíselnou mocninou dvojky:
#include <stdio.h>
typedef _Float16 float16x8 __attribute__((vector_size(20)));
int main(void)
{
printf("scalar: %ld bytes\n", sizeof(_Float16));
printf("vector: %ld bytes\n", sizeof(float16x8));
return 0;
}
I tento problém bude překladačem céčka odhalen:
$ gcc -Wall fp16_vector_incorrect_size_2.c
fp16_vector_incorrect_size_2.c:3:1: error: number of vector components 10 not a power of two
3 | typedef _Float16 float16x8 __attribute__((vector_size(20)));
| ^~~~~~~
6. Základní aritmetické operace s vektory
Překladač GCC umožňuje aplikaci základních aritmetických (a popř. i dalších) operátorů nad dvojicí vektorů stejné délky a stejného typu. Tyto operace budou provedeny prvek po prvku, tj. operace se provede pro odpovídající si prvky vektorů s indexem 0, dále s odpovídajícími si prvky vektorů s indexem 1 atd. Důležitý je fakt, že pokud instrukční sada umožňuje provedení těchto operací „paralelně“, tedy s využitím vhodné vektorové instrukce či instrukcí, budou tyto instrukce skutečně použity.
Pro vektory s prvky typu half float jsme omezeni na čtyři základní aritmetické operace a taktéž na porovnání vektorů prvek po prvku. Vyzkoušejme si aritmetickou operaci součtu:
typedef _Float16 float16x8 __attribute__((vector_size(16)));
float16x8 add(float16x8 x, float16x8 y)
{
return x+y;
}
Na platformě AArch64 s instrukcemi pro fp16 bude tato funkce (která interně provádí osm součtů) přeložena do pouhých dvou instrukcí:
add:
fadd v0.8h, v0.8h, v1.8h
ret
typedef _Float16 float16x8 __attribute__((vector_size(16)));
typedef float float32x8 __attribute__((vector_size(32)));
float32x8 add(float16x8 x, float32x8 y)
{
return x+y;
}
Chyba nalezená překladačem:
fp16_vector_add_3.c:6:13: error: invalid operands to binary + (have ‘float16x8’ {aka ‘__vector(8) _Float16’} and ‘float32x8’ {aka ‘__vector(8) float’})
6 | return x+y;
| ^
Vektory odlišné délky:
typedef _Float16 float16x8 __attribute__((vector_size(16)));
typedef _Float16 float16x16 __attribute__((vector_size(32)));
float32x8 add(float16x8 x, float16x16 y)
{
return x+y;
}
Chyba nalezená překladačem:
fp16_vector_add_4.c:6:13: error: invalid operands to binary + (have ‘float16x8’ {aka ‘__vector(8) _Float16’} and ‘float16x16’ {aka ‘__vector(16) _Float16’})
6 | return x+y;
| ^
7. Způsob překladu aritmetických operací s prvky vektorů do strojového kódu A64
S celými vektory, které mají stejnou délku a současně i shodný typ prvků half float, je pochopitelně možné provádět i ostatní tři základní aritmetické operace, což je ukázáno na dalším demonstračním příkladu. Připomeňme si, že tyto operace jsou prováděny s odpovídajícími si typy prvků, tj. například operátor * neznačí ani skalární ani vektorový součin, ale skutečně pouze součiny prvků s indexem 0, další součin prvků s indexem 1 atd.:
typedef _Float16 float16x8 __attribute__((vector_size(16)));
float16x8 add(float16x8 x, float16x8 y)
{
return x+y;
}
float16x8 sub(float16x8 x, float16x8 y)
{
return x-y;
}
float16x8 mul(float16x8 x, float16x8 y)
{
return x*y;
}
float16x8 div(float16x8 x, float16x8 y)
{
return x/y;
}
Způsob realizace překladu tohoto příkladu do strojového kódu ukazuje, že se skutečně provádí „vektorizované“ aritmetické operace, tj. vlastně osm aritmetických operací paralelně:
add:
fadd v0.8h, v0.8h, v1.8h
ret
sub:
fsub v0.8h, v0.8h, v1.8h
ret
mul:
fmul v0.8h, v0.8h, v1.8h
ret
div:
fdiv v0.8h, v0.8h, v1.8h
ret
8. Zpracování delších vektorů
Prozatím jsme si otestovali, jakým způsobem se provádí zvolené aritmetické operace s vektory o velikosti 16 bajtů (128 bitů). Vzhledem k tomu, že prvek typu half float obsazuje v paměti 2 bajty (16 bitů), mohou tyto vektory obsahovat osm prvků. Šířka 128 bitů není pochopitelně zvolena náhodně, protože v instrukční sadě NEON na AArch64 mají „vektorové“ registry právě tuto bitovou šířku a tudíž byly (alespoň prozatím) aritmetické operace realizovány jedinou strojovou instrukcí.
Nic nám ovšem nebrání v použití delších vektorů. Musíme pouze dodržet zásadu, že délka (měřená v bajtech) musí být celočíselnou mocninou dvojky. Snadno tedy provedeme součet vektorů s šestnácti prvky typu half float:
typedef _Float16 float16x16 __attribute__((vector_size(32)));
float16x16 add(float16x16 x, float16x16 y)
{
return x+y;
}
Z výsledků překladu tohoto příkladu do strojového kódu je patrné, že se provedl součet pro první polovinu vektorů následovaný součtem pro polovinu druhou. Ovšem současně se změnil i způsob předání parametrů – nyní se parametry předávají referencí a totéž platí pro návratovou hodnotu (to je zajímavé, protože lze teoreticky pro předávání parametrů použít registry v0 až v7):
add:
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, [x8]
ret
9. Základní aritmetické operace s vektory obsahujícími 64 prvků
Pokusme se o další zvětšení vektorů, se kterými se budou provádět aritmetické operace. Konkrétně vektory rozšíříme na 128 bajtů, což pro typy half float znamená, že vektory budou obsahovat 64 prvků. A právě s těmito vektory provedeme všechny čtyři základní aritmetické operace:
typedef _Float16 float16x64 __attribute__((vector_size(128)));
float16x64 add(float16x64 x, float16x64 y)
{
return x+y;
}
float16x64 sub(float16x64 x, float16x64 y)
{
return x-y;
}
float16x64 mul(float16x64 x, float16x64 y)
{
return x*y;
}
float16x64 div(float16x64 x, float16x64 y)
{
return x/y;
}
Z výsledků překladu do strojového kódu je patrné, že se vždy načte dvojice částí registrů (osm prvků) a provede se s nimi požadovaná operace. Následně se výsledek uloží. Tato trojice instrukcí se opakuje celkem 8× a částečně se překrývá s ostatními sedmi opakováními stejné sekvence instrukcí:
add:
ldp q17, q19, [x0]
ldp q16, q18, [x1]
ldp q21, q23, [x0, 32]
ldp q20, q22, [x1, 32]
ldp q25, q27, [x0, 64]
ldp q24, q26, [x1, 64]
ldp q29, q31, [x0, 96]
ldp q28, q30, [x1, 96]
fadd v16.8h, v17.8h, v16.8h
fadd v18.8h, v19.8h, v18.8h
fadd v20.8h, v21.8h, v20.8h
fadd v22.8h, v23.8h, v22.8h
fadd v24.8h, v25.8h, v24.8h
fadd v26.8h, v27.8h, v26.8h
stp q16, q18, [x8]
fadd v28.8h, v29.8h, v28.8h
stp q20, q22, [x8, 32]
fadd v30.8h, v31.8h, v30.8h
stp q24, q26, [x8, 64]
stp q28, q30, [x8, 96]
ret
sub:
ldp q17, q19, [x0]
ldp q16, q18, [x1]
ldp q21, q23, [x0, 32]
ldp q20, q22, [x1, 32]
ldp q25, q27, [x0, 64]
ldp q24, q26, [x1, 64]
ldp q29, q31, [x0, 96]
ldp q28, q30, [x1, 96]
fsub v16.8h, v17.8h, v16.8h
fsub v18.8h, v19.8h, v18.8h
fsub v20.8h, v21.8h, v20.8h
fsub v22.8h, v23.8h, v22.8h
fsub v24.8h, v25.8h, v24.8h
fsub v26.8h, v27.8h, v26.8h
stp q16, q18, [x8]
fsub v28.8h, v29.8h, v28.8h
stp q20, q22, [x8, 32]
fsub v30.8h, v31.8h, v30.8h
stp q24, q26, [x8, 64]
stp q28, q30, [x8, 96]
ret
mul:
ldp q17, q19, [x0]
ldp q16, q18, [x1]
ldp q21, q23, [x0, 32]
ldp q20, q22, [x1, 32]
ldp q25, q27, [x0, 64]
ldp q24, q26, [x1, 64]
ldp q29, q31, [x0, 96]
ldp q28, q30, [x1, 96]
fmul v16.8h, v17.8h, v16.8h
fmul v18.8h, v19.8h, v18.8h
fmul v20.8h, v21.8h, v20.8h
fmul v22.8h, v23.8h, v22.8h
fmul v24.8h, v25.8h, v24.8h
fmul v26.8h, v27.8h, v26.8h
stp q16, q18, [x8]
fmul v28.8h, v29.8h, v28.8h
stp q20, q22, [x8, 32]
fmul v30.8h, v31.8h, v30.8h
stp q24, q26, [x8, 64]
stp q28, q30, [x8, 96]
ret
div:
ldp q17, q19, [x0]
ldp q16, q18, [x1]
ldp q21, q23, [x0, 32]
ldp q20, q22, [x1, 32]
ldp q25, q27, [x0, 64]
ldp q24, q26, [x1, 64]
ldp q29, q31, [x0, 96]
ldp q28, q30, [x1, 96]
fdiv v16.8h, v17.8h, v16.8h
fdiv v18.8h, v19.8h, v18.8h
fdiv v20.8h, v21.8h, v20.8h
fdiv v22.8h, v23.8h, v22.8h
fdiv v24.8h, v25.8h, v24.8h
fdiv v26.8h, v27.8h, v26.8h
stp q16, q18, [x8]
fdiv v28.8h, v29.8h, v28.8h
fdiv v30.8h, v31.8h, v30.8h
stp q20, q22, [x8, 32]
stp q24, q26, [x8, 64]
stp q28, q30, [x8, 96]
ret
10. Stejná operace prováděná se všemi prvky vektorů
V dalším ukázkovém příkladu je realizována funkce, která ke všem prvkům pole o známé délce přičítá konstantu předanou formou argumentu. Konstanta je přitom stejného datového typu, jaký mají všechny prvky vektoru. 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 – pouze zde důsledně využíváme typ „vektor“:
typedef _Float16 float16x8 __attribute__((vector_size(16)));
float16x8 add_delta(float16x8 x, _Float16 delta)
{
return x+delta;
}
Z výsledku překladu je patrné, že opět došlo k „vektorizaci“ operace součtu. Ovšem nejprve bylo nutné hodnotu parametru delta taktéž převést na vektor instrukcí dup:
add_delta:
dup v1.8h, v1.h[0]
fadd v0.8h, v1.8h, v0.8h
ret
11. Konverze všech prvků vektoru: z typu half float na typ float
Některé operace nad celými vektory je nutné provádět s využitím vestavěných funkcí nabízených překladačem GCC. Jedna z těchto funkcí se jmenuje __builtin_convertvector. Jedná se o „generickou“ funkci, která dokáže převést prvky vektoru z původního datového typu na nový datový typ, který je do funkce předán jako druhý parametr (prvním parametrem je převáděný vektor). Už jen z tohoto popisu je zřejmé, že se nemůže jednat o běžnou céčkovskou funkci, ale o specializovaný intrinsic.
Podívejme se nejprve na převod všech prvků vektoru typu half float do nového vektoru se stejným počtem prvků. Nyní ovšem budou mít nové prvky typ float, což mj. znamená, že cílový vektor v operační paměti zabere dvojnásobek místa (8×4 bajty namísto původních 8×2 bajtů):
typedef _Float16 float16x8 __attribute__((vector_size(16)));
typedef float float32x8 __attribute__((vector_size(32)));
float32x8 to_float32x8(float16x8 x)
{
return __builtin_convertvector(x, float32x8);
}
Překlad do strojového kódu je v tomto případě založen na instrukcích fcvtl a fcvtl2. Samotná konverze je tedy rozdělena na dvě instrukce, protože výsledkem musí být vždy maximálně 128bitová hodnota:
to_float32x8:
fcvtl v31.4s, v0.4h
fcvtl2 v0.4s, v0.8h
stp q31, q0, [x8]
ret
12. Konverze všech prvků vektoru: z typu float na typ half float
Pochopitelně je podporována i konverze opačným směrem, tj. lze zkonvertovat vektor s prvky typu float na vektor s prvky typu half float. Céčkový kód se až na rozdílné typy parametrů neliší od kódu předchozího:
typedef _Float16 float16x8 __attribute__((vector_size(16)));
typedef float float32x8 __attribute__((vector_size(32)));
float16x8 from_float32x8(float32x8 x)
{
return __builtin_convertvector(x, float16x8);
}
Ovšem ve strojovém kódu je situace v porovnání s předchozím příkladem mnohem složitější. Nyní se prvky konvertují postupně a nikoli celý vektor jako celek. Navíc se výsledný vektor postupně skládá (resp. přesněji řečeno se do něj vkládají prvky) instrukcí INS s následným prokladem prvků instrukcí ZIP. Nejedná se ani o hezký ani o závratně rychlý kód:
from_float32x8:
ldp s28, s29, [x0, 8]
ldp s31, s30, [x0]
fcvt h24, s28
fcvt h25, s29
ldp s26, s27, [x0, 16]
fcvt h31, s31
fcvt h30, s30
ldp s28, s29, [x0, 24]
fcvt h26, s26
fcvt h27, s27
ins v31.h[1], v24.h[0]
ins v30.h[1], v25.h[0]
fcvt h28, s28
fcvt h29, s29
ins v31.h[2], v26.h[0]
ins v30.h[2], v27.h[0]
ins v31.h[3], v28.h[0]
ins v30.h[3], v29.h[0]
zip1 v0.8h, v31.8h, v30.8h
ret
13. Operace Multiply–accumulate (MAC) s vektory
Při zpracování signálů ale i v dalších oblastech se poměrně často setkáme s operací typu multiply-accumulate, tj. výpočtem a=a+b×c. Tato operace je na platformě AArch64 široce podporována, a to jak pro výpočty se skaláry, tak i pro výpočty s vektory. Ostatně si to můžeme vyzkoušet sami. Pokusíme se o výše uvedený výpočet a=a+b×c, ovšem s tím, že hodnoty a, b a c nejsou skaláry, ale vektory s osmi prvky typu half float:
typedef _Float16 float16x8 __attribute__((vector_size(16)));
float16x8 mac(float16x8 a, float16x8 b, float16x8 c)
{
return a+b*c;
}
Překladač jazyka C (GCC) v tomto případě výpočet rozezná a namísto osmi součinů a osmi součtů zavolá jedinou instrukci FMLA:
mac:
fmla v0.8h, v1.8h, v2.8h
ret
14. Součet všech prvků vektoru
Prozatím jsme si vyzkoušeli dva typy operací prováděných s vektory obsahujícími prvky typu half float:
- Operace prováděné s odpovídajícími si prvky dvou vektorů (součet, rozdíl, součin, podíl, později i porovnání)
- Operace prováděné se všemi prvky a jedinou skalární hodnotou, která je k prvkům přičítána, odečítána atd.
Jednalo se tedy (prozatím) o operace, které by bylo možné naznačit takto:
vektor × vektor → vektor vektor × skalár → vektor
Nyní k těmto množinám přidáme poslední typ operací:
vektor × vektor → skalár
nebo jen:
vektor → skalár
Mezi nejčastější operaci, která akceptuje na vstupu vektor a výsledkem je skalární hodnota, je součet všech prvků vektoru. Ten můžeme v jazyku C zapsat mnoha různými způsoby. Nejdříve se podívejme na možná poněkud naivní, ale plně funkční způsob – použití počítané programové smyčky:
typedef _Float16 float16x8 __attribute__((vector_size(16)));
_Float16 sum1(float16x8 x)
{
_Float16 sum = 0.0;
int i;
for (i=0; i<8; i++) {
sum += x[i];
}
return sum;
}
V tomto případě je překladač GCC relativně úspěšný a zapsanou programovou smyčku rozbalí. Výsledek sice není ideální, ale stále bude rychlejší, než explicitní smyčka, která vždy z vektoru přečte jeden prvek a provede součet:
sum1:
movi v31.4s, 0
ext v1.16b, v0.16b, v31.16b, #8
fadd v0.8h, v1.8h, v0.8h
ext v30.16b, v0.16b, v31.16b, #4
fadd v0.8h, v30.8h, v0.8h
ext v31.16b, v0.16b, v31.16b, #2
fadd v0.8h, v31.8h, v0.8h
ret
15. Alternativní realizace součtu všech prvků vektoru
Zajímavé bude zjistit, co se stane ve chvíli, kdy se budeme snažit překladači „pomoci“ a výpočet sumy rozepíšeme bez použití počítané programové smyčky. Můžeme to realizovat následujícím způsobem:
typedef _Float16 float16x8 __attribute__((vector_size(16)));
_Float16 sum2(float16x8 x)
{
return x[0] + x[1] + x[2] + x[3] + x[4] + x[5] + x[6] + x[7];
}
Výsledkem bude v tomto případě zcela odlišný strojový kód, ve kterém se explicitně provede všech sedm operací součtu a současně se využije celá řada dalších registrů technologie NEON:
sum2:
dup h31, v0.h[1]
dup h25, v0.h[2]
dup h26, v0.h[3]
dup h27, v0.h[4]
fadd h31, h31, h0
dup h28, v0.h[5]
dup h29, v0.h[6]
dup h30, v0.h[7]
fadd h31, h31, h25
fadd h31, h31, h26
fadd h31, h31, h27
fadd h31, h31, h28
fadd h31, h31, h29
fadd h0, h31, h30
ret
16. Explicitní výpočet skalárního součinu
Dalším velmi často prováděným výpočtem je realizace skalárního součinu (dot product). V dnešním článku si ukážeme nepříliš dobré výsledky překladu realizace tohoto výpočtu do strojového kódu. Nejdříve naprogramujeme výpočet skalárního součinu přesně tak, jak je definován. Vstupem bude dvojice osmiprvkových vektorů s prvky typu half float, výsledkem pak jediná hodnota typu half float:
typedef _Float16 float16x8 __attribute__((vector_size(16)));
_Float16 dot_product(float16x8 a, float16x8 b) {
int i;
_Float16 result = 0.0;
for (i=0; i<sizeof(float16x8)/sizeof(_Float16); i++) {
result += a[i] * b[i];
}
return result;
}
Překladač korektně celou smyčku rozbalí a dokonce postupné „skalární“ násobení převede na násobení prvků dvou vektorů. Následně provede postupný součet mezivýsledků:
dot_product:
movi v31.4s, 0
fmul v0.8h, v0.8h, v1.8h
ext v2.16b, v0.16b, v31.16b, #8
fadd v0.8h, v2.8h, v0.8h
ext v1.16b, v0.16b, v31.16b, #4
fadd v0.8h, v1.8h, v0.8h
ext v31.16b, v0.16b, v31.16b, #2
fadd v0.8h, v31.8h, v0.8h
ret
Nepatrně lepšího výsledku bude možné dosáhnout použitím intrinsic, což si ukážeme v části věnované optimalizacím.
Ještě si ověřme, jak se přeloží stejná operace, ovšem s delšími vektory:
typedef _Float16 float16x64 __attribute__((vector_size(128)));
_Float16 dot_product(float16x64 a, float16x64 b) {
int i;
_Float16 result = 0.0;
for (i=0; i<sizeof(float16x64)/sizeof(_Float16); i++) {
result += a[i] * b[i];
}
return result;
}
Nyní je způsob překladu odlišný – vidět zde můžeme instrukci fmla pro provedení operace multiply-accumulate, mezivýsledky se nakonec sečtou běžným součtem realizovaným instrukcí fadd:
dot_product:
ldp q22, q23, [x0]
ldp q21, q30, [x1]
ldp q19, q20, [x0, 32]
ldp q18, q1, [x1, 32]
fmul v30.8h, v23.8h, v30.8h
ldp q17, q7, [x0, 64]
fmul v1.8h, v20.8h, v1.8h
ldp q16, q6, [x1, 64]
fmla v30.8h, v22.8h, v21.8h
fmla v1.8h, v19.8h, v18.8h
ldp q4, q3, [x1, 96]
ldp q5, q2, [x0, 96]
fmla v30.8h, v17.8h, v16.8h
fmla v1.8h, v7.8h, v6.8h
movi v0.4s, 0
fmla v30.8h, v5.8h, v4.8h
fmla v1.8h, v3.8h, v2.8h
fadd v1.8h, v1.8h, v30.8h
ext v31.16b, v1.16b, v0.16b, #8
fadd v31.8h, v31.8h, v1.8h
ext v29.16b, v31.16b, v0.16b, #4
fadd v29.8h, v29.8h, v31.8h
ext v0.16b, v29.16b, v0.16b, #2
fadd v0.8h, v0.8h, v29.8h
ret
17. Alternativní výpočet skalárního součinu
Pro zajímavost se opět překladači pokusíme pomoci rozepsáním počítané smyčky na sumu součinů:
typedef _Float16 float16x8 __attribute__((vector_size(16)));
_Float16 dot_product(float16x8 a, float16x8 b) {
return a[0]*b[0] + a[1]*b[1] + a[2]*b[2] + a[3]*b[3] + a[4]*b[4] + a[5]*b[5] + a[6]*b[6] + a[7]*b[7];
}
Výsledek bude odlišný, ovšem zajímavé je, že se použije instrukce fmadd, což je varianta operace multiply-accumulate, jejíž vektorovou variantu jsme si již popsali:
dot_product:
dup h30, v1.h[1]
dup h31, v0.h[1]
dup h19, v0.h[2]
dup h20, v1.h[2]
dup h21, v0.h[3]
dup h22, v1.h[3]
fmul h31, h31, h30
dup h23, v0.h[4]
fmadd h31, h1, h0, h31
dup h24, v1.h[4]
dup h25, v0.h[5]
dup h26, v1.h[5]
fmadd h31, h19, h20, h31
dup h27, v0.h[6]
dup h28, v1.h[6]
dup h29, v0.h[7]
fmadd h31, h21, h22, h31
dup h30, v1.h[7]
fmadd h31, h23, h24, h31
fmadd h31, h25, h26, h31
fmadd h31, h27, h28, h31
fmadd h0, h29, h30, h31
ret
18. Porovnání prvků vektorů s konstantou
Poslední operací s celými vektory, kterou si v dnešním článku ukážeme, je porovnání všech osmi prvků vektoru (s prvky typu half float) s konstantou 0.0. Výsledkem bude taktéž osm prvků, ovšem typu short int (booleovské hodnoty 0 a 1). V jazyku C lze tedy realizovat osm porovnání s nulou a výsledkem bude osm pravdivostních hodnot:
typedef _Float16 float16x8 __attribute__((vector_size(16)));
typedef short int int16x8 __attribute__((vector_size(16)));
int16x8 zeros(float16x8 x)
{
return x==0;
}
Mohlo by se zdát, že se v tomto případě zavolá nějaká instrukce určená pro porovnání vektorů, ovšem ve vygenerovaném strojovém kódu je patrné, že se namísto toho osmkrát volá instrukce fcmeq a navíc byly hodnoty před porovnáním převedeny na typ single osmi instrukcemi fcvt. Výsledek tedy nebude tak rychlý, jak by to mohlo při pohledu na céčkovský zdrojový kód vypadat:
zeros:
dup h30, v0.h[1]
dup h24, v0.h[3]
dup h25, v0.h[2]
dup h26, v0.h[5]
dup h27, v0.h[4]
fcvt s29, h0
fcvt s30, h30
fcvt s24, h24
fcvt s25, h25
dup h28, v0.h[7]
dup h31, v0.h[6]
fcvt s26, h26
fcmeq s0, s29, 0
fcmeq s24, s24, 0
fcmeq s25, s25, 0
fcvt s27, h27
fcmeq s30, s30, 0
fcvt s28, h28
fcvt s31, h31
fcmeq s26, s26, 0
ins v0.h[1], v25.h[0]
fcmeq s27, s27, 0
ins v30.h[1], v24.h[0]
fcmeq s28, s28, 0
fcmeq s31, s31, 0
ins v30.h[2], v26.h[0]
ins v0.h[2], v27.h[0]
ins v30.h[3], v28.h[0]
ins v0.h[3], v31.h[0]
zip1 v0.8h, v0.8h, v30.8h
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_vector_size.c | získání a tisk velikosti prvku typu half float i vektoru s těmito prvky | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_vector_size.c |
| 24 | fp16_vector_incorrect_size1.c | pokus o konstrukci vektoru s neplatnou velikostí | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_vector_incorrect_size1.c |
| 25 | fp16_vector_incorrect_size2.c | pokus o konstrukci vektoru s neplatnou velikostí | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_vector_incorrect_size2.c |
| 26 | fp16_vector_add1.c | součet odpovídajících si prvků vektorů typu half float, vektory mají délku osmi prvků | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_vector_add1.c |
| 27 | fp16_vector_add1.asm | překlad do strojového kódu s přímým povolením práce s typy half float | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_vector_add1.asm |
| 28 | fp16_vector_add2.c | součet odpovídajících si prvků vektorů typu half float, vektory mají délku šestnácti prvků | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_vector_add2.c |
| 29 | fp16_vector_add2.asm | překlad do strojového kódu s přímým povolením práce s typy half float | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_vector_add2.asm |
| 30 | fp16_vector_add3.c | pokus o součet vektorů, které mají stejný počet prvků, ovšem odlišného typu | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_vector_add3.c |
| 31 | fp16_vector_add4.c | pokus o součet vektorů, které mají prvky stejného typu, ovšem odlišnou délku | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_vector_add4.c |
| 32 | fp16_vector_arith1.c | čtyři základní aritmetické operace s vektory typu half float s osmi prvky | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_vector_arith1.c |
| 33 | fp16_vector_arith1.asm | překlad do strojového kódu s přímým povolením práce s typy half float | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_vector_arith1.asm |
| 34 | fp16_vector_arith2.c | čtyři základní aritmetické operace s vektory typu half float se šestnácti prvky | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_vector_arith2.c |
| 35 | fp16_vector_arith2.asm | překlad do strojového kódu s přímým povolením práce s typy half float | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_vector_arith2.asm |
| 36 | fp16_vector_convert1.c | konverze všech prvků vektorů mezi typy float a half float | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_vector_convert1.c |
| 37 | fp16_vector_convert1.asm | překlad do strojového kódu s přímým povolením práce s typy half float | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_vector_convert1.asm |
| 38 | fp16_vector_convert2.c | konverze všech prvků vektorů mezi typy float a half float | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_vector_convert2.c |
| 39 | fp16_vector_convert2.asm | překlad do strojového kódu s přímým povolením práce s typy half float | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_vector_convert2.asm |
| 39 | fp16_vector_delta.c | přičtení konstanty ke všem prvkům vektoru | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_vector_delta.c |
| 40 | fp16_vector_delta.asm | překlad do strojového kódu s přímým povolením práce s typy half float | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_vector_delta.asm |
| 41 | fp16_vector_mac.c | operace typu Multiply-accumulate s vektory | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_vector_mac.c |
| 42 | fp16_vector_mac.asm | překlad do strojového kódu s přímým povolením práce s typy half float | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_vector_mac.asm |
| 43 | fp16_vector_sum.c | součet všech prvků vektoru | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_vector_sum.c |
| 44 | fp16_vector_sum.asm | překlad do strojového kódu s přímým povolením práce s typy half float | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_vector_sum.asm |
| 45 | fp16_dot_product.c | skalární součin dvou vektorů | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_dot_product.c |
| 46 | fp16_dot_product.asm | překlad do strojového kódu s přímým povolením práce s typy half float | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_dot_product.asm |
| 47 | fp16_vector_zeros.c | porovnání všech prvků vektoru s nulovou hodnotou | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_vector_zeros.c |
| 48 | fp16_vector_zeros.asm | překlad do strojového kódu s přímým povolením práce s typy half float | https://github.com/tisnik/8bit-fame/blob/master/fp/fp16_vector_zeros.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 - A whirlwind tour of AArch64 vector instructions (NEON)
https://www.corsix.org/content/whirlwind-tour-aarch64-vector-instructions - How is arm_neon.h generated or maintained?
https://stackoverflow.com/questions/71422209/how-is-arm-neon-h-generated-or-maintained - Arm Neon programming quick reference
https://community.arm.com/arm-community-blogs/b/operating-systems-blog/posts/arm-neon-programming-quick-reference - SimSIMD
https://github.com/ashvardanian/simsimd - SIMD-dot-products-ARM-NEON-RISC-V
https://github.com/crissmath/SIMD-dot-products-ARM-NEON-RISC-V - Procedure Call Standard for the Arm® 64-bit Architecture (AArch64)
https://github.com/ARM-software/abi-aa/blob/main/aapcs64/aapcs64.rst#simd-and-floating-point-registers