Dlouho očekávaná podpora SIMD operací v programovacím jazyku Go

1. 9. 2026
Doba čtení: 41 minut

Sdílet

Autor: Go lang
Popíšeme si, jak je v programovacím jazyku Go implementována podpora pro SIMD (vektorové) operace. Do Go 1.26 totiž byly přidány experimentální balíčky s podporou SIMD.

V dnešním článku si popíšeme, jakým způsobem jsou v nových verzích Go (1.26 a především 1.27) podporovány SIMD operace (někdy nepřesně nazývané vektorové instrukce). Přitom SIMD/vektorové instrukce v současnosti patří ke standardní výbavě prakticky všech variant moderních mikroprocesorů – jedná se totiž o jednu z technologií umožňujících zvýšení jejich výpočetního výkonu. Připomeňme si, že z hlediska dosahovaného výpočetního výkonu leží na samém „výkonnostním dně“ klasické mikroprocesory s architekturou CISC, které vykonávají všechny instrukce postupně a dokončení jedné instrukce může v závislosti na jejich složitosti trvat i několik desítek strojových taktů.

Co se dozvíte v článku
  1. Od skalárních instrukcí k SIMD operacím
  2. SIMD operace na architektuře x86–64
  3. Rozšíření původní instrukční sady x86 o SIMD operace
  4. A co těmito rozšířeními vlastně získáme?
  5. Podpora SIMD operací v programovacím jazyku Go
  6. API pro SIMD operace: explicitní přístup bez využití autovektorizace
  7. Praktická část: instalace Go 1.27 a nastavení vývojových prostředí
  8. Základní základy, na nichž je postaven experimentální balíček simd/archsimd
  9. Zjištění, zda je podporováno konkrétní rozšíření instrukční sady o SIMD operace
  10. Vektorová operace součtu
  11. Součet provedený s vektory s větším počtem prvků
  12. Základní unární operace s prvky vektorů
  13. Další binární operace s prvky vektorů
  14. Různé způsoby zaokrouhlení hodnot prvků vektorů
  15. Operace typu broadcast
  16. Vytvoření masky, základní práce s maskami
  17. Repositář s demonstračními příklady
  18. Seznam všech předchozích částí seriálu a článků o SIMD instrukcích
  19. Odkazy na Internetu

Předností těchto procesorů může být poměrně velká informační hustota instrukční sady (například i díky tomu, že operandy některých instrukcí jsou zadány implicitně), což mj. znamená, že se procesory tohoto typu po poměrně dlouhou dobu obešly bez nutnosti využití drahých vyrovnávacích pamětí první a druhé úrovně (L1 cache, L2 cache). Klasické procesory s architekturou CISC byly založeny na mikroprogramovém řadiči vybaveném pamětí mikroinstrukcí a teprve později začaly být tyto procesory doplňovány technologiemi získanými z jiných architektur – instrukční pipeline, prediktorem skoků, vektorovými instrukcemi (což byly oblasti klasických RISCových architektur) atd.

Od skalárních instrukcí k SIMD operacím

Výpočetní výkon mikroprocesorů se podařilo poměrně výrazným způsobem zvýšit u architektury RISC zavedením instrukční pipeline. Provedení jedné instrukce sice stále trvalo větší počet strojových cyklů (v některých případech možná i větší počet cyklů, než při použití CISC), ovšem díky rozfázování operací v instrukční pipeline bylo umožněno překrývání většího množství instrukcí, a to bez nutnosti zavádění skutečné paralelizace (která vede k velkému nárůstu složitosti a tím i ceny čipu). Spolu se zavedením mikroprocesorů RISC do praxe se skutečně stalo, že reálný i špičkový výpočetní výkon mikroprocesorů vzrostl, ale relativně brzy bylo nutné k těmto čipům přidat vyrovnávací paměti (cache), jelikož rychlost procesorů (přesněji řečeno frekvence hodinového signálu) rostla mnohem rychleji, než vybavovací doba pamětí. Tento rozpor mezi rychlostmi obou nejdůležitějších součástí moderních počítačů ostatně trvá dodnes.

Pro další zvýšení výpočetního výkonu však bylo nutné použít další technologie, například instrukční sadu VLIW, která však – opět – měla velké nároky na rychlost pamětí. Podobně jako u procesorů RISC, i u VLIW bylo pro zmírnění požadavků na rychlost pamětí možné použít Harvardskou architekturu, tj. oddělení paměti programu od paměti dat (programová paměť navíc mohla mít větší šířku datové sběrnice odpovídající šířce instrukčních slov). Na druhou stranu však VLIW klade velké požadavky na kvalitu překladače (nebo vývojáře v assembleru). Další zvýšení výkonu umožňují právě vektorové instrukce (SIMD), které ale mají jeden poměrně zásadní nedostatek – sémantickou mezeru mezi imperativním „skalárním“ kódem psaným například v C (C++ atd.) a instrukční sadou, která je SIMD (jinými slovy – běžné programovací jazyky neumožňují dostatečně popsat vektorové operace). Tuto mezeru v případě jazyka Go do určité míry vyplňují balíčky simd a archsimd, kterým se budeme věnovat v dnešním článku.

SIMD operace na architektuře x86–64

V současné verzi 1.27 programovací jazyk Go (experimentálně) podporuje SIMD operace realizované na architekturách x86–64, a nově též ARM64 (Neon) a WebAssembly (ovšem zde jen pro 128bitové vektory). V praktické části článku budu ukazovat způsob realizace operací na architektuře x86–64 (později se pravděpodobně vrátíme i k ARM64). Nicméně začněme právě na x86–64.

Všechny CISCové mikroprocesory firmy Intel řady 80×86, od ještě z poloviny osmibitového čipu Intel 8088 (právě ten byl použit v IBM PC, které tak není čistě 16bitovou architekturou), až po model Intel 80486 (včetně) byly založeny na skalární architektuře SISD, stejně jako velké množství dobových mikrořadičů či digitálních signálových procesorů (DSP – Digital Signal Processor). Nevýhodou systémů SISD ovšem je, že rychlost načítání a tím i zpracování instrukcí je shora omezena a že ani s využitím velmi dlouhé instrukční pipeline se nedá – vcelku logicky – překonat limit jedné zpracované instrukce za jeden takt. Příliš velké množství řezů (slices) pipeline má naopak i své zápory, především při zpracování skoků, návratů z podprogramů či odezvy na přerušení – ve všech těchto případech je nutné vyřešit problém, co se má udělat s instrukcemi, které se nachází v rozpracovaném stavu v pipeline (mohou se buď zahodit nebo naopak dokončit, podle toho, jakým způsobem byl lineární běh programu přerušen). To čipy komplikuje, vyžaduje kvalitní prediktory skoků, kvalitní překladače atd.

V současnosti se těší značné popularitě mikroprocesory patřící do kategorie SIMD, jejíž kořeny ovšem sahají hluboko do minulosti, konkrétně do šedesátých a sedmdesátých let minulého století (tato oblast výpočetní techniky je spojena se Symourem Crayem a jeho slavnými superpočítači – ty byly skutečně vektorové). Do této kategorie patří ty architektury procesorů, u kterých se s využitím jediné instrukce může zpracovat větší množství dat. Například u rozšířené instrukční sady MMX je možné s využitím jediné instrukce provést součet dvou vektorů číselných hodnot. Může se jednat o osm osmibitových hodnot uložených v jednom vektoru, čtyři šestnáctibitové hodnoty v jednom vektoru atd.

A u rozšířeních SSE/SSE2 (ty Go podporuje prakticky od začátku, ovšem pro skalární operace) jsou délky vektorů ještě násobně větší, nemluvě o AVX a AVX-512. Této vlastnosti se dá v mnoha případech využít pro urychlení běhu programů, protože některé algoritmy (ve skutečnosti je těchto algoritmů možná až udivující počet) provádí velké množství stejných operací s rozsáhlým objemem dat – například se může jednat o aplikaci konvolučního filtru na rastrový obrázek, zpracování zvukového signálu, vynásobení matice vektorem, vynásobení dvou matic atd. A zapomenout nesmíme ani na algoritmy provádějící výpočty podobnosti vektorů – similarity search – které jsou mj. součástí RAGu v aplikacích s LLM (ale nejenom zde).

Rozšíření původní instrukční sady x86 o SIMD operace

Zkusme se nyní podívat na seznam různých SIMD (neboli nesprávně řečeno „vektorových“) rozšíření původní instrukční sady x86:

Technologie Rok uvedení Společnost Poprvé použito v čipu
MMX 1996 Intel Intel Pentium P5
3DNow! 1998 AMD AMD K6–2
SSE 1999 Intel Intel Pentium III (mikroarchitektura P6)
SSE2 2001 Intel Intel Pentium 4 (mikroarchitektura NetBurst)
SSE3 2004 Intel Intel Pentium 4 (Prescott)
SSSE3 2006 Intel mikroarchitektura Intel Core
SSE4 2006 Intel+AMD AMD K10 (SSE4a) , mikroarchitektura Intel Core
SSE5 2007 AMD (nakonec rozděleno do menších celků), mikroarchitektura Bulldozer
AVX 2008 Intel mikroarchitektura Sandy Bridge
F16C (CVT16) 2009 AMD Jaguar, Puma, Bulldozer atd.
XOP 2009 AMD mikroarchitektura Bulldozer
FMA3 2012 AMD mikroarchitektura Piledriver, Intel: Haswell a Broadwell
FMA4 2011 AMD mikroarchitektura Bulldozer (pozdější architektury po Zen 1 již ne)
AVX2 2013 Intel mikroarchitektura Haswell
AVX-512 2013 Intel Knights Landing
AMX 2020 Intel Sapphire Rapids

To, jaká rozšíření instrukční sady podporuje váš mikroprocesor, lze získat snadno:

$ cat /proc/cpuinfo

V mém konkrétním případě (Intel® Core™ i7–8665U CPU @ 1.90GHz v Lenovu T14 po záruce a s totálně zničenými konektory (kvalita se pozná) se vypíšou následující vlastnosti CPU. Z těchto příznaků jsem zdůraznil příznaky odpovídající SIMD instrukcím:

flags           : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov
pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb
rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology
nonstop_tsc cpuid aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est
tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt
tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch
cpuid_fault epb invpcid_single ssbd ibrs ibpb stibp ibrs_enhanced tpr_shadow
vnmi flexpriority ept vpid ept_ad fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms
invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves
dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp md_clear
flush_l1d arch_capabilities
Poznámka: výše uvedený výpis je ve skutečnosti proveden pro každé procesorové jádro, protože každé jádro může podporovat odlišné rozšiřující instrukce.

A co těmito rozšířeními vlastně získáme?

Pro někoho může být taktéž zajímavá i informace o tom, jak velké změny v instrukční sadě mikroprocesorů byly vlastně při přidávání nových „vektorových“ rozšiřujících instrukčních sad typu SIMD provedeny. To nám ukáže další tabulka. Je pouze nutné dát si pozor na to, že počty nových instrukcí zavedených v rámci těchto nových technologií, které jsou vypsány v tabulce pod odstavcem, nemusí přesně souhlasit s počty uváděnými v jiných informačních materiálech. Je tomu tak především z toho důvodu, že se v některých případech rozlišuje i datový typ, s nímž instrukce pracují (například se může jednat o součet vektoru s 32 bitovými hodnotami nebo 64bitovými hodnotami reprezentovanými v obou případech ve formátu s plovoucí řádovou čárkou) a někdy se taková instrukce do celkové sumy započítává pouze jedenkrát. Nicméně údaje vypsané v níže uvedené tabulce by měly být (snad s výjimkou AVX512) konzistentní, protože se jedná o počty nově přidaných operačních kódů instrukcí (například u instrukční sady SSE2 končí instrukce znakem D, S, I či Q podle typu zpracovávaných dat/operandů):

Název technologie Počet nových instrukcí
MMX 56
3DNow! 21
SSE 70
SSE2 144
SSE3 13
SSSE3 32 (ve skutečnosti vlastně jen 16 instrukcí, ovšem pro dva datové typy)
SSE4 54 (z toho 47 v rámci SSE4.1, zbytek v rámci SSE4.2)
SSE5 170 (z toho 46 základních instrukcí)
F16C 4
AVX 160
AVX2 184
AVX512 více než 300, podle konkrétních podporovaných podmnožin (subset)

Podpora SIMD operací v programovacím jazyku Go

Podpora pro SIMD operace se (postupně s akceptací této technologie) stala součástí ekosystému programovacího jazyka C. Připomeňme si, že například v GCC je možné využívat takzvané intrinsics, což je (zjednodušeně řečeno) přímé rozhraní k instrukcím mikroprocesoru (tj. jedno volání intrinsic může odpovídat tomu, že se instrukce vloží do výsledného strojového kódu). Navíc je podporováno i rozšíření pro práci s vektory a konečně lze zapnout i podporu pro autovektorizaci, při níž se překladač pokusí odhadovat ty části kódu, které by SIMD využily. V případě jazyka Go se jeho autoři poměrně dlouhou dobu bránili tomu, aby se do Go podobné koncepty dostaly (opět – z pochopitelných důvodů). Až v Go 1.26 (a nyní v Go 1.27 ještě ve větší míře) se objevily experimentální balíčky simd a simd/archsimd, které vektorové operace podporují. Jak přesně je tato podpora implementována si ukážeme v praktické části dnešního článku.

I když jsou oba výše zmíněné balíčky experimentální, jsou plně zdokumentovány (i když se nabídka funkcí a struktur pravděpodobně bude měnit):

  1. Balíček simd
    https://pkg.go.dev/simd
  2. Balíček simd/archsimd
    https://pkg.go.dev/simd/archsimd

API pro SIMD operace: explicitní přístup bez využití autovektorizace

K podpoře SIMD operací je možné přistupovat různým způsobem. Například mohou být podporovány intrinsic. Alternativně mohou být do programovacího jazyka zavedeny nové datové typy odpovídající vektorům a taktéž operátory definované nad těmito novými typy. Nebo může být upraven překladač takovým způsobem, aby prováděl autovektorizaci. V případě programovacího jazyka Go se (alespoň prozatím) žádná z těchto technologií nepoužila, protože se preferuje relativně jednoduchý, ovšem rychlý překladač, opět relativně jednoduchý programovací jazyk bez obrovské množiny datových typů a obecně je Go postaveno nikoli na „magii“ (autovektorizace), ale na explicitním zápisu všech vyžadovaných operací. Z tohoto důvodu je v Go nutné všechny SIMD operace zapisovat explicitně voláním metod na různými typy vektor. Taktéž vznikly dva balíčky. Jeden z nich je více nízkoúrovňový (simd/archsimd) a do jisté míry nahrazuje intrinsic, druhý balíček simd podporuje operace nad zobecněnými vektory.

V dnešním článku se zaměříme na nízkoúrovňový balíček simd/archsimd. Možnostem poskytovaným balíčkem simd se budeme věnovat v navazujících textech.

Praktická část: instalace Go 1.27 a nastavení vývojových prostředí

Podporu SIMD operací v jazyku Go je samozřejmě vhodné si otestovat. Vzhledem k tomu, že Go verze 1.27 byla vydána před necelými čtrnácti dny, nemusí být balíčky s touto verzí zařazeny do repositáře vaší distribuce Linuxu. To však nemusí příliš vadit, protože je možné „paralelně“ provozovat větší množství verzí Go současně, tedy jak verzi nabízenou distribucí Linuxu, tak i verzi novou.

V prvním kroku je nutné novou verzi jazyka Go přeloženou pro architekturu vašeho procesoru stáhnout z adresy https://go.dev/dl/ a rozbalit ji (v mém případě do adresáře /tmp/ramdisk/go, ale může se jednat o libovolnou jinou cestu).

Následně se musí nastavit proměnná prostředí GOROOT s cestou k rozbalenému Go:

export GOROOT=/tmp/ramdisk/go

Aby bylo možné spouštět novou verzi překladače jazyka Go bez nutnosti uvedení celé cesty, upravte si i proměnnou PATH:

export PATH=$GOROOT:$PATH

A konečně je nutné nastavit i proměnnou GOEXPERIMENT tak, aby obsahovala slovo simd:

export GOEXPERIMENT=simd
Poznámka: nastavení GOROOT by měla respektovat i vývojová prostředí. Příkladem je GOLSP a taktéž Helix.

Základní základy, na nichž je postaven experimentální balíček simd/archsimd

Balíček simd/archsimd poskytuje operace nad vektory s pevně daným počtem a typem prvků. Každá taková operace se provádí předem známou instrukcí:

Struktura Typ vektoru, který struktura reprezentuje Šířka vektoru (bity)
Float32×4 float32 × 4 prvky 32×4=128
Float32×8 float32 × 8 prvků 32×8=256
Float32×16 float32 × 16 prvků 32×16=512
     
Float64×2 float64 × 2 prvky 64×2=128
Float64×4 float64 × 4 prvky 64×4=256
Float64×8 float64 × 8 prvků 64×8=512
     
Int8×16 int8 × 16 prvků 8×16=128
Int8×32 int8 × 32 prvků 8×32=256
Int8×64 int8 × 64 prvků 8×64=512
     
Int16×8 int16 × 8 prvků 16×8=128
Int16×16 int16 × 16 prvků 16×16=256
Int16×32 int16 × 32 prvků 16×32=512
     
Int32×4 int32 × 4 prvky 32×4=128
Int32×8 int32 × 8 prvků 32×8=256
Int32×16 int32 × 16 prvků 32×16=512
     
Int64×2 int64 × 2 prvky 64×2=128
Int64×4 int64 × 4 prvky 64×4=256
Int64×8 int64 × 8 prvků 64×8=512

Pro každou z těchto struktur existuje dvojice konstruktorů, protože strukturu představující vektor lze inicializovat z pole nebo z řezu. Ukažme si příklady těchto konstruktorů pro první tři typy vektorů:

Struktura Konstruktor z řezu Konstruktor z pole
Float32×4 LoadFloat32×4 LoadFloat32×4Array
Float32×8 LoadFloat32×8 LoadFloat32×8Array
Float32×16 LoadFloat32×16 LoadFloat32×16Array

A naopak pro každou strukturu existují metody pro uložení prvků vektoru do řezu, který ovšem musí být inicializován a alokován:

func (x Float32x4) Store(s []float32)
func (x Float32x4) StoreArray(y *[4]float32)
Poznámka: to znamená, že pro vektor Float32×4 musí být řez alokován minimálně pro čtyři prvky typu float32 atd.

Zjištění, zda je podporováno konkrétní rozšíření instrukční sady o SIMD operace

Operace poskytované balíčkem simd/archsimd jsou nízkoúrovňové mj. i z toho důvodu, že jejich korektní provedení je řešeno přímým voláním instrukcí mikroprocesoru. Ovšem situace na všech moderních architekturách mikroprocesorů (AArch64, RISC-V i x86–64) je komplikovaná, protože kromě základní sady instrukcí mikroprocesor může, ale taktéž nemusí podporovat nějakou sadu či sady rozšiřujících instrukcí. Z tohoto důvodu je nutné nějakým způsobem (přímo v runtime) zjistit, které rozšiřující sady instrukcí jsou podporovány a které nikoli.

Balíček simd/archsimd v současnosti primárně cílí na architekturu x86–64. A zjištění, které rozšiřující sady instrukcí jsou podporovány (pochopitelně v čase běhu, nikoli při překladu), lze provést přes strukturu nazvanou archsimd.X86. Tato struktura neobsahuje žádné viditelné prvky (přesněji řečeno žádné prvky viditelné mimo balíček archsimd), ovšem nabízí několik metod, které vrací pravdivostní hodnotu na základě toho, zda je zkoumané rozšíření instrukční sady podporovány či nikoli.

V dnešním prvním demonstračním příkladu se pokusíme zjistit, jestli mikroprocesor podporuje rozšíření instrukční sady AVX („nižší“ rozšíření typu SSE není nutné kontrolovat – to vyžaduje i základní Go):

// ----------------------------------------------------------------------
// Podpora nativních SIMD operací v experimentálním balíčku simd/archsimd
// ----------------------------------------------------------------------
 
package main
 
import (
        "fmt"
        "simd/archsimd"
)
 
func main() {
        // struktura zpřístupňující informace o vlastnostech CPU
        features := archsimd.X86
 
        // samotná struktura může být prázdná, resp. může obsahovat
        // nedostupné prvky (mimo svůj balíček)
        fmt.Println("Features struct:", features)
 
        // metoda vracející informaci o podpoře
        // rozšíření instrukční sady AVX
        fmt.Println("AVX support:", features.AVX())
}

Ukažme si, jak může vypadat výsledek:

$ go run test_avx.go
 
Features struct: {}
AVX support: true

Struktura archsimd.X86 nabízí i další dvě metody, kterými lze zjistit podporu instrukcí AVX2 a AVX512 (tam je to pochopitelně komplikovanější kvůli několika podmnožinám instrukcí). Není nic snadnějšího, než si tyto metody otestovat:

// ----------------------------------------------------------------------
// Podpora nativních SIMD operací v experimentálním balíčku simd/archsimd
// ----------------------------------------------------------------------
 
package main
 
import (
        "fmt"
        "simd/archsimd"
)
 
func main() {
        // struktura zpřístupňující informace o vlastnostech CPU
        features := archsimd.X86
 
        // samotná struktura může být prázdná, resp. může obsahovat
        // nedostupné prvky (mimo svůj balíček)
        fmt.Println("Features struct:", features)
 
        // metoda vracející informaci o podpoře
        // rozšíření instrukční sady AVX
        fmt.Println("AVX support:", features.AVX())
 
        // metoda vracející informaci o podpoře
        // rozšíření instrukční sady AVX2
        fmt.Println("AVX2 support:", features.AVX2())
 
        // metoda vracející informaci o podpoře
        // rozšíření instrukční sady AVX512
        fmt.Println("AVX512 support:", features.AVX512())
}

Na mém počítači (výše zmíněné Lenovo T14 s i7–8665U) jsou vráceny korektní výsledky:

$ go run test_other_variants.go
 
Features struct: {}
AVX support: true
AVX2 support: true
AVX512 support: false

Vektorová operace součtu

Ukažme si tedy, jakým způsobem lze realizovat vektorovou operaci součtu. Připomeňme si, že vektory mají pevnou délku i předem daný typ prvků. Začneme vektory typu float32×4, což jsou vektory o šířce 128 bitů. Operace s těmito vektory by měla být podporována na naprosté většině mikroprocesorů z rodiny x86–64.

Vektory budou naplněny prvky získanými z pole nebo z řezů. Pravděpodobněji se častěji v praxi setkáme s řezy, ovšem pro jednoduchost začněme s poli (počet prvků si odvodí překladač):

// konstrukce a inicializace pole s odvozením počtu prvků
a1 := [...]float32{1.0, 2.0, 3.0, 4.0}
a2 := [...]float32{0.5, 0.5, 0.5, 0.5}

Pro převod pole na vektor se používá konstruktor LoadFloat32×4Array (a další konstruktory pro vektory jiných typů a odlišné délky):

// převod pole na vektor se čtyřmi prvky
v1 := archsimd.LoadFloat32x4Array(&a1)
v2 := archsimd.LoadFloat32x4Array(&a2)

Samotná operace součtu je realizována metodou Add. Výsledkem je přitom nový vektor:

// SIMD operace součtu prvků vektorů
v3 := v1.Add(v2)

Nyní nám již zbývá poslední operace – uložení prvků z vektoru v3 zpět do pole nebo do řezu. Postup je následující:

// konstrukce řezu
result := make([]float32, 4)
 
// zápis prvků vektoru do řezu
v3.Store(result)
 
// výpis obsahu řezu
fmt.Println("a1+a2", result)

Úplný zdrojový kód tohoto demonstračního příkladu vypadá následovně:

// ----------------------------------------------------------------------
// Podpora nativních SIMD operací v experimentálním balíčku simd/archsimd
// ----------------------------------------------------------------------
 
package main
 
import (
        "fmt"
        "simd/archsimd"
)
 
func main() {
        // konstrukce a inicializace pole s odvozením počtu prvků
        a1 := [...]float32{1.0, 2.0, 3.0, 4.0}
        a2 := [...]float32{0.5, 0.5, 0.5, 0.5}
 
        // výpis obsahu polí
        fmt.Println("a1", a1)
        fmt.Println("a2", a2)
 
        // převod pole na vektor se čtyřmi prvky
        v1 := archsimd.LoadFloat32x4Array(&a1)
        v2 := archsimd.LoadFloat32x4Array(&a2)
 
        // SIMD operace součtu prvků vektorů
        v3 := v1.Add(v2)
 
        // konstrukce řezu
        result := make([]float32, 4)
 
        // zápis prvků vektoru do řezu
        v3.Store(result)
 
        // výpis obsahu řezu
        fmt.Println("a1+a2", result)
}

Ověřme si, že výpočet proběhne v pořádku:

$ go run vector_add_1.go
 
a1 [1 2 3 4]
a2 [0.5 0.5 0.5 0.5]
a1+a2 [1.5 2.5 3.5 4.5]

Samozřejmě můžeme operaci součtu vektorů „izolovat“ do zvláštní funkce (důvod si ukážeme pod výpisem):

// ----------------------------------------------------------------------
// Podpora nativních SIMD operací v experimentálním balíčku simd/archsimd
// ----------------------------------------------------------------------
 
package main
 
import (
        "fmt"
        "simd/archsimd"
)
 
func AddTwoVectors(v1, v2 archsimd.Float32x4, result []float32) {
        // SIMD operace součtu prvků vektorů
        v3 := v1.Add(v2)
 
        // zápis prvků vektoru do řezu
        v3.Store(result)
}
 
func main() {
        // konstrukce a inicializace pole s odvozením počtu prvků
        a1 := [...]float32{1.0, 2.0, 3.0, 4.0}
        a2 := [...]float32{0.5, 0.5, 0.5, 0.5}
 
        // výpis obsahu polí
        fmt.Println("a1", a1)
        fmt.Println("a2", a2)
 
        // převod pole na vektor se čtyřmi prvky
        v1 := archsimd.LoadFloat32x4Array(&a1)
        v2 := archsimd.LoadFloat32x4Array(&a2)
 
        // konstrukce řezu
        result := make([]float32, 4)
        AddTwoVectors(v1, v2, result)
 
        // výpis obsahu řezu
        fmt.Println("a1+a2", result)
}

Výsledek by měl být stejný, jako tomu bylo v předchozím příkladu:

$ go run vector_add_2.go
 
a1 [1 2 3 4]
a2 [0.5 0.5 0.5 0.5]
a1+a2 [1.5 2.5 3.5 4.5]

Nás však bude zajímat, jak se vlastně funkce AddTwoVectors přeloží do strojového kódu. I to lze pochopitelně zjistit:

$ GOEXPERIMENT=simd ./go build -gcflags=-S vector_add_2.go

Ve výsledném výpisu assembleru Go (ten je sám o sobě zvláštní, ale lze ho s trochou snahy přečíst) vidíme použití instrukce VADDPS X1, X0, X0 s trojicí registrů Xn, což jsou v okolním světě registry XMMx::

main.AddTwoVectors STEXT nosplit size=32 align=0x0 args=0x38 locals=0x8 funcid=0x0
        0x0000 00000 (/tmp/ramdisk/go/bin/vector_add_2.go:8)    TEXT    main.AddTwoVectors(SB), NOSPLIT|ABIInternal, $8-56
        0x0000 00000 (/tmp/ramdisk/go/bin/vector_add_2.go:8)    PUSHQ   BP
        0x0001 00001 (/tmp/ramdisk/go/bin/vector_add_2.go:8)    MOVQ    SP, BP
        0x0004 00004 (/tmp/ramdisk/go/bin/vector_add_2.go:8)    MOVQ    AX, main.result+48(FP)
        0x0009 00009 (/tmp/ramdisk/go/bin/vector_add_2.go:8)    FUNCDATA        $0, gclocals·yXxAkm5WuMpaq4nGL4Dxjw==(SB)
        0x0009 00009 (/tmp/ramdisk/go/bin/vector_add_2.go:8)    FUNCDATA        $1, gclocals·J26BEvPExEQhJvjp9E8Whg==(SB)
        0x0009 00009 (/tmp/ramdisk/go/bin/vector_add_2.go:8)    FUNCDATA        $5, main.AddTwoVectors.arginfo1(SB)
        0x0009 00009 (/tmp/ramdisk/go/bin/vector_add_2.go:8)    FUNCDATA        $6, main.AddTwoVectors.argliveinfo(SB)
        0x0009 00009 (/tmp/ramdisk/go/bin/vector_add_2.go:8)    PCDATA  $3, $1
        0x0009 00009 (/tmp/ramdisk/go/bin/vector_add_2.go:9)    VADDPS  X1, X0, X0
        0x000d 00013 (/tmp/ramdisk/go/bin/vector_add_2.go:10)   XCHGL   AX, AX
        0x000e 00014 (/tmp/ramdisk/go/src/simd/archsimd/slice_gen_amd64.go:114) CMPQ    BX, $4
        0x0012 00018 (/tmp/ramdisk/go/src/simd/archsimd/slice_gen_amd64.go:114) JCS     26
        0x0014 00020 (/tmp/ramdisk/go/src/simd/archsimd/slice_gen_amd64.go:114) VMOVDQU X0, (AX)
        0x0018 00024 (/tmp/ramdisk/go/bin/vector_add_2.go:11)   POPQ    BP
        0x0019 00025 (/tmp/ramdisk/go/bin/vector_add_2.go:11)   RET
        0x001a 00026 (/tmp/ramdisk/go/src/simd/archsimd/slice_gen_amd64.go:114) PCDATA  $1, $1
        0x001a 00026 (/tmp/ramdisk/go/src/simd/archsimd/slice_gen_amd64.go:114) PCDATA  $4, $3626
        0x001a 00026 (/tmp/ramdisk/go/src/simd/archsimd/slice_gen_amd64.go:114) CALL    runtime.panicBounds(SB)
        0x001f 00031 (/tmp/ramdisk/go/src/simd/archsimd/slice_gen_amd64.go:114) XCHGL   AX, AX
        0x0000 55 48 89 e5 48 89 44 24 30 c5 f8 58 c1 90 48 83  UH..H.D$0..X..H.
        0x0010 fb 04 72 06 c5 fa 7f 00 5d c3 e8 00 00 00 00 90  ..r.....].......
        rel 27+4 t=R_CALL runtime.panicBounds+0
Poznámka: registry XMMx jsou podporovány již v rozšíření instrukční sady SSE (i když nebyly podporovány všechny typy vektorů).

Součet provedený s vektory s větším počtem prvků

V osmé kapitole jsme si mj. řekli, že vektory s prvky typu float32 mohou obsahovat čtyři, osm, nebo dokonce šestnáct prvků:

Struktura Typ vektoru, který struktura reprezentuje Šířka vektoru (bity)
Float32×4 float32 × 4 prvky 128
Float32×8 float32 × 8 prvků 256
Float32×16 float32 × 16 prvků 512

Problém spočívá v tom, že operace nad širšími vektory musí být podporovány mikroprocesorem. Go přeloží i ty operace, které ve skutečnosti podporovány nebudou. To si ukážeme na další dvojici demonstračních příkladů. V prvním příkladu sečteme osmiprvkové vektory s hodnotami typu float32:

// ----------------------------------------------------------------------
// Podpora nativních SIMD operací v experimentálním balíčku simd/archsimd
// ----------------------------------------------------------------------
 
package main
 
import (
        "fmt"
        "simd/archsimd"
)
 
func AddTwoVectors(v1, v2 archsimd.Float32x8, result []float32) {
        // SIMD operace součtu prvků vektorů
        v3 := v1.Add(v2)
 
        // zápis prvků vektoru do řezu
        v3.Store(result)
}
 
func main() {
        // konstrukce a inicializace pole s odvozením počtu prvků
        a1 := [...]float32{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0}
        a2 := [...]float32{0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5}
 
        // výpis obsahu polí
        fmt.Println("a1", a1)
        fmt.Println("a2", a2)
 
        // převod pole na vektor s osmi prvky
        v1 := archsimd.LoadFloat32x8Array(&a1)
        v2 := archsimd.LoadFloat32x8Array(&a2)
 
        // konstrukce řezu
        result := make([]float32, 8)
        AddTwoVectors(v1, v2, result)
 
        // výpis obsahu řezu
        fmt.Println("a1+a2", result)
}

Otestování funkcionality dopadne na mém počítači v pořádku:

$ ./go run vector_add_3.go
 
a1 [1 2 3 4 5 6 7 8]
a2 [0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5]
a1+a2 [1.5 2.5 3.5 4.5 5.5 6.5 7.5 8.5]

Zajímavé bude zjistit, jak se operace součtu přeložila do strojového kódu:

main.AddTwoVectors STEXT nosplit size=32 align=0x0 args=0x58 locals=0x8 funcid=0x0
        0x0000 00000 (/tmp/ramdisk/go/bin/vector_add_3.go:8)    TEXT    main.AddTwoVectors(SB), NOSPLIT|ABIInternal, $8-88
        0x0000 00000 (/tmp/ramdisk/go/bin/vector_add_3.go:8)    PUSHQ   BP
        0x0001 00001 (/tmp/ramdisk/go/bin/vector_add_3.go:8)    MOVQ    SP, BP
        0x0004 00004 (/tmp/ramdisk/go/bin/vector_add_3.go:8)    MOVQ    AX, main.result+80(FP)
        0x0009 00009 (/tmp/ramdisk/go/bin/vector_add_3.go:8)    FUNCDATA        $0, gclocals·P6TvcXLOJJut7zaN2ve3yQ==(SB)
        0x0009 00009 (/tmp/ramdisk/go/bin/vector_add_3.go:8)    FUNCDATA        $1, gclocals·J26BEvPExEQhJvjp9E8Whg==(SB)
        0x0009 00009 (/tmp/ramdisk/go/bin/vector_add_3.go:8)    FUNCDATA        $5, main.AddTwoVectors.arginfo1(SB)
        0x0009 00009 (/tmp/ramdisk/go/bin/vector_add_3.go:8)    FUNCDATA        $6, main.AddTwoVectors.argliveinfo(SB)
        0x0009 00009 (/tmp/ramdisk/go/bin/vector_add_3.go:8)    PCDATA  $3, $1
        0x0009 00009 (/tmp/ramdisk/go/bin/vector_add_3.go:9)    VADDPS  Y1, Y0, Y0
        0x000d 00013 (/tmp/ramdisk/go/bin/vector_add_3.go:10)   XCHGL   AX, AX
        0x000e 00014 (/tmp/ramdisk/go/src/simd/archsimd/slice_gen_amd64.go:234) CMPQ    BX, $8
        0x0012 00018 (/tmp/ramdisk/go/src/simd/archsimd/slice_gen_amd64.go:234) JCS     26
        0x0014 00020 (/tmp/ramdisk/go/src/simd/archsimd/slice_gen_amd64.go:234) VMOVDQU Y0, (AX)
        0x0018 00024 (/tmp/ramdisk/go/bin/vector_add_3.go:11)   POPQ    BP
        0x0019 00025 (/tmp/ramdisk/go/bin/vector_add_3.go:11)   RET
        0x001a 00026 (/tmp/ramdisk/go/src/simd/archsimd/slice_gen_amd64.go:234) PCDATA  $1, $1
        0x001a 00026 (/tmp/ramdisk/go/src/simd/archsimd/slice_gen_amd64.go:234) PCDATA  $4, $3770
        0x001a 00026 (/tmp/ramdisk/go/src/simd/archsimd/slice_gen_amd64.go:234) CALL    runtime.panicBounds(SB)
        0x001f 00031 (/tmp/ramdisk/go/src/simd/archsimd/slice_gen_amd64.go:234) XCHGL   AX, AX
        0x0000 55 48 89 e5 48 89 44 24 50 c5 fc 58 c1 90 48 83  UH..H.D$P..X..H.
        0x0010 fb 08 72 06 c5 fe 7f 00 5d c3 e8 00 00 00 00 90  ..r.....].......
        rel 27+4 t=R_CALL runtime.panicBounds+0

Z výpisu je patrné použití instrukce VADDPS (vektory s prvky typu single), ovšem nikoli s registry XMMx, ale s registry YMMx, které jsou podporovány rozšířením instrukční sady AVX (a vyšší).

Pojďme o krok dále a pokusme se sečíst dvojici vektorů se šestnácti prvky:

// ----------------------------------------------------------------------
// Podpora nativních SIMD operací v experimentálním balíčku simd/archsimd
// ----------------------------------------------------------------------
 
package main
 
import (
        "fmt"
        "simd/archsimd"
)
 
func AddTwoVectors(v1, v2 archsimd.Float32x16, result []float32) {
        // SIMD operace součtu prvků vektorů
        v3 := v1.Add(v2)
 
        // zápis prvků vektoru do řezu
        v3.Store(result)
}
 
func main() {
        // konstrukce a inicializace pole s odvozením počtu prvků
        a1 := [...]float32{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0}
        a2 := [...]float32{0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5}
 
        // výpis obsahu polí
        fmt.Println("a1", a1)
        fmt.Println("a2", a2)
 
        // převod pole na vektor se šestnácti prvky
        v1 := archsimd.LoadFloat32x16Array(&a1)
        v2 := archsimd.LoadFloat32x16Array(&a2)
 
        // konstrukce řezu
        result := make([]float32, 16)
        AddTwoVectors(v1, v2, result)
 
        // výpis obsahu řezu
        fmt.Println("a1+a2", result)
}

V přeloženém strojovém kódu je opět použita instrukce VADDPS, nyní ovšem pracující s registry ZMMX, které jsou podporovány v AVX-512. Tyto registry mají šířku 512 bitů a tedy mohou obsahovat 512/32=16 prvků typu single/float32:

main.AddTwoVectors STEXT nosplit size=39 align=0x0 args=0x98 locals=0x8 funcid=0x0
        0x0000 00000 (/tmp/ramdisk/go/bin/vector_add_4.go:8)    TEXT    main.AddTwoVectors(SB), NOSPLIT|ABIInternal, $8-152
        0x0000 00000 (/tmp/ramdisk/go/bin/vector_add_4.go:8)    PUSHQ   BP
        0x0001 00001 (/tmp/ramdisk/go/bin/vector_add_4.go:8)    MOVQ    SP, BP
        0x0004 00004 (/tmp/ramdisk/go/bin/vector_add_4.go:8)    MOVQ    AX, main.result+144(FP)
        0x000c 00012 (/tmp/ramdisk/go/bin/vector_add_4.go:8)    FUNCDATA        $0, gclocals·sqUa24CZbyx6dzelDMK6vA==(SB)
        0x000c 00012 (/tmp/ramdisk/go/bin/vector_add_4.go:8)    FUNCDATA        $1, gclocals·J26BEvPExEQhJvjp9E8Whg==(SB)
        0x000c 00012 (/tmp/ramdisk/go/bin/vector_add_4.go:8)    FUNCDATA        $5, main.AddTwoVectors.arginfo1(SB)
        0x000c 00012 (/tmp/ramdisk/go/bin/vector_add_4.go:8)    FUNCDATA        $6, main.AddTwoVectors.argliveinfo(SB)
        0x000c 00012 (/tmp/ramdisk/go/bin/vector_add_4.go:8)    PCDATA  $3, $1
        0x000c 00012 (/tmp/ramdisk/go/bin/vector_add_4.go:9)    VADDPS  Z1, Z0, Z0
        0x0012 00018 (/tmp/ramdisk/go/bin/vector_add_4.go:10)   XCHGL   AX, AX
        0x0013 00019 (/tmp/ramdisk/go/src/simd/archsimd/slice_gen_amd64.go:354) CMPQ    BX, $16
        0x0017 00023 (/tmp/ramdisk/go/src/simd/archsimd/slice_gen_amd64.go:354) JCS     33
        0x0019 00025 (/tmp/ramdisk/go/src/simd/archsimd/slice_gen_amd64.go:354) VMOVDQU64       Z0, (AX)
        0x001f 00031 (/tmp/ramdisk/go/bin/vector_add_4.go:11)   POPQ    BP
        0x0020 00032 (/tmp/ramdisk/go/bin/vector_add_4.go:11)   RET
        0x0021 00033 (/tmp/ramdisk/go/src/simd/archsimd/slice_gen_amd64.go:354) PCDATA  $1, $1
        0x0021 00033 (/tmp/ramdisk/go/src/simd/archsimd/slice_gen_amd64.go:354) PCDATA  $4, $4058
        0x0021 00033 (/tmp/ramdisk/go/src/simd/archsimd/slice_gen_amd64.go:354) CALL    runtime.panicBounds(SB)
        0x0026 00038 (/tmp/ramdisk/go/src/simd/archsimd/slice_gen_amd64.go:354) XCHGL   AX, AX
        0x0000 55 48 89 e5 48 89 84 24 90 00 00 00 62 f1 7c 48  UH..H..$....b.|H
        0x0010 58 c1 90 48 83 fb 10 72 08 62 f1 fe 48 7f 00 5d  X..H...r.b..H..]
        0x0020 c3 e8 00 00 00 00 90                             .......
        rel 34+4 t=R_CALL runtime.panicBounds+0

Pokud o spuštění v mém případě ovšem dopadne neslavně, protože mikroprocesor v mém počítači AVX-512 nepodporuje (je totiž určen pro běžnou práci, nikoli pro vytápění domácnosti):

SIGILL: illegal instruction
PC=0x499ebe m=0 sigcode=2
instruction bytes: 0x62 0xf1 0xfe 0x48 0x6f 0x84 0x24 0x68 0x0 0x0 0x0 0x62 0xf1 0x7c 0x48 0x58
 
goroutine 1 gp=0x1970112861e0 m=0 mp=0x572260 [running]:
main.main()
        /tmp/ramdisk/go/bin/vector_add_4.go:17 +0x7e fp=0x197011322eb8 sp=0x197011322db0 pc=0x499ebe
runtime.main()
        /tmp/ramdisk/go/src/runtime/proc.go:302 +0x427 fp=0x197011322fe0 sp=0x197011322eb8 pc=0x447987
runtime.goexit({})
        /tmp/ramdisk/go/src/runtime/asm_amd64.s:1264 +0x1 fp=0x197011322fe8 sp=0x197011322fe0 pc=0x47e421
 
goroutine 2 gp=0x197011286780 m=nil [force gc (idle)]:
runtime.gopark(0x0?, 0x0?, 0x0?, 0x0?, 0x0?)
        /tmp/ramdisk/go/src/runtime/proc.go:474 +0xca fp=0x1970112fafa8 sp=0x1970112faf88 pc=0x478fca
runtime.goparkunlock(...)
        /tmp/ramdisk/go/src/runtime/proc.go:480
runtime.forcegchelper()
        /tmp/ramdisk/go/src/runtime/proc.go:387 +0xb3 fp=0x1970112fafe0 sp=0x1970112fafa8 pc=0x447c53
runtime.goexit({})
        /tmp/ramdisk/go/src/runtime/asm_amd64.s:1264 +0x1 fp=0x1970112fafe8 sp=0x1970112fafe0 pc=0x47e421
created by runtime.init.7 in goroutine 1
        /tmp/ramdisk/go/src/runtime/proc.go:375 +0x1a
 
goroutine 3 gp=0x197011286d20 m=nil [GC sweep wait]:
runtime.gopark(0x0?, 0x0?, 0x0?, 0x0?, 0x0?)
        /tmp/ramdisk/go/src/runtime/proc.go:474 +0xca fp=0x1970112fb788 sp=0x1970112fb768 pc=0x478fca
runtime.goparkunlock(...)
        /tmp/ramdisk/go/src/runtime/proc.go:480
runtime.bgsweep(0x1970112a8080)
        /tmp/ramdisk/go/src/runtime/mgcsweep.go:279 +0x94 fp=0x1970112fb7c8 sp=0x1970112fb788 pc=0x4339f4
runtime.gcenable.gowrap1()
        /tmp/ramdisk/go/src/runtime/mgc.go:214 +0x17 fp=0x1970112fb7e0 sp=0x1970112fb7c8 pc=0x472177
runtime.goexit({})
        /tmp/ramdisk/go/src/runtime/asm_amd64.s:1264 +0x1 fp=0x1970112fb7e8 sp=0x1970112fb7e0 pc=0x47e421
created by runtime.gcenable in goroutine 1
        /tmp/ramdisk/go/src/runtime/mgc.go:214 +0x66
 
goroutine 4 gp=0x197011286f00 m=nil [GC scavenge wait]:
runtime.gopark(0x1970112a8080?, 0x4a3428?, 0x1?, 0x0?, 0x197011286f00?)
        /tmp/ramdisk/go/src/runtime/proc.go:474 +0xca fp=0x1970112fbf78 sp=0x1970112fbf58 pc=0x478fca
runtime.goparkunlock(...)
        /tmp/ramdisk/go/src/runtime/proc.go:480
runtime.(*scavengerState).park(0x571260)
        /tmp/ramdisk/go/src/runtime/mgcscavenge.go:425 +0x49 fp=0x1970112fbfa8 sp=0x1970112fbf78 pc=0x4315c9
runtime.bgscavenge(0x1970112a8080)
        /tmp/ramdisk/go/src/runtime/mgcscavenge.go:653 +0x3c fp=0x1970112fbfc8 sp=0x1970112fbfa8 pc=0x431b1c
runtime.gcenable.gowrap2()
        /tmp/ramdisk/go/src/runtime/mgc.go:215 +0x17 fp=0x1970112fbfe0 sp=0x1970112fbfc8 pc=0x472137
runtime.goexit({})
        /tmp/ramdisk/go/src/runtime/asm_amd64.s:1264 +0x1 fp=0x1970112fbfe8 sp=0x1970112fbfe0 pc=0x47e421
created by runtime.gcenable in goroutine 1
        /tmp/ramdisk/go/src/runtime/mgc.go:215 +0xa5
 
goroutine 5 gp=0x1970112874a0 m=nil [GOMAXPROCS updater (idle)]:
runtime.gopark(0x0?, 0x0?, 0x0?, 0x0?, 0x0?)
        /tmp/ramdisk/go/src/runtime/proc.go:474 +0xca fp=0x1970112fa788 sp=0x1970112fa768 pc=0x478fca
runtime.goparkunlock(...)
        /tmp/ramdisk/go/src/runtime/proc.go:480
runtime.updateMaxProcsGoroutine()
        /tmp/ramdisk/go/src/runtime/proc.go:7146 +0xe7 fp=0x1970112fa7e0 sp=0x1970112fa788 pc=0x454fc7
runtime.goexit({})
        /tmp/ramdisk/go/src/runtime/asm_amd64.s:1264 +0x1 fp=0x1970112fa7e8 sp=0x1970112fa7e0 pc=0x47e421
created by runtime.defaultGOMAXPROCSUpdateEnable in goroutine 1
        /tmp/ramdisk/go/src/runtime/proc.go:7134 +0x37
 
goroutine 6 gp=0x197011287680 m=nil [finalizer wait]:
runtime.gopark(0x0?, 0x0?, 0x0?, 0x0?, 0x0?)
        /tmp/ramdisk/go/src/runtime/proc.go:474 +0xca fp=0x1970112fc620 sp=0x1970112fc600 pc=0x478fca
runtime.runFinalizers()
        /tmp/ramdisk/go/src/runtime/mfinal.go:210 +0x107 fp=0x1970112fc7e0 sp=0x1970112fc620 pc=0x424dc7
runtime.goexit({})
        /tmp/ramdisk/go/src/runtime/asm_amd64.s:1264 +0x1 fp=0x1970112fc7e8 sp=0x1970112fc7e0 pc=0x47e421
created by runtime.createfing in goroutine 1
        /tmp/ramdisk/go/src/runtime/mfinal.go:172 +0x3d
 
rax    0x499e40
rbx    0x0
rcx    0x1970112861e0
rdx    0x197011322dd8
rdi    0x0
rsi    0x4a6460
rbp    0x197011322ea8
rsp    0x197011322db0
r8     0x2
r9     0x38
r10    0x7f53ff011620
r11    0xd0
r12    0x197011322e30
r13    0x1970113360d0
r14    0x1970112861e0
r15    0xffffffffffffffff
rip    0x499ebe
rflags 0x10202
cs     0x33
fs     0x0
gs     0x0
exit status 2
Poznámka: z tohoto pádu je patrné, že pokud použijeme přímo balíček simd/archsimd, je nutné explicitně testovat, která rozšíření instrukční sady jsou podporována a která nikoli.

Základní unární operace s prvky vektorů

S vektory lze provádět i různé unární operace, přičemž každá taková operace se volá jako metoda vektoru a výsledkem je nový vektor. Některé z těchto operací jsou ukázány v následujícím demonstračním příkladu:

Metoda Stručný popis
Abs absolutní hodnoty všech prvků vektoru
Neg změna znaménka všech prvků vektoru
Reciprocal převrácená hodnota všech prvků vektoru
Sqrt druhá odmocnina všech prvků vektoru
ReciprocalSqrt převrácená hodnota druhé odmocniny (používá se například při normalizacích atd.)

V dalším demonstračním příkladu jsou tyto operace ukázány:

// ----------------------------------------------------------------------
// Podpora nativních SIMD operací v experimentálním balíčku simd/archsimd
// ----------------------------------------------------------------------
 
package main
 
import (
        "fmt"
        "simd/archsimd"
)
 
func main() {
        // konstrukce a inicializace pole s odvozením počtu prvků
        a1 := [...]float32{-1.0, -2.0, -3.0, -4.0}
        a2 := [...]float32{1.0, 2.0, 3.0, 4.0}
 
        // převod pole na vektor se čtyřmi prvky
        v1 := archsimd.LoadFloat32x4Array(&a1)
        v2 := archsimd.LoadFloat32x4Array(&a2)
 
        // konstrukce řezu
        result := make([]float32, 4)
 
        // SIMD operace výpočtu absolutní hodnoty
        v3 := v1.Abs()
        v2.Store(result)
        fmt.Println("abs", result)
 
        // SIMD operace změny znaménka
        v3 = v1.Neg()
        v3.Store(result)
        fmt.Println("neg", result)
 
        // SIMD operace výpočtu převrácené hodnoty
        v3 = v1.Reciprocal()
        v3.Store(result)
        fmt.Println("reciprocal", result)
 
        // SIMD operace výpočtu druhé odmocniny
        v3 = v2.Sqrt()
        v3.Store(result)
        fmt.Println("sqrt", result)
 
        // SIMD operace výpočtu převrácené hodnoty druhé odmocniny
        v3 = v2.ReciprocalSqrt()
        v3.Store(result)
        fmt.Println("reciprocal sqrt", result)
}

Vypočtené výsledky:

$ ./go run vector_unary.go
 
abs [1 2 3 4]
neg [1 2 3 4]
reciprocal [-0.99975586 -0.49987793 -0.33325195 -0.24993896]
sqrt [1 1.4142135 1.7320508 2]
reciprocal sqrt [0.99975586 0.7069092 0.5772705 0.49987793]
Poznámka: přesnost výpočtů i výsledků pochopitelně stále odpovídá formátu single a tudíž i datovému typu float32.

Další binární operace s prvky vektorů

Ve stručnosti si taktéž ukážeme další binární operace prováděné s odpovídajícími si prvky dvou vektorů. Tyto operace jsou opět dostupné přes metody, které mají jeden parametr (druhý vektor) a návratovou hodnotou je nový vektor s výsledky:

Metoda Stručný popis
Add součet odpovídajících si prvků vektorů
Sub rozdíl odpovídajících si prvků vektorů
Mul součin odpovídajících si prvků vektorů
Div podíl odpovídajících si prvků vektorů
Min porovnání odpovídajících si prvků vektorů, uložení menšího z nich do výsledného vektoru
Max porovnání odpovídajících si prvků vektorů, uložení většího z nich do výsledného vektoru

Tyto operace si pochopitelně opět otestujeme:

// ----------------------------------------------------------------------
// Podpora nativních SIMD operací v experimentálním balíčku simd/archsimd
// ----------------------------------------------------------------------
 
package main
 
import (
        "fmt"
        "simd/archsimd"
)
 
func main() {
        // konstrukce a inicializace pole s odvozením počtu prvků
        a1 := [...]float32{1.0, 2.0, 3.0, 4.0}
 
        // převod pole na vektor se čtyřmi prvky
        v1 := archsimd.LoadFloat32x4Array(&a1)
 
        // naplnění celého vektoru stejnou hodnotou
        v2 := archsimd.BroadcastFloat32x4(0.5)
 
        // konstrukce řezu
        result := make([]float32, 4)
 
        // SIMD operace součtu prvků vektorů
        v3 := v1.Add(v2)
        v3.Store(result)
        fmt.Println("add", result)
 
        // SIMD operace rozdílu prvků vektorů
        v3 = v1.Sub(v2)
        v3.Store(result)
        fmt.Println("sub", result)
 
        // SIMD operace součinu prvků vektorů
        v3 = v1.Mul(v2)
        v3.Store(result)
        fmt.Println("mul", result)
 
        // SIMD operace podílu prvků vektorů
        v3 = v1.Div(v2)
        v3.Store(result)
        fmt.Println("div", result)
 
        // SIMD operace výběru menších prvků vektorů
        v3 = v1.Min(v2)
        v3.Store(result)
        fmt.Println("min", result)
 
        // SIMD operace výběru větších prvků vektorů
        v3 = v1.Max(v2)
        v3.Store(result)
        fmt.Println("max", result)
}

Výsledky:

$ ./go run vector_binary.go
 
add [1.5 2.5 3.5 4.5]
sub [0.5 1.5 2.5 3.5]
mul [0.5 1 1.5 2]
div [2 4 6 8]
min [0.5 0.5 0.5 0.5]
max [1 2 3 4]

Různé způsoby zaokrouhlení hodnot prvků vektorů

V nabídce operací s vektory nalezneme i čtyři metody s různými způsoby zaokrouhlení hodnot prvků vektorů. Zajímavé je, že výsledkem je vždy vektor stejného typu, jako vektor vstupní – nedochází zde tedy ke konverzi na typ celé číslo:

Metoda Provedená operace
Floor zaokrouhlení na nejbližší nižší celočíselnou hodnotu
Ceil zaokrouhlení na nejbližší vyšší celočíselnou hodnotu
Trunc odříznutí desetinné části
Round klasické zaokrouhlování

Vše si opět ověříme na čtyřprvkovém vektoru s vhodně zvolenými hodnotami (0,5 vs 0,5+epsilon):

// ----------------------------------------------------------------------
// Podpora nativních SIMD operací v experimentálním balíčku simd/archsimd
// ----------------------------------------------------------------------
 
package main
 
import (
        "fmt"
        "simd/archsimd"
)
 
func main() {
        // konstrukce a inicializace pole s odvozením počtu prvků
        a1 := [...]float32{0.4, 0.5, 0.50001, 0.6}
 
        fmt.Println("a1", a1)
 
        // převod pole na vektor se čtyřmi prvky
        v1 := archsimd.LoadFloat32x4Array(&a1)
 
        // konstrukce řezu
        result := make([]float32, 4)
 
        v2 := v1.Floor()
        v2.Store(result)
        fmt.Println("floor", result)
 
        v2 = v1.Ceil()
        v2.Store(result)
        fmt.Println("ceil ", result)
 
        v2 = v1.Trunc()
        v2.Store(result)
        fmt.Println("trunc", result)
 
        v2 = v1.Round()
        v2.Store(result)
        fmt.Println("round", result)
}

Výsledky by měly vypadat takto:

$ ./go run vector_round.go
 
a1 [0.4 0.5 0.50001 0.6]
floor [0 0 0 0]
ceil  [1 1 1 1]
trunc [0 0 0 0]
round [0 0 1 1]
Poznámka: za povšimnutí stojí především to, že hodnota 0,5 (ta je reprezentována přesně) může být zaokrouhlena směrem dolů.

Operace typu broadcast

Vyzkoušejme si ještě operaci, která se nazývá broadcast, tedy všesměrové vysílání. Tato operace umožňuje naplnění všech prvků vektorů stejnou hodnotou. Použití je snadné, protože přímo z názvu funkce je zřejmé, jaký bude typ výsledného vektoru. Například naplníme vektor se čtyřmi prvky typu float32 hodnotou 0.5:

// naplnění celého vektoru stejnou hodnotou
v2 := archsimd.BroadcastFloat32x4(0.5)

Použití této funkce/konstruktoru v příkladu je snadné:

// ----------------------------------------------------------------------
// Podpora nativních SIMD operací v experimentálním balíčku simd/archsimd
// ----------------------------------------------------------------------
 
package main
 
import (
        "fmt"
        "simd/archsimd"
)
 
func AddTwoVectors(v1, v2 archsimd.Float32x4, result []float32) {
        // SIMD operace součtu prvků vektorů
        v3 := v1.Add(v2)
 
        // zápis prvků vektoru do řezu
        v3.Store(result)
}
 
func main() {
        // konstrukce a inicializace pole s odvozením počtu prvků
        a1 := [...]float32{1.0, 2.0, 3.0, 4.0}
 
        // převod pole na vektor se čtyřmi prvky
        v1 := archsimd.LoadFloat32x4Array(&a1)
 
        // naplnění celého vektoru stejnou hodnotou
        v2 := archsimd.BroadcastFloat32x4(0.5)
 
        // konstrukce řezu
        result := make([]float32, 4)
 
        // provedení vybrané operace s vektory
        AddTwoVectors(v1, v2, result)
 
        // výpis obsahu řezu
        fmt.Println(result)
}

Výsledky resp. výsledek ukazuje, že se původní vektor skutečně sečetl s vektorem, jehož všechny prvky mají hodnotu 0.5:

$ go run vector_broadcast.go 
 
[1.5 2.5 3.5 4.5]

Ve výsledném strojovém kódu se pro implementaci broadcastu používají instrukce VMOVSS a VBROADCASTSS (S na konci znamená označení typu single, tedy v Go float32):

main.main STEXT size=185 align=0x0 args=0x0 locals=0x60 funcid=0x0
        0x0000 00000 (/tmp/ramdisk/go/bin/vector_broadcast.go:20)       TEXT    main.main(SB), ABIInternal, $96-0
        0x0000 00000 (/tmp/ramdisk/go/bin/vector_broadcast.go:20)       CMPQ    SP, 16(R14)
        0x0004 00004 (/tmp/ramdisk/go/bin/vector_broadcast.go:20)       PCDATA  $0, $-2
        0x0004 00004 (/tmp/ramdisk/go/bin/vector_broadcast.go:20)       JLS     175
        0x000a 00010 (/tmp/ramdisk/go/bin/vector_broadcast.go:20)       PCDATA  $0, $-1
        0x000a 00010 (/tmp/ramdisk/go/bin/vector_broadcast.go:20)       PUSHQ   BP
        0x000b 00011 (/tmp/ramdisk/go/bin/vector_broadcast.go:20)       MOVQ    SP, BP
        0x000e 00014 (/tmp/ramdisk/go/bin/vector_broadcast.go:20)       SUBQ    $88, SP
        0x0012 00018 (/tmp/ramdisk/go/bin/vector_broadcast.go:20)       FUNCDATA        $0, gclocals·J26BEvPExEQhJvjp9E8Whg==(SB)
        0x0012 00018 (/tmp/ramdisk/go/bin/vector_broadcast.go:20)       FUNCDATA        $1, gclocals·5VlIHulmjZXLybuW+4WJgQ==(SB)
        0x0012 00018 (/tmp/ramdisk/go/bin/vector_broadcast.go:20)       FUNCDATA        $2, main.main.stkobj(SB)
        0x0012 00018 (/tmp/ramdisk/go/bin/vector_broadcast.go:22)       MOVQ    $4611686019492741120, DX
        0x001c 00028 (/tmp/ramdisk/go/bin/vector_broadcast.go:22)       MOVQ    DX, main.a1+40(SP)
        0x0021 00033 (/tmp/ramdisk/go/bin/vector_broadcast.go:22)       MOVQ    $4647714816524288000, DX
        0x002b 00043 (/tmp/ramdisk/go/bin/vector_broadcast.go:22)       MOVQ    DX, main.a1+48(SP)
        0x0030 00048 (/tmp/ramdisk/go/src/simd/archsimd/other_gen_amd64.go:85)  VMOVSS  $f32.3f000000(SB), X0
        0x0038 00056 (/tmp/ramdisk/go/src/simd/archsimd/other_gen_amd64.go:85)  VBROADCASTSS    X0, X0

Vytvoření masky, základní práce s maskami

Některé operace s vektory využívají takzvané masky, což jsou taktéž vektory (s pevným počtem prvků), které ovšem obsahují pouze pravdivostní hodnoty. Příkladem jsou operace určené pro porovnání odpovídajících si prvků vektorů na nějakou relaci („menší než“ atd.). Výsledkem je maska s pravdivostní hodnotou nastavenou pro každou dvojici porovnávaných prvků. Použití je snadné:

// ----------------------------------------------------------------------
// Podpora nativních SIMD operací v experimentálním balíčku simd/archsimd
// ----------------------------------------------------------------------
 
package main
 
import (
        "fmt"
        "simd/archsimd"
)
 
func main() {
        // konstrukce a inicializace pole s odvozením počtu prvků
        a1 := [...]float32{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0}
        a2 := [...]float32{5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0}
 
        // výpis obsahu polí
        fmt.Println("a1", a1)
        fmt.Println("a2", a2)
 
        // převod pole na vektor s osmi prvky
        v1 := archsimd.LoadFloat32x8Array(&a1)
        v2 := archsimd.LoadFloat32x8Array(&a2)
 
        // SIMD operace porovnání prvků vektoru
        mask := v1.Less(v2)
        fmt.Println("< ", mask.String())
 
        mask = v1.LessEqual(v2)
        fmt.Println("<=", mask.String())
 
        mask = v1.Equal(v2)
        fmt.Println("==", mask.String())
 
        mask = v1.GreaterEqual(v2)
        fmt.Println(>=", mask.String())
 
        mask = v1.Greater(v2)
        fmt.Println("> ", mask.String())
 
        mask = v1.NotEqual(v2)
        fmt.Println("!=", mask.String())
}

Výsledky porovnání prvků dvou vektorů na zvolenou relaci dopadne následovně:

$ ./go run vector_comparison.go
 
a1 [1 2 3 4 5 6 7 8]
a2 [5 5 5 5 5 5 5 5]
<  {1,1,1,1,0,0,0,0}
<= {1,1,1,1,1,0,0,0}
== {0,0,0,0,1,0,0,0}
>= {0,0,0,0,1,1,1,1}
>  {0,0,0,0,0,1,1,1}
!= {1,1,1,1,0,1,1,1}

Představme si, že z nějakého vektoru potřebujeme získat pouze ty prvky, jejichž hodnoty jsou menší než zadaná limitní hodnota. Jednu z možností představuje právě použití masky. Maska se inicializuje porovnáním prvků vektoru s mezní hodnotou (uloženou v dalším vektoru – použít lze broadcast). A následně se s využitím metody Masked z původního vektoru vrátí jen ty prvky, jejichž odpovídající příznak v masce je roven jedné. Ostatní prvky budou vynulovány:

// ----------------------------------------------------------------------
// Podpora nativních SIMD operací v experimentálním balíčku simd/archsimd
// ----------------------------------------------------------------------
 
package main
 
import (
        "fmt"
        "simd/archsimd"
)
 
func main() {
        // konstrukce a inicializace pole s odvozením počtu prvků
        a1 := [...]float32{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0}
        a2 := [...]float32{5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0}
 
        // výpis obsahu polí
        fmt.Println("a1    ", a1)
        fmt.Println("a2    ", a2)
 
        // převod pole na vektor s osmi prvky
        v1 := archsimd.LoadFloat32x8Array(&a1)
        v2 := archsimd.LoadFloat32x8Array(&a2)
 
        // SIMD operace porovnání prvků vektoru
        mask := v1.Less(v2)
        fmt.Println("<     ", mask.String())
 
        v3 := v1.Masked(mask)
 
        // konstrukce řezu
        result := make([]float32, 8)
 
        // zápis prvků vektoru do řezu
        v3.Store(result)
 
        // výpis obsahu řezu
        fmt.Println("masked", v3)
}

Ověřme si funkcionalitu:

Školení Zabbix

$ ./go run vector_masked.go
 
a1     [1 2 3 4 5 6 7 8]
a2     [5 5 5 5 5 5 5 5]
<      {1,1,1,1,0,0,0,0}
masked {1,2,3,4,0,0,0,0}
Poznámka: sofistikovanější příklady použití masky si ukážeme v navazujícím článku.

Repositář s demonstračními příklady

Zdrojové kódy všech dnes použitých demonstračních příkladů byly uloženy do Git repositáře, který je dostupný na adrese https://github.com/tisnik/go-root. Všechny příklady vyžadují Go verze 1.26 nebo 1.27. V případě, že nebudete chtít klonovat celý repositář (ten je ovšem – alespoň prozatím – relativně malý, dnes má přibližně šest až sedm megabajtů), můžete namísto toho použít odkazy na jednotlivé demonstrační příklady, které naleznete v následující tabulce:

# Příklad Stručný popis Cesta
1 test_avx.go runtime test, zda procesor podporuje instrukční sadu AVX https://github.com/tisnik/go-root/blob/master/article_B2/tes­t_avx.go
2 test_other_variants.go runtime test, zda procesor podporuje další rozšíření instrukční sady o SIMD operace https://github.com/tisnik/go-root/blob/master/article_B2/tes­t_other_variants.go
       
3 vector_add_1.go součet dvou vektorů typu Float32×4 https://github.com/tisnik/go-root/blob/master/article_B2/vec­tor_add_1.go
4 vector_add_2.go součet dvou vektorů typu Float32×4 (upraveno do samostatné funkce) https://github.com/tisnik/go-root/blob/master/article_B2/vec­tor_add_2.go
5 vector_add_3.go součet dvou vektorů typu Float32×8 https://github.com/tisnik/go-root/blob/master/article_B2/vec­tor_add_3.go
6 vector_add_4.go součet dvou vektorů typu Float32×16 https://github.com/tisnik/go-root/blob/master/article_B2/vec­tor_add_4.go
       
7 vector_unary.go základní unární operace s vektory https://github.com/tisnik/go-root/blob/master/article_B2/vec­tor_unary.go
8 vector_binary.go základní binární operace s vektory https://github.com/tisnik/go-root/blob/master/article_B2/vec­tor_binary.go
9 vector_round.go zaokrouhlení a podobné operace s vektory https://github.com/tisnik/go-root/blob/master/article_B2/vec­tor_round.go
10 vector_broadcast.go naplnění všech prvků vektoru stejnou hodnotou https://github.com/tisnik/go-root/blob/master/article_B2/vec­tor_broadcast.go
       
11 vector_comparison.go porovnání prvků dvou vektorů https://github.com/tisnik/go-root/blob/master/article_B2/vec­tor_comparison.go
12 vector_masked.go základní operace s maskou https://github.com/tisnik/go-root/blob/master/article_B2/vec­tor_masked.go

Seznam všech předchozích částí seriálu a článků o SIMD instrukcích

Podporou SIMD instrukcí na úrovni intrinsic (v céčku, nikoli v jazyku Go) jsme se už na Rootu zabývali, stejně jako samotnými SIMD instrukcemi na úrovni assembleru. Pro úplnost jsou v této příloze uvedeny odkazy na příslušné články:

  1. Užitečné rozšíření GCC: podpora SIMD (vektorových) instrukcí
    https://www.root.cz/clanky/uzitecne-rozsireni-gcc-podpora-simd-vektorovych-instrukci/
  2. Užitečné rozšíření GCC – podpora SIMD (vektorových) instrukcí: nedostatky technologie
    https://www.root.cz/clanky/uzitecne-rozsireni-gcc-podpora-simd-vektorovych-instrukci-nedostatky-technologie/
  3. Podpora SIMD (vektorových) instrukcí na RISCových procesorech
    https://www.root.cz/clanky/podpora-simd-vektorovych-instrukci-na-riscovych-procesorech/
  4. Podpora SIMD operací v GCC s využitím intrinsic pro nízkoúrovňové optimalizace
    https://www.root.cz/clanky/podpora-simd-operaci-v-gcc-s-vyuzitim-intrinsic-pro-nizkourovnove-optimalizace/
  5. Podpora SIMD operací v GCC s využitím intrinsic: technologie SSE
    https://www.root.cz/clanky/podpora-simd-operaci-v-gcc-s-vyuzitim-intrinsic-technologie-sse/
  6. Rozšíření instrukční sady „Advanced Vector Extensions“ na platformě x86–64
    https://www.root.cz/clanky/rozsireni-instrukcni-sady-advanced-vector-extensions-na-platforme-x86–64/
  7. Rozšíření instrukční sady F16C, FMA a AVX-512 na platformě x86–64
    https://www.root.cz/clanky/rozsireni-instrukcni-sady-f16c-fma-a-avx-512-na-platforme-x86–64/
  8. Rozšíření instrukční sady AVX-512 na platformě x86–64 (dokončení)
    https://www.root.cz/clanky/rozsireni-instrukcni-sady-avx-512-na-platforme-x86–64-dokonceni/
  9. SIMD instrukce na platformě 80×86: instrukční sada MMX
    https://www.root.cz/clanky/simd-instrukce-na-platforme-80×86-instrukcni-sada-mmx/
  10. SIMD instrukce na 80×86: dokončení popisu MMX, instrukce 3DNow!
    https://www.root.cz/clanky/simd-instrukce-na-80–86-dokonceni-popisu-mmx-instrukce-3dnow/
  11. SIMD instrukce v rozšíření SSE
    https://www.root.cz/clanky/simd-instrukce-v-rozsireni-sse/
  12. SIMD instrukce v rozšíření SSE (2. část)
    https://www.root.cz/clanky/simd-instrukce-v-rozsireni-sse-2-cast/
  13. Pokročilejší SSE operace: přeskupení, promíchání a rozbalování prvků vektorů
    https://www.root.cz/clanky/po­krocilejsi-sse-operace-preskupeni-promichani-a-rozbalovani-prvku-vektoru/
  14. Od instrukční sady SSE k sadě SSE2
    https://www.root.cz/clanky/od-instrukcni-sady-sse-k-sade-sse2/
  15. Instrukční sady SIMD a automatické vektorizace prováděné překladačem GCC
    https://www.root.cz/clanky/instrukcni-sady-simd-a-automaticke-vektorizace-provadene-prekladacem-gcc/
  16. Instrukční sady SIMD a automatické vektorizace prováděné překladačem GCC (2)
    https://www.root.cz/clanky/instrukcni-sady-simd-a-automaticke-vektorizace-provadene-prekladacem-gcc-2/

Odkazy na Internetu

  1. Go 1.27 Release Notes
    https://go.dev/doc/go1.27
  2. New experimental simd package
    https://go.dev/doc/go1.27#simd
  3. Balíček simd
    https://pkg.go.dev/simd
  4. Balíček archsimd
    https://pkg.go.dev/simd/archsimd
  5. Experimental SIMD with simd/archsimd
    https://go-cookbook.com/snippets/per­formance/experimental-simd-archsimd
  6. Go Generic Methods: A Hands-On Go 1.27 Tutorial
    https://www.danilchenko.dev/posts/go-generic-methods/
  7. Auto-vectorization in GCC
    https://gcc.gnu.org/projects/tree-ssa/vectorization.html
  8. GCC documentation: Extensions to the C Language Family
    https://gcc.gnu.org/onlinedocs/gcc/C-Extensions.html#C-Extensions
  9. GCC documentation: Using Vector Instructions through Built-in Functions
    https://gcc.gnu.org/online­docs/gcc/Vector-Extensions.html
  10. SSE (Streaming SIMD Extentions)
    http://www.songho.ca/misc/sse/sse­.html
  11. Timothy A. Chagnon: SSE and SSE2
    http://www.cs.drexel.edu/~tc365/mpi-wht/sse.pdf
  12. Intel corporation: Extending the Worldr's Most Popular Processor Architecture
    http://download.intel.com/techno­logy/architecture/new-instructions-paper.pdf
  13. SIMD architectures:
    http://arstechnica.com/ol­d/content/2000/03/simd.ar­s/
  14. Tour of the Black Holes of Computing!: Floating Point
    http://www.cs.hmc.edu/~ge­off/classes/hmc.cs105…/sli­des/class02_floats.ppt
  15. 3Dnow! Technology Manual
    AMD Inc., 2000
  16. Intel MMXTM Technology Overview
    Intel corporation, 1996
  17. MultiMedia eXtensions
    http://softpixel.com/~cwrig­ht/programming/simd/mmx.phpi
  18. AMD K5 („K5“ / „5k86“)
    http://www.pcguide.com/ref/cpu/fam/g5K5-c.html
  19. Sixth Generation Processors
    http://www.pcguide.com/ref/cpu/fam/g6­.htm
  20. Great Microprocessors of the Past and Present
    http://www.cpushack.com/CPU/cpu1.html
  21. Very long instruction word (Wikipedia)
    http://en.wikipedia.org/wi­ki/Very_long_instruction_word
  22. CPU design (Wikipedia)
    http://en.wikipedia.org/wi­ki/CPU_design
  23. Bulldozer (microarchitecture)
    https://en.wikipedia.org/wi­ki/Bulldozer_(microarchitec­ture)
  24. SIMD Instructions Considered Harmful
    https://www.sigarch.org/simd-instructions-considered-harmful/
  25. GCC Compiler Intrinsics
    https://iq.opengenus.org/gcc-compiler-intrinsics/
  26. Scalable_Vector_Extension_(SVE)
    https://en.wikipedia.org/wi­ki/AArch64#Scalable_Vector_Ex­tension_(SVE)
  27. Improve the Multimedia User Experience
    https://www.arm.com/technologies/neon
  28. NEON Technology (stránky ARM)
    https://developer.arm.com/techno­logies/neon
  29. SIMD Assembly Tutorial: ARM NEON – Xiph.org
    https://people.xiph.org/~tte­rribe/daala/neon_tutorial­.pdf
  30. Ne10
    http://projectne10.github.io/Ne10/
  31. NEON and Floating-Point architecture
    http://infocenter.arm.com/hel­p/index.jsp?topic=/com.ar­m.doc.den0024a/BABIGHEB.html
  32. An Introduction to ARM NEON
    http://peterdn.com/post/an-introduction-to-ARM-NEON.aspx
  33. ARM NEON Intrinsics Reference
    http://infocenter.arm.com/hel­p/topic/com.arm.doc.ihi0073a/I­HI0073A_arm_neon_intrinsic­s_ref.pdf
  34. Arm Neon Intrinsics vs hand assembly
    https://stackoverflow.com/qu­estions/9828567/arm-neon-intrinsics-vs-hand-assembly
  35. ARM NEON Optimization. An Example
    http://hilbert-space.de/?p=22
  36. AArch64 NEON instruction format
    https://developer.arm.com/doc­s/den0024/latest/7-aarch64-floating-point-and-neon/73-aarch64-neon-instruction-format
  37. ARM SIMD instructions
    https://developer.arm.com/do­cumentation/dht0002/a/Intro­ducing-NEON/What-is-SIMD-/ARM-SIMD-instructions
  38. Learn the architecture – Migrate Neon to SVE Version 1.0
    https://developer.arm.com/do­cumentation/102131/0100/?lan­g=en
  39. 1.2.2. Comparison between NEON technology and other SIMD solutions
    https://developer.arm.com/do­cumentation/den0018/a/Intro­duction/Comparison-between-ARM-NEON-technology-and-other-implementations/Comparison-between-NEON-technology-and-other-SIMD-solutions?lang=en
  40. NEON Programmer’s Guide
    https://documentation-service.arm.com/static/63299276e68c6809a6b4­1308
  41. Brain Floating Point – nový formát uložení čísel pro strojové učení a chytrá čidla
    https://www.root.cz/clanky/brain-floating-point-ndash-novy-format-ulozeni-cisel-pro-strojove-uceni-a-chytra-cidla/
  42. Other Built-in Functions Provided by GCC
    https://gcc.gnu.org/online­docs/gcc/Other-Builtins.html
  43. GCC: 6.60 Built-in Functions Specific to Particular Target Machines
    https://gcc.gnu.org/online­docs/gcc/Target-Builtins.html#Target-Builtins
  44. Advanced Vector Extensions
    https://en.wikipedia.org/wi­ki/Advanced_Vector_Extensi­ons
  45. Top 10 Craziest Assembly Language Instructions
    https://www.youtube.com/wat­ch?v=Wz_xJPN7lAY
  46. Intel x86: let's take a look at one of the most complex instruction set!
    https://www.youtube.com/wat­ch?v=KBLy23B38-c
  47. x64 Assembly Tutorial 58: Intro to AVX
    https://www.youtube.com/wat­ch?v=yAvuHd8cBJY
  48. AVX512 (1 of 3): Introduction and Overview
    https://www.youtube.com/watch?v=D-mM6X5×nTY
  49. AVX512 (2 of 3): Programming AVX512 in 3 Different Ways
    https://www.youtube.com/wat­ch?v=I3efQKLgsjM
  50. AVX512 (3 of 3): Deep Dive into AVX512 Mechanisms
    https://www.youtube.com/watch?v=543a1b-cPmU
  51. AVX-512
    https://en.wikipedia.org/wiki/AVX-512
  52. AVX-512
    https://iq.opengenus.org/avx512/
  53. SIMD Algorithms Youtube course
    https://denisyaroshevskiy­.github.io/presentations/
  54. Compiler explorer
    https://godbolt.org/
  55. Restricting pointers
    https://gcc.gnu.org/online­docs/gcc/Restricted-Pointers.html
  56. Does the restrict keyword provide significant benefits in gcc/g++
    https://stackoverflow.com/qu­estions/1965487/does-the-restrict-keyword-provide-significant-benefits-in-gcc-g
  57. Demystifying The Restrict Keyword
    https://cellperformance.be­yond3d.com/articles/2006/05/de­mystifying-the-restrict-keyword.html
  58. Basics of Vectorization for Fortran Applications
    https://inria.hal.science/hal-01688488/document
  59. What does the restrict keyword mean in C++?
    https://stackoverflow.com/qu­estions/776283/what-does-the-restrict-keyword-mean-in-c
  60. restrict keyword (Wikipedia)
    https://en.wikipedia.org/wi­ki/Restrict
  61. Reduction operator
    https://en.wikipedia.org/wi­ki/Reduction_operator
  62. The Power of the Dot Product in Artificial Intelligence
    https://medium.com/data-science/the-power-of-the-dot-product-in-artificial-intelligence-c002331e1829
  63. Can any one explain why dot product is used in neural network and what is the intitutive thought of dot product
    https://stats.stackexchan­ge.com/questions/291680/can-any-one-explain-why-dot-product-is-used-in-neural-network-and-what-is-the-in
  64. Aligned and unaligned memory accesses?
    https://stackoverflow.com/qu­estions/1063809/aligned-and-unaligned-memory-accesses
  65. Go finally gets SIMD in 1.26
    https://hexfusion.io/posts/go-simd-art/
  66. simd/archsimd: architecture-specific SIMD intrinsics under a GOEXPERIMENT #73787
    https://github.com/golang/go/is­sues/73787

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.