Na dvojici článků Dlouho očekávaná podpora SIMD operací v programovacím jazyku Go a Dlouho očekávaná podpora SIMD operací v programovacím jazyku Go (2. část) dnes navážeme a celé téma o podpoře SIMD operací v programovacím jazyku Go dokončíme. Zabývat se budeme popisem (stále ještě experimentálního) balíčku nazvaného simd, který je dostupný v Go verze 1.26 (i když v tomto případě s omezeními) a především pak v Go 1.27.
Co se dozvíte v článku
- Datové typy definované v rámci balíčku simd
- Součet dvou vektorů: rozdíl mezi použitím balíčku simd a simd/archsimd
- Operace prováděné se všemi prvky vektoru
- Operace s dvojicemi vektorů stejného typu
- Porovnání dvou vektorů
- Operace MADD (Multiply-Add)
- Jaké délky vektorů jsou podporovány?
- Dobře, jaká je tedy bitová šířka vektorů?
- Lze nakonfigurovat šířku vektorů?
- Kompatibilita různých typů vektorů
- Základní manipulace s maskami
- Konstrukce masky
- Převod masky na plnohodnotný vektor
- Logické operace prováděné s dvojicí masek
- Výběr prvků z jednoho vektoru na základě masky
- Výběr prvků z dvojice vektorů na základě masky
- Způsob překladu do strojového kódu
- Repositář s demonstračními příklady
- Odkazy na Internetu
Balíček simd nabízí SIMD operace prováděné s vektory, jejichž bitová šířka (a tím i počet prvků) je odvozena z možností použité architektury mikroprocesorů. To tedy znamená, že například u mikroprocesorů s podporou AVX či AVX-2 budou podporovány vektory o šířce 256 bitů, zatímco v případě AVX-512 bude možné použít vektory o šířce 512 bitů. A samozřejmě je možné v runtime zjistit, jaká je skutečná šířka vektorů.
Datové typy definované v rámci balíčku simd
V balíčku simd je definováno deset datových typů představujících vektory o určité délce (k tomu se ještě vrátíme) v nichž jsou uloženy prvky různých typů. Povšimněte si, že názvy těchto typů (na rozdíl od typů definovaných v balíčku simd/archsimd) neobsahují specifikaci počtu uložených prvků, ale pouze jejich typy:
| Jméno typu | Stručný popis typu |
|---|---|
| Float32s | vektor obsahující prvky typu float32 (single) |
| Float64s | vektor obsahující prvky typu float64 (double) |
| Int8s | vektor obsahující prvky typu int8 |
| Int16s | vektor obsahující prvky typu int16 |
| Int32s | vektor obsahující prvky typu int32 |
| Int64s | vektor obsahující prvky typu int64 |
| Uint8s | vektor obsahující prvky typu uint8 |
| Uint16s | vektor obsahující prvky typu uint16 |
| Uint32s | vektor obsahující prvky typu uint32 |
| Uint64s | vektor obsahující prvky typu uint64 |
Další čtyři datové typy reprezentují takzvané masky s uloženými bity resp. z pohledu vývojáře s pravdivostními hodnotami. Masky jsou používány v některých operacích, s nimiž se seznámíme v navazujících kapitolách. Jednotlivé typy reprezentující masky se liší podle toho, s jakými vektory je možné je použít:
| Jméno typu | Lze použít s vektory |
|---|---|
| Mask8s | Int8s, Uint8s |
| Mask16s | Int16s, Uint16s |
| Mask32s | Int32s, Uint32s, Float32s |
| Mask64s | Int64s, Uint64s, Float64s |
Součet dvou vektorů: rozdíl mezi použitím balíčku simd a simd/archsimd
Praktickou část dnešního článku začneme typickým demonstračním příkladem, který se velmi často používá při vysvětlování SIMD operací. Pokusíme se o provedení součtu dvou vektorů stejného typu. Součet vektorů je realizován snadno – je proveden součtem odpovídajících si prvků vektorů. Připomeňme si, že v případě přímého použití balíčku simd/archsimd může realizace této úlohy vypadat následovně:
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)
}
Přepis výše uvedeného demonstračního příkladu do takové podoby, aby se používal balíček simd, je snadný. Struktura celého příkladu zůstane prakticky stejná, pouze se při převodu řezu (původně pole) na vektor namísto metody LoadFloat32×8Array použije metoda nazvaná LoadFloat32s. A pochopitelně se pracuje s odlišnými datovými typy, což je však ve zdrojovém kódu do značné míry skryto díky tomu, že se využívá typová inference:
package main
import (
"fmt"
"simd"
)
func main() {
// konstrukce a inicializace řezu
s1 := []float32{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0}
s2 := []float32{0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5}
// výpis obsahu řezů
fmt.Println("s1 ", s1)
fmt.Println("s2 ", s2)
// převod řezu na vektor
v1 := simd.LoadFloat32s(s1)
v2 := simd.LoadFloat32s(s2)
// SIMD operace součtu prvků vektorů
v3 := v1.Add(v2)
// konstrukce řezu
result := make([]float32, v3.Len())
// zápis prvků vektoru do řezu
v3.Store(result)
// výpis obsahu řezu
fmt.Println("s1+s2", result)
}
Ověříme si výsledky výpočtu součtu vektorů:
$ ./go run float32_add.go s1 [1 2 3 4 5 6 7 8] s2 [0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5] s1+s2 [1.5 2.5 3.5 4.5 5.5 6.5 7.5 8.5]
Operace prováděné se všemi prvky vektoru
S vektory lze provádět i různé unární operace (tedy operace s jediným operandem), přičemž každá taková operace se volá jako metoda vektoru a výsledkem je nový vektor. Připomeňme si, že v balíčku simd/archsimd je podporováno celkem pět unárních operací s prvky vektorů:
| 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 balíčku simd z nějakého důvodu chybí operace pro výpočet převrácené hodnoty a kombinace výpočtu převrácené hodnoty z odmocniny hodnoty prvku, takže nám zbývá pouze:
| Metoda | Stručný popis |
|---|---|
| Abs | absolutní hodnoty všech prvků vektoru |
| Neg | změna znaménka všech prvků vektoru |
| Sqrt | druhá odmocnina všech prvků vektoru |
Všechny tři výše zmíněné operace jsou použity v následujícím demonstračním příkladu:
package main
import (
"fmt"
"simd"
)
func main() {
// konstrukce a inicializace řezu
s := []float32{-4.0, -3.0, -2.0, -1.0, 1.0, 2.0, 3.0, 4.0}
// výpis obsahu řezu
fmt.Println("s ", s)
// převod řezu na vektor
v := simd.LoadFloat32s(s)
// konstrukce řezu pro uložení výsledků
result := make([]float32, v.Len())
// SIMD operace výpočtu absolutní hodnoty
v2 := v.Abs()
// zápis prvků vektoru do řezu
v2.Store(result)
// výpis obsahu řezu
fmt.Println("abs(s) ", result)
// SIMD operace výpočtu hodnoty s opačným znaménkem
v3 := v.Neg()
// zápis prvků vektoru do řezu
v3.Store(result)
// výpis obsahu řezu
fmt.Println("neg(s) ", result)
// SIMD operace výpočtu druhé odmocniny
v4 := v.Sqrt()
// zápis prvků vektoru do řezu
v4.Store(result)
// výpis obsahu řezu
fmt.Println("sqrt(s)", result)
}
Samozřejmě si zkontrolujeme i způsob výpočtu:
$ go run float32_unary.go s [-4 -3 -2 -1 1 2 3 4] abs(s) [4 3 2 1 1 2 3 4] neg(s) [4 3 2 1 -1 -2 -3 -4] sqrt(s) [NaN NaN NaN NaN 1 1.4142135 1.7320508 2]
Operace s dvojicemi vektorů stejného typu
Ve stručnosti si taktéž ukážeme další binární operace prováděné s odpovídajícími si prvky dvou vektorů (prozatím jsme si totiž ukázali pouze operaci součtu). Tyto operace jsou opět dostupné přes metody, které mají jeden parametr (druhý vektor) a návratovou hodnotou je nový vektor stejného typu i délky 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 |
Realizace těchto operací je snadná, což je ostatně patrné i při pohledu na zdrojový kód dalšího demonstračního příkladu:
package main
import (
"fmt"
"simd"
)
func main() {
// konstrukce a inicializace řezu
s1 := []float32{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0}
s2 := []float32{0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5}
// výpis obsahu řezů
fmt.Println("s1 ", s1)
fmt.Println("s2 ", s2)
// převod řezu na vektor
v1 := simd.LoadFloat32s(s1)
v2 := simd.LoadFloat32s(s2)
// konstrukce řezu pro uložení výsledků
result := make([]float32, v1.Len())
// SIMD operace součtu prvků vektorů
v3 := v1.Add(v2)
// zápis prvků vektoru do řezu
v3.Store(result)
// výpis obsahu řezu
fmt.Println("s1+s2 ", result)
// SIMD operace rozdílu prvků vektorů
v4 := v1.Sub(v2)
// zápis prvků vektoru do řezu
v4.Store(result)
// výpis obsahu řezu
fmt.Println("s1-s2 ", result)
// SIMD operace součinu prvků vektorů
v5 := v1.Mul(v2)
// zápis prvků vektoru do řezu
v5.Store(result)
// výpis obsahu řezu
fmt.Println("s1*s2 ", result)
// SIMD operace podílu prvků vektorů
v6 := v1.Div(v2)
// zápis prvků vektoru do řezu
v6.Store(result)
// výpis obsahu řezu
fmt.Println("s1/s2 ", result)
// SIMD operace výběru větších prvků vektorů
v7 := v1.Max(v2)
// zápis prvků vektoru do řezu
v7.Store(result)
// výpis obsahu řezu
fmt.Println("max(s1/s2)", result)
// SIMD operace výběru menších prvků vektorů
v8 := v1.Min(v2)
// zápis prvků vektoru do řezu
v8.Store(result)
// výpis obsahu řezu
fmt.Println("min(s1/s2)", result)
}
Ověříme si výsledky všech implementovaných binárních operací:
$ ./go run float32_binary.go s1 [1 2 3 4 5 6 7 8] s2 [0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5] s1+s2 [1.5 2.5 3.5 4.5 5.5 6.5 7.5 8.5] s1-s2 [0.5 1.5 2.5 3.5 4.5 5.5 6.5 7.5] s1*s2 [0.5 1 1.5 2 2.5 3 3.5 4] s1/s2 [2 4 6 8 10 12 14 16] max(s1/s2) [1 2 3 4 5 6 7 8] min(s1/s2) [0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5]
Pro zajímavost si ještě ověřme operaci podílu nulou (včetně 0/0):
package main
import (
"fmt"
"simd"
)
func main() {
// konstrukce a inicializace řezu
s1 := []float32{0.0, 1.0, -1.0, 1.0, 0.0, 1.0, -1.0, 0.0}
s2 := []float32{1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0}
// výpis obsahu řezů
fmt.Println("s1 ", s1)
fmt.Println("s2 ", s2)
// převod řezu na vektor
v1 := simd.LoadFloat32s(s1)
v2 := simd.LoadFloat32s(s2)
// SIMD operace podílu prvků vektorů
v3 := v1.Div(v2)
// konstrukce řezu
result := make([]float32, v3.Len())
// zápis prvků vektoru do řezu
v3.Store(result)
// výpis obsahu řezu
fmt.Println("s1+s2", result)
}
Ani v tomto případě nedojde k běhové chybě, ale příslušné prvky vektoru s výsledky budou obsahovat buď kladné či záporné nekonečno, nebo v případě podílu 0/0 hodnotu NaN:
$ go run float32_div.go s1 [0 1 -1 1 0 1 -1 0] s2 [1 1 1 1 0 0 0 0] s1+s2 [0 1 -1 1 NaN +Inf -Inf NaN]
Porovnání dvou vektorů
Odpovídající si prvky dvou vektorů stejného typu je možné porovnat. Výsledkem porovnání je sada pravdivostních hodnot, která se uloží do masky (těmi se budeme podrobněji zabývat v navazujících kapitolách). V dalším demonstračním příkladu jsou nejprve zkonstruovány dva vektory, z nichž každý obsahuje osm prvků typu float32. Odpovídající si prvky těchto vektorů jsou porovnány s využitím zvolené relační operace (rovno, nerovno, menší než, menší nebo rovno atd.) a výsledných osm hodnot typu bool je uloženo do masky. Obsah této masky je posléze vypsán (a to pochopitelně pro každou provedenou relační operaci):
package main
import (
"fmt"
"simd"
)
func main() {
// konstrukce a inicializace řezu
s1 := []float32{1, 2, 3, 4, 5, 6, 7, 8}
s2 := []float32{4, 4, 4, 4, 4, 4, 4, 4}
// výpis obsahu řezů
fmt.Println("s1 ", s1)
fmt.Println("s2 ", s2)
// převod řezu na vektor
v1 := simd.LoadFloat32s(s1)
v2 := simd.LoadFloat32s(s2)
// SIMD operace porovnání prvků vektorů
v3 := v1.Equal(v2)
// výpis obsahu řezu
fmt.Println("s1==s2", v3.String())
// SIMD operace porovnání prvků vektorů
v4 := v1.Less(v2)
// výpis obsahu řezu
fmt.Println("s1<s2 ", v4.String())
// SIMD operace porovnání prvků vektorů
v5 := v1.LessEqual(v2)
// výpis obsahu řezu
fmt.Println("s1<=s2", v5.String())
// SIMD operace porovnání prvků vektorů
v6 := v1.Greater(v2)
// výpis obsahu řezu
fmt.Println("s1>s2 ", v6.String())
// SIMD operace porovnání prvků vektorů
v7 := v1.GreaterEqual(v2)
// výpis obsahu řezu
fmt.Println("s1>=s2", v7.String())
// SIMD operace porovnání prvků vektorů
v8 := v1.NotEqual(v2)
// výpis obsahu řezu
fmt.Println("s1<>s2", v8.String())
}
Z vypsaných výsledků je patrné, jakým způsobem je porovnání dvou vektorů provedeno:
$ go run float32_comparison.go
s1 [1 2 3 4 5 6 7 8]
s2 [4 4 4 4 4 4 4 4]
s1==s2 {0,0,0,1,0,0,0,0}
s1<s2 {1,1,1,0,0,0,0,0}
s1<=s2 {1,1,1,1,0,0,0,0}
s1>s2 {0,0,0,0,1,1,1,1}
s1>=s2 {0,0,0,1,1,1,1,1}
s1<>s2 {1,1,1,0,1,1,1,1}
Operace MADD (Multiply-Add)
V předchozím článku jsme se mj. zmínili i o operaci multiply-add, kterou lze využít například při implementaci konvolučních filtrů pro zpracování signálů atd. I tuto operaci balíček simd podporuje, ovšem pouze pro vektory, které obsahují prvky typu float32 či float64. Operace multiply-add je ternární, tj. vyžaduje tři operandy. Prvním operandem je příjemce (receiver), tedy struktura, jejíž metodu voláme. A další dva parametry jsou této metodě předány při jejím volání:
package main
import (
"fmt"
"simd"
)
func main() {
// konstrukce a inicializace řezu
s1 := []float32{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0}
s2 := []float32{2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0}
s3 := []float32{0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5}
// výpis obsahu řezů
fmt.Println("s1 ", s1)
fmt.Println("s2 ", s2)
fmt.Println("s3 ", s3)
// převod řezu na vektor
v1 := simd.LoadFloat32s(s1)
v2 := simd.LoadFloat32s(s2)
v3 := simd.LoadFloat32s(s3)
// SIMD operace multiply-add
v4 := v1.MulAdd(v2, v3)
// konstrukce řezu
result := make([]float32, v4.Len())
// zápis prvků vektoru do řezu
v4.Store(result)
// výpis obsahu řezu
fmt.Println("s1*s2+s3", result)
}
Samozřejmě si opět ověříme, jak je výpočet proveden:
$ go run float32_mul_add.go s1 [1 2 3 4 5 6 7 8] s2 [2 2 2 2 2 2 2 2] s3 [0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5] s1*s2+s3 [2.5 4.5 6.5 8.5 10.5 12.5 14.5 16.5]
Jaké délky vektorů jsou podporovány?
Pokusme se zjistit, jaké délky vektorů jsou podporovány. Nejdříve začneme vektory s délkou devíti prvků:
package main
import (
"fmt"
"simd"
)
func main() {
// konstrukce a inicializace řezu
s1 := []float32{0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0}
s2 := []float32{0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5}
// výpis obsahu řezů
fmt.Println("s1 ", s1)
fmt.Println("s2 ", s2)
// převod řezu na vektor
v1 := simd.LoadFloat32s(s1)
v2 := simd.LoadFloat32s(s2)
// výpis délky vektorů
fmt.Println("len(v1)=", v1.Len())
fmt.Println("len(v2)=", v2.Len())
// SIMD operace součtu prvků vektorů
v3 := v1.Add(v2)
// konstrukce řezu
result := make([]float32, v3.Len())
// zápis prvků vektoru do řezu
v3.Store(result)
// výpis obsahu řezu
fmt.Println("s1+s2", result)
}
Tento příklad bude možné přeložit a dokonce i spustit bez chyby. Ovšem ve skutečnosti se bude zpracovávat pouze prvních osm prvků vektorů!:
$ go run float32_length_1.go s1 [0 1 2 3 4 5 6 7 8] s2 [0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5] len(v1)= 8 len(v2)= 8 s1+s2 [0.5 1.5 2.5 3.5 4.5 5.5 6.5 7.5]
Nyní vektory zkrátíme na čtyři prvky:
package main
import (
"fmt"
"simd"
)
func main() {
// konstrukce a inicializace řezu
s1 := []float32{5.0, 6.0, 7.0, 8.0}
s2 := []float32{0.5, 0.5, 0.5, 0.5}
// výpis obsahu řezů
fmt.Println("s1 ", s1)
fmt.Println("s2 ", s2)
// převod řezu na vektor
v1 := simd.LoadFloat32s(s1)
v2 := simd.LoadFloat32s(s2)
// výpis délky vektorů
fmt.Println("len(v1)=", v1.Len())
fmt.Println("len(v2)=", v2.Len())
// SIMD operace součtu prvků vektorů
v3 := v1.Add(v2)
// konstrukce řezu
result := make([]float32, v3.Len())
// zápis prvků vektoru do řezu
v3.Store(result)
// výpis obsahu řezu
fmt.Println("s1+s2", result)
}
Tento příklad bude možné přeložit (bez chyby), ovšem po spuštění dojde k pádu, protože se očekává, že se minimální délka vektorů s prvky typu float32 bude rovna osmi:
$ go run float32_length_2.go
s1 [5 6 7 8]
s2 [0.5 0.5 0.5 0.5]
panic: runtime error: cannot convert slice with length 4 to array or pointer to array with length 8
goroutine 1 [running]:
simd/archsimd.LoadFloat32x8(...)
/tmp/ramdisk/go/src/simd/archsimd/slice_gen_amd64.go:228
simd/internal/bridge.LoadFloat32x8(...)
/tmp/ramdisk/go/src/simd/internal/bridge/decls_amd64.go:42
main.main@simd256()
/tmp/ramdisk/go/bin/float32_length_2.go:18 +0x153
main.main()
/tmp/ramdisk/go/bin/float32_length_2.go:8 +0x65
exit status 2
Dobře, prozatím to vypadá, že lze zpracovat vektory s délkou osmi prvků. Pokusme se ještě tuto délku zdvojnásobit, což by teoreticky mohlo být možné (minimálně bych od vysokoúrovňové knihovny očekával):
package main
import (
"fmt"
"simd"
)
func main() {
// konstrukce a inicializace řezu
s1 := []float32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
s2 := []float32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
// výpis obsahu řezů
fmt.Println("s1 ", s1)
fmt.Println("s2 ", s2)
// převod řezu na vektor
v1 := simd.LoadFloat32s(s1)
v2 := simd.LoadFloat32s(s2)
// výpis délky vektorů
fmt.Println("len(v1)=", v1.Len())
fmt.Println("len(v2)=", v2.Len())
// SIMD operace součtu prvků vektorů
v3 := v1.Add(v2)
// konstrukce řezu
result := make([]float32, v3.Len())
// zápis prvků vektoru do řezu
v3.Store(result)
// výpis obsahu řezu
fmt.Println("s1+s2", result)
}
Program sice nezhavaruje, ale bude počítat pouze s osmiprvkovými vektory:
$ go run float32_length_3.go s1 [1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16] s2 [1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16] len(v1)= 8 len(v2)= 8 s1+s2 [2 4 6 8 10 12 14 16]
Dobře, jaká je tedy bitová šířka vektorů?
Skutečnou délku vektorů lze vypočítat z jejich bitové šířky. Ta je závislá na použité architektuře a dá se zjistit (v čase běhu) funkcí simd.VectorBitSize:
package main
import (
"fmt"
"simd"
)
func main() {
// výpis bitové šířky vektorů
fmt.Println("vector bit size=", simd.VectorBitSize())
}
Na mém počítači s rozšířením instrukční sady AVX a AVX-2 dostanu hodnotu 256 bitů:
$ go run vector_bit_size.go vector bit size= 256
Lze nakonfigurovat šířku vektorů?
Šířku vektorů, s nimiž se pracuje, je možné specifikovat nastavením proměnné prostředí GODEBUG. Zmenšení šířky na 128 bitů je podporováno a ovlivní všechny operace s vektory:
$ GODEBUG=simd=128 go run vector_bit_size.go vector bit size= 128
$ GODEBUG=simd=128 go run float32_length_3.go s1 [1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16] s2 [1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16] len(v1)= 4 len(v2)= 4 s1+s2 [2 4 6 8]
Ovšem zvětšení šířky na architektuře, která takové vektory nepodporuje, možné není:
$ GODEBUG=simd=512 go run vector_bit_size.go
panic: Requested GODEBUG=gosimd=512 is larger than the simd length (256) supported on this cpu
goroutine 1 [running]:
simd.init.0()
/tmp/ramdisk/go/src/simd/midway_common.go:95 +0x3ae
exit status 2
Teoreticky je sice možné nastavit libovolnou šířku menší než je šířka nativní, ovšem to je cesta do nepěkných míst, jak si můžeme snadno ověřit:
$ GODEBUG=simd=127 go run vector_bit_size.go vector bit size= 127
$ GODEBUG=simd=127 go run float32_add.go
panic: unsupported vector size in simd-rewritten code
goroutine 1 [running]:
main.main()
/tmp/ramdisk/go/bin/float32_add.go:8 +0x7e
exit status 2
Kompatibilita různých typů vektorů
Vzhledem k tomu, jakým způsobem se pracuje s délkou vektorů, bude zajímavé zjistit, které vektory jsou vlastně kompatibilní z pohledu operací, jenž s nimi lze provádět. Příkladem může být pokus o součet dvou vektorů, přičemž první vektor má prvky typu float32 a druhý vektor prvky typu float64. Jazyk Go neumožňuje ani součet skalárních hodnot těchto dvou typů, takže není divu, že taková operace není rozšiřitelná ani na vektory:
package main
import "fmt"
func main() {
var x float32 = 1.2
var y float64 = 3.4
z := x + y
fmt.Println(z)
}
t.go:8:7: invalid operation: x + y (mismatched types float32 and float64)
Pokusme se nejprve z řezu s prvky typu float64 vytvořit vektor typu Float32s:
package main
import (
"fmt"
"simd"
)
func main() {
// konstrukce a inicializace řezu (odlišné typy)
s1 := []float32{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0}
s2 := []float64{0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5}
// výpis obsahu řezů
fmt.Println("s1 ", s1)
fmt.Println("s2 ", s2)
// převod řezu na vektor
v1 := simd.LoadFloat32s(s1)
v2 := simd.LoadFloat32s(s2)
// SIMD operace součtu prvků vektorů
v3 := v1.Add(v2)
// konstrukce řezu
result := make([]float32, v3.Len())
// zápis prvků vektoru do řezu
v3.Store(result)
// výpis obsahu řezu
fmt.Println("s1+s2", result)
}
Překladač pochopitelně odhalí chybu, protože do funkce LoadFloat32s předáváme řez odlišného typu:
./vector_compatibility_1.go:19:26: cannot use s2 (variable of type []float64) as []float32 value in argument to simd.LoadFloat32s
Zkusme tedy provést korektní převod druhého řezu na vektor typu Float64s a až poté se pokusit o součet vektorů:
package main
import (
"fmt"
"simd"
)
func main() {
// konstrukce a inicializace řezu (odlišné typy)
s1 := []float32{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0}
s2 := []float64{0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5}
// výpis obsahu řezů
fmt.Println("s1 ", s1)
fmt.Println("s2 ", s2)
// převod řezu na vektor
v1 := simd.LoadFloat32s(s1)
v2 := simd.LoadFloat64s(s2)
// SIMD operace součtu prvků vektorů
v3 := v1.Add(v2)
// konstrukce řezu
result := make([]float32, v3.Len())
// zápis prvků vektoru do řezu
v3.Store(result)
// výpis obsahu řezu
fmt.Println("s1+s2", result)
}
Ani tato operace není podporována a chyba je zjištěna již v době překladu:
./vector_compatibility_2.go:22:15: cannot use v2 (variable of struct type simd.Float64s) as simd.Float32s value in argument to v1.Add
Naproti tomu příklad, který (zdánlivě) sčítá vektory různé délky, ovšem s prvky totožného typu, bude přeložen bez problémů:
package main
import (
"fmt"
"simd"
)
func main() {
// konstrukce a inicializace řezu (odlišné typy)
s1 := []float32{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0}
s2 := []float32{0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5}
// výpis obsahu řezů
fmt.Println("s1 ", s1)
fmt.Println("s2 ", s2)
// převod řezu na vektor
v1 := simd.LoadFloat32s(s1)
v2 := simd.LoadFloat32s(s2)
// výpis délky vektorů
fmt.Println("len(v1)=", v1.Len())
fmt.Println("len(v2)=", v2.Len())
// SIMD operace součtu prvků vektorů
v3 := v1.Add(v2)
// konstrukce řezu
result := make([]float32, v3.Len())
// zápis prvků vektoru do řezu
v3.Store(result)
// výpis obsahu řezu
fmt.Println("s1+s2", result)
}
V runtime ovšem zjistíme, že oba vektory jsou limitovány na pouhých osm prvků, takže se druhá polovina prvního řezu vůbec nebere v úvahu:
$ go run vector_compatibility_3.go s1 [1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16] s2 [0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5] len(v1)= 8 len(v2)= 8 s1+s2 [1.5 2.5 3.5 4.5 5.5 6.5 7.5 8.5]
Základní manipulace s maskami
Některé operace s vektory využívají takzvané masky, které taktéž můžeme považovat za vektory (s pevným počtem prvků), jenž ovšem obsahují pouze pravdivostní hodnoty. Interně ovšem mohou být uloženy formou bitového pole. 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ů:
func (x Float32s) Equal(y Float32s) Mask32s func (x Float32s) Greater(y Float32s) Mask32s func (x Float32s) GreaterEqual(y Float32s) Mask32s func (x Float32s) Less(y Float32s) Mask32s func (x Float32s) LessEqual(y Float32s) Mask32s func (x Float32s) NotEqual(y Float32s) Mask32s func (x Float64s) Equal(y Float64s) Mask64s func (x Float64s) Greater(y Float64s) Mask64s func (x Float64s) GreaterEqual(y Float64s) Mask64s func (x Float64s) Less(y Float64s) Mask64s func (x Float64s) LessEqual(y Float64s) Mask64s func (x Float64s) NotEqual(y Float64s) Mask64s func (x Int16s) Equal(y Int16s) Mask16s func (x Int16s) Greater(y Int16s) Mask16s func (x Int16s) GreaterEqual(y Int16s) Mask16s func (x Int16s) Less(y Int16s) Mask16s func (x Int16s) LessEqual(y Int16s) Mask16s func (x Int16s) NotEqual(y Int16s) Mask16s func (x Int32s) Equal(y Int32s) Mask32s func (x Int32s) Greater(y Int32s) Mask32s func (x Int32s) GreaterEqual(y Int32s) Mask32s func (x Int32s) Less(y Int32s) Mask32s func (x Int32s) LessEqual(y Int32s) Mask32s func (x Int32s) NotEqual(y Int32s) Mask32s func (x Int64s) Equal(y Int64s) Mask64s func (x Int64s) Greater(y Int64s) Mask64s func (x Int64s) GreaterEqual(y Int64s) Mask64s func (x Int64s) Less(y Int64s) Mask64s func (x Int64s) LessEqual(y Int64s) Mask64s func (x Int64s) NotEqual(y Int64s) Mask64s func (x Int8s) Equal(y Int8s) Mask8s func (x Int8s) Greater(y Int8s) Mask8s func (x Int8s) GreaterEqual(y Int8s) Mask8s func (x Int8s) Less(y Int8s) Mask8s func (x Int8s) LessEqual(y Int8s) Mask8s func (x Int8s) NotEqual(y Int8s) Mask8s func (x Uint16s) Equal(y Uint16s) Mask16s func (x Uint16s) Greater(y Uint16s) Mask16s func (x Uint16s) GreaterEqual(y Uint16s) Mask16s func (x Uint16s) Less(y Uint16s) Mask16s func (x Uint16s) LessEqual(y Uint16s) Mask16s func (x Uint16s) NotEqual(y Uint16s) Mask16s func (x Uint32s) Equal(y Uint32s) Mask32s func (x Uint32s) Greater(y Uint32s) Mask32s func (x Uint32s) GreaterEqual(y Uint32s) Mask32s func (x Uint32s) Less(y Uint32s) Mask32s func (x Uint32s) LessEqual(y Uint32s) Mask32s func (x Uint32s) NotEqual(y Uint32s) Mask32s func (x Uint64s) Equal(y Uint64s) Mask64s func (x Uint64s) Greater(y Uint64s) Mask64s func (x Uint64s) GreaterEqual(y Uint64s) Mask64s func (x Uint64s) Less(y Uint64s) Mask64s func (x Uint64s) LessEqual(y Uint64s) Mask64s func (x Uint64s) NotEqual(y Uint64s) Mask64s func (x Uint8s) Equal(y Uint8s) Mask8s func (x Uint8s) NotEqual(y Uint8s) Mask8s
Logické operace nad booleovskými hodnotami uloženými v maskách:
func (x Mask16s) And(y Mask16s) Mask16s func (x Mask16s) Or(y Mask16s) Mask16s func (x Mask16s) ToArch() any func (x Mask16s) ToInt16s() (to Int16s) func (x Mask32s) And(y Mask32s) Mask32s func (x Mask32s) Or(y Mask32s) Mask32s func (x Mask32s) ToArch() any func (x Mask32s) ToInt32s() (to Int32s) func (x Mask64s) And(y Mask64s) Mask64s func (x Mask64s) Or(y Mask64s) Mask64s func (x Mask64s) ToArch() any func (x Mask64s) ToInt64s() (to Int64s) func (x Mask8s) And(y Mask8s) Mask8s func (x Mask8s) Or(y Mask8s) Mask8s func (x Mask8s) ToArch() any func (x Mask8s) ToInt8s() (to Int8s)
A konečně převody masky na tisknutelný řetězec:
func (x Mask16s) String() string func (x Mask32s) String() string func (x Mask64s) String() string func (x Mask8s) String() string
A naopak – pokud máme k dispozici masku, je jí možné využít pro výběr některých prvků z jednoho vektoru nebo dokonce pro kombinaci prvků ze dvou vektorů do jednoho vektoru cílového (je to vlastně jediná ternární operace s vektory, která je v balíčku simd podporována).
func (x Float32s) Masked(mask Mask32s) Float32s func (x Float64s) Masked(mask Mask64s) Float64s func (x Int16s) Masked(mask Mask16s) Int16s func (x Int32s) Masked(mask Mask32s) Int32s func (x Int64s) Masked(mask Mask64s) Int64s func (x Int8s) Masked(mask Mask8s) Int8s func (x Uint16s) Masked(mask Mask16s) Uint16s func (x Uint32s) Masked(mask Mask32s) Uint32s func (x Uint64s) Masked(mask Mask64s) Uint64s func (x Uint8s) Masked(mask Mask8s) Uint8s func (x Float64s) IfElse(mask Mask64s, y Float64s) Float64s func (x Float32s) IfElse(mask Mask32s, y Float32s) Float32s func (x Int16s) IfElse(mask Mask16s, y Int16s) Int16s func (x Int32s) IfElse(mask Mask32s, y Int32s) Int32s func (x Int64s) IfElse(mask Mask64s, y Int64s) Int64s func (x Int8s) IfElse(mask Mask8s, y Int8s) Int8s func (x Uint16s) IfElse(mask Mask16s, y Uint16s) Uint16s func (x Uint32s) IfElse(mask Mask32s, y Uint32s) Uint32s func (x Uint64s) IfElse(mask Mask64s, y Uint64s) Uint64s func (x Uint8s) IfElse(mask Mask8s, y Uint8s) Uint8s
Konstrukce masky
Maska je většinou vytvořena na základě výsledku nějaké relační operace provedené nad odpovídajícími si prvky dvou vektorů, popř. se maska zkonstruuje nějakou logickou operací nad dvojicí již existující dvojice masek. Podívejme se nejdříve na první způsob vytvoření masky s následným převodem masky na tisknutelný řetězec. V demonstračním příkladu se nejdříve zkonstruuje dvojice vektorů s prvky typu float32, následně se odpovídající si prvky vektorů porovnají na relaci „menší než“ a poté je vypsána textová podoba výsledné masky:
package main
import (
"fmt"
"simd"
)
func createMask() simd.Mask32s {
// konstrukce a inicializace řezu
s1 := []float32{1, 2, 3, 4, 5, 6, 7, 8}
s2 := []float32{4, 4, 4, 4, 4, 4, 4, 4}
// převod řezu na vektor
v1 := simd.LoadFloat32s(s1)
v2 := simd.LoadFloat32s(s2)
// SIMD operace porovnání prvků vektorů
return v1.LessEqual(v2)
}
func main() {
mask := createMask()
// výpis obsahu masky
fmt.Println("mask", mask.String())
}
Otestování je snadné a i výsledek je očekávatelný:
$ go run mask_create.go
mask {1,1,1,1,0,0,0,0}
Převod masky na plnohodnotný vektor
Masku je možné převést na vektor metodou, která se jmenuje To<typ_vektor>. Konkrétně se jedná o tyto metody:
func (x Mask8s) ToInt8s() (to Int8s) func (x Mask16s) ToInt16s() (to Int16s) func (x Mask32s) ToInt32s() (to Int32s) func (x Mask64s) ToInt64s() (to Int64s)
To si opět můžeme velmi snadno otestovat, a to konkrétně v případě masky typu Mask32s:
package main
import (
"fmt"
"simd"
)
func createMask() simd.Mask32s {
// konstrukce a inicializace řezu
s1 := []float32{1, 2, 3, 4, 5, 6, 7, 8}
s2 := []float32{4, 4, 4, 4, 4, 4, 4, 4}
// převod řezu na vektor
v1 := simd.LoadFloat32s(s1)
v2 := simd.LoadFloat32s(s2)
// SIMD operace porovnání prvků vektorů
return v1.LessEqual(v2)
}
func main() {
mask := createMask()
vector := mask.ToInt32s()
// konstrukce řezu
result := make([]int32, vector.Len())
// zápis prvků vektoru do řezu
vector.Store(result)
// výpis obsahu řezu
fmt.Println("mask as vector", result)
}
Po spuštění tohoto demonstračního příkladu se vytvoří maska s osmi prvky a ta je následně převedena na plnohodnotný vektor:
$ go run mask_to_vector.go mask as vector [-1 -1 -1 -1 0 0 0 0]
Logické operace prováděné s dvojicí masek
Nad dvojicí masek stejného typu (musí mít stejný počet booleovských hodnot) lze provádět logické operace součtu nebo součinu odpovídajících si hodnot. Kupodivu není podporována operace negace prvků – možná nemá tak velké možnosti využití v praxi. Ovšem podívejme se na způsob použití obou podporovaných operací. Ty budeme aplikovat na dva páry masek, a to z toho důvodu, že je relativně složité zkonstruovat dvě masky, jejichž odpovídající si prvky tvoří všechny možné kombinace hodnot true a false:
package main
import (
"fmt"
"simd"
)
func createFourMasks() (simd.Mask32s, simd.Mask32s, simd.Mask32s, simd.Mask32s) {
// konstrukce a inicializace řezu
s1 := []float32{1, 2, 3, 4, 5, 6, 7, 8}
s2 := []float32{4, 4, 4, 4, 4, 4, 4, 4}
// převod řezu na vektor
v1 := simd.LoadFloat32s(s1)
v2 := simd.LoadFloat32s(s2)
// SIMD operace porovnání prvků vektorů
mask1 := v1.LessEqual(v2)
mask2 := v1.GreaterEqual(v2)
mask3 := v1.Less(v2)
mask4 := v1.Greater(v2)
return mask1, mask2, mask3, mask4
}
func main() {
mask1, mask2, mask3, mask4 := createFourMasks()
// výpis obsahu masek
fmt.Println("mask 1 ", mask1.String())
fmt.Println("mask 2 ", mask2.String())
// operace s maskami
mask5 := mask1.Or(mask2)
fmt.Println("mask 1 | mask2", mask5.String())
mask6 := mask1.And(mask2)
fmt.Println("mask 1 & mask2", mask6.String())
fmt.Println()
// výpis obsahu masek
fmt.Println("mask 3 ", mask3.String())
fmt.Println("mask 4 ", mask4.String())
// operace s maskami
mask7 := mask3.Or(mask4)
fmt.Println("mask 3 | mask4", mask7.String())
mask8 := mask3.And(mask4)
fmt.Println("mask 3 & mask4", mask8.String())
}
Po překladu a spuštění bude zřejmé, že se vlastně jedná o dva příklady, protože se provádí operace logického součtu a součinu nad dvojicí rozdílných párů masek:
$ go run mask_operations.go
mask 1 {1,1,1,1,0,0,0,0}
mask 2 {0,0,0,1,1,1,1,1}
mask 1 | mask2 {1,1,1,1,1,1,1,1}
mask 1 & mask2 {0,0,0,1,0,0,0,0}
mask 3 {1,1,1,0,0,0,0,0}
mask 4 {0,0,0,0,1,1,1,1}
mask 3 | mask4 {1,1,1,0,1,1,1,1}
mask 3 & mask4 {0,0,0,0,0,0,0,0}
Výběr prvků z jednoho vektoru na základě masky
Všechny typy reprezentující vektory v balíčku simd nabízejí metodu nazvanou Masked. Tato metoda slouží k výběru prvků ze zdrojového vektoru a uložení těchto prvků do vektoru cílového (návratová hodnota metody). Pokud má prvek v masce pravdivostní hodnotu nastavenou na true, bude odpovídající prvek zdrojového vektoru zkopírován do vektoru cílového. Pokud má ovšem prvek masky hodnotu false, je odpovídající prvek zdrojového vektoru ignorován a do vektoru cílového se na toto místo zapíše nula:
func (x Float32s) Masked(mask Mask32s) Float32s func (x Float64s) Masked(mask Mask64s) Float64s func (x Int16s) Masked(mask Mask16s) Int16s func (x Int32s) Masked(mask Mask32s) Int32s func (x Int64s) Masked(mask Mask64s) Int64s func (x Int8s) Masked(mask Mask8s) Int8s func (x Uint16s) Masked(mask Mask16s) Uint16s func (x Uint32s) Masked(mask Mask32s) Uint32s func (x Uint64s) Masked(mask Mask64s) Uint64s func (x Uint8s) Masked(mask Mask8s) Uint8s
Otestujme si nyní tuto operaci, konkrétně s maskou typu Mask32s (která je vhodným způsobem naplněna) a zdrojovým vektorem s osmi prvky typu float32:
package main
import (
"fmt"
"simd"
)
func createMask() simd.Mask32s {
// konstrukce a inicializace řezu
s1 := []float32{1, 2, 3, 4, 5, 6, 7, 8}
s2 := []float32{4, 4, 4, 4, 4, 4, 4, 4}
// převod řezu na vektor
v1 := simd.LoadFloat32s(s1)
v2 := simd.LoadFloat32s(s2)
// SIMD operace porovnání prvků vektorů
return v1.GreaterEqual(v2)
}
func main() {
mask := createMask()
// konstrukce a inicializace řezu
s := []float32{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0}
// výpis obsahu řezu
fmt.Println("original", s)
// převod řezu na vektor
v1 := simd.LoadFloat32s(s)
// výběr prvků na základě masky
v2 := v1.Masked(mask)
// konstrukce řezu
result := make([]float32, v2.Len())
// zápis prvků vektoru do řezu
v2.Store(result)
// výpis obsahu řezu
fmt.Println("selected", result)
}
Po překladu a spuštění tohoto demonstračního příkladu je patrné, že se do výsledného vektoru přepsalo posledních pět prvků vektoru zdrojového. A zbývající prvky v cílovém vektoru byly vynulovány:
$ go run mask_selection_1.go original [1 2 3 4 5 6 7 8] selected [0 0 0 4 5 6 7 8]
Výběr prvků z dvojice vektorů na základě masky
V knihovně simd nalezneme ještě jednu velmi užitečnou metodu, která se jmenuje IfElse. Jedná se vlastně o rozšíření možností metody Masked. Dochází zde ke kombinaci prvků dvou vektorů. Na základě hodnoty uložené v masce se vybere buď prvek z vektoru prvního (což je příjemce metody) nebo naopak prvek z vektoru druhého (druhý parametr metody). A opět platí, že tyto metody jsou definovány pro všechny typy vektorů:
func (x Float64s) IfElse(mask Mask64s, y Float64s) Float64s func (x Float32s) IfElse(mask Mask32s, y Float32s) Float32s func (x Int16s) IfElse(mask Mask16s, y Int16s) Int16s func (x Int32s) IfElse(mask Mask32s, y Int32s) Int32s func (x Int64s) IfElse(mask Mask64s, y Int64s) Int64s func (x Int8s) IfElse(mask Mask8s, y Int8s) Int8s func (x Uint16s) IfElse(mask Mask16s, y Uint16s) Uint16s func (x Uint32s) IfElse(mask Mask32s, y Uint32s) Uint32s func (x Uint64s) IfElse(mask Mask64s, y Uint64s) Uint64s func (x Uint8s) IfElse(mask Mask8s, y Uint8s) Uint8s
Vše si opět otestujeme na příkladu dvou vektorů typu Float32s a masky typu Mask32s. Jedná se vlastně o nepatrnou úpravu předchozího demonstračního příkladu tak, aby se prvky výsledného vektoru vybíraly z vektoru v1 i vektoru v2:
package main
import (
"fmt"
"simd"
)
func createMask() simd.Mask32s {
// konstrukce a inicializace řezu
s1 := []float32{1, 2, 3, 4, 5, 6, 7, 8}
s2 := []float32{4, 4, 4, 4, 4, 4, 4, 4}
// převod řezu na vektor
v1 := simd.LoadFloat32s(s1)
v2 := simd.LoadFloat32s(s2)
// SIMD operace porovnání prvků vektorů
return v1.GreaterEqual(v2)
}
func main() {
mask := createMask()
// konstrukce a inicializace řezu
s1 := []float32{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0}
s2 := []float32{99, 99, 99, 99, 99, 99, 99, 99}
// výpis obsahu řezu
fmt.Println("original", s1)
// převod řezu na vektor
v1 := simd.LoadFloat32s(s1)
v2 := simd.LoadFloat32s(s2)
// výběr prvků na základě masky
v3 := v1.IfElse(mask, v2)
// konstrukce řezu
result := make([]float32, v3.Len())
// zápis prvků vektoru do řezu
v3.Store(result)
// výpis obsahu řezu
fmt.Println("selected", result)
}
Z vypsaných výsledků je patrné, že první tři prvky výsledku byly přečteny z vektoru v2, zatímco dalších pět prvků z vektoru v1:
$ go run mask_selection_2.go original [1 2 3 4 5 6 7 8] selected [99 99 99 4 5 6 7 8]
Způsob překladu do strojového kódu
Interně většina funkcí a metod implementovaných v balíčku simd volá funkce či metody z balíčku simd/archsimd, který jsme si již popsali. A tyto funkce a metody jsou typicky transformovány na sérii SIMD instrukcí. Můžeme si to ukázat například na způsobu implementace operace součtu. Po překladu by měl výsledný strojový kód vypadat přibližně takto:
0x0170 00368 (/tmp/ramdisk/go/bin/float32_add.go:15) CALL runtime.convTslice(SB) 0x0175 00373 (/tmp/ramdisk/go/bin/float32_add.go:15) LEAQ type:[]float32(SB), DX 0x017c 00380 (/tmp/ramdisk/go/bin/float32_add.go:15) MOVQ DX, main..autotmp_50+120(SP) 0x0181 00385 (/tmp/ramdisk/go/bin/float32_add.go:15) MOVQ AX, main..autotmp_50+128(SP) 0x0189 00393 (/tmp/ramdisk/go/src/fmt/print.go:307) MOVQ os.Stdout(SB), BX 0x0190 00400 (<unknown line number>) NOP 0x0190 00400 (/tmp/ramdisk/go/src/fmt/print.go:307) LEAQ go:itab.*os.File,io.Writer(SB), AX 0x0197 00407 (/tmp/ramdisk/go/src/fmt/print.go:307) LEAQ main..autotmp_50+104(SP), CX 0x019c 00412 (/tmp/ramdisk/go/src/fmt/print.go:307) MOVL $2, DI 0x01a1 00417 (/tmp/ramdisk/go/src/fmt/print.go:307) MOVL DI, SI 0x01a3 00419 (/tmp/ramdisk/go/src/fmt/print.go:307) PCDATA $1, $3 0x01a3 00419 (/tmp/ramdisk/go/src/fmt/print.go:307) CALL fmt.Fprintln(SB) 0x01a8 00424 (/tmp/ramdisk/go/src/simd/archsimd/slice_gen_amd64.go:108) MOVQ main..autotmp_95+64(SP), DX 0x01ad 00429 (/tmp/ramdisk/go/src/simd/archsimd/slice_gen_amd64.go:108) VMOVDQU (DX), X0 0x01b1 00433 (/tmp/ramdisk/go/src/simd/internal/bridge/decls_amd64.go:3348) MOVQ main..autotmp_96+56(SP), DX 0x01b6 00438 (/tmp/ramdisk/go/src/simd/internal/bridge/decls_amd64.go:3348) VADDPS (DX), X0, X0 0x01ba 00442 (/tmp/ramdisk/go/src/simd/internal/bridge/decls_amd64.go:3348) MOVUPS X0, main..autotmp_97+40(SP) 0x01bf 00447 (/tmp/ramdisk/go/bin/float32_add.go:18) XCHGL AX, AX 0x01c0 00448 (/tmp/ramdisk/go/bin/float32_add.go:22) XCHGL AX, AX
Povšimněte si, že ústřední částí celého kódu je volání SIMD instrukce VADDPS:
0x01b1 00433 (/tmp/ramdisk/go/src/simd/internal/bridge/decls_amd64.go:3348) MOVQ main..autotmp_96+56(SP), DX 0x01b6 00438 (/tmp/ramdisk/go/src/simd/internal/bridge/decls_amd64.go:3348) VADDPS (DX), X0, X0 0x01ba 00442 (/tmp/ramdisk/go/src/simd/internal/bridge/decls_amd64.go:3348) MOVUPS X0, main..autotmp_97+40(SP)
Repositář s demonstračními příklady
Zdrojové kódy všech minule i 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:
Odkazy na Internetu
- Go 1.27 Release Notes
https://go.dev/doc/go1.27 - New experimental simd package
https://go.dev/doc/go1.27#simd - Balíček simd
https://pkg.go.dev/simd - Balíček archsimd
https://pkg.go.dev/simd/archsimd - Experimental SIMD with simd/archsimd
https://go-cookbook.com/snippets/performance/experimental-simd-archsimd - Go Generic Methods: A Hands-On Go 1.27 Tutorial
https://www.danilchenko.dev/posts/go-generic-methods/ - Auto-vectorization in GCC
https://gcc.gnu.org/projects/tree-ssa/vectorization.html - GCC documentation: Extensions to the C Language Family
https://gcc.gnu.org/onlinedocs/gcc/C-Extensions.html#C-Extensions - GCC documentation: Using Vector Instructions through Built-in Functions
https://gcc.gnu.org/onlinedocs/gcc/Vector-Extensions.html - SSE (Streaming SIMD Extentions)
http://www.songho.ca/misc/sse/sse.html - Timothy A. Chagnon: SSE and SSE2
http://www.cs.drexel.edu/~tc365/mpi-wht/sse.pdf - Intel corporation: Extending the Worldr's Most Popular Processor Architecture
http://download.intel.com/technology/architecture/new-instructions-paper.pdf - SIMD architectures:
http://arstechnica.com/old/content/2000/03/simd.ars/ - Tour of the Black Holes of Computing!: Floating Point
http://www.cs.hmc.edu/~geoff/classes/hmc.cs105…/slides/class02_floats.ppt - 3Dnow! Technology Manual
AMD Inc., 2000 - Intel MMXTM Technology Overview
Intel corporation, 1996 - MultiMedia eXtensions
http://softpixel.com/~cwright/programming/simd/mmx.phpi - AMD K5 („K5“ / „5k86“)
http://www.pcguide.com/ref/cpu/fam/g5K5-c.html - Sixth Generation Processors
http://www.pcguide.com/ref/cpu/fam/g6.htm - Great Microprocessors of the Past and Present
http://www.cpushack.com/CPU/cpu1.html - Very long instruction word (Wikipedia)
http://en.wikipedia.org/wiki/Very_long_instruction_word - CPU design (Wikipedia)
http://en.wikipedia.org/wiki/CPU_design - Bulldozer (microarchitecture)
https://en.wikipedia.org/wiki/Bulldozer_(microarchitecture) - SIMD Instructions Considered Harmful
https://www.sigarch.org/simd-instructions-considered-harmful/ - GCC Compiler Intrinsics
https://iq.opengenus.org/gcc-compiler-intrinsics/ - Scalable_Vector_Extension_(SVE)
https://en.wikipedia.org/wiki/AArch64#Scalable_Vector_Extension_(SVE) - Improve the Multimedia User Experience
https://www.arm.com/technologies/neon - 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 - ARM SIMD instructions
https://developer.arm.com/documentation/dht0002/a/Introducing-NEON/What-is-SIMD-/ARM-SIMD-instructions - Learn the architecture – Migrate Neon to SVE Version 1.0
https://developer.arm.com/documentation/102131/0100/?lang=en - 1.2.2. Comparison between NEON technology and other SIMD solutions
https://developer.arm.com/documentation/den0018/a/Introduction/Comparison-between-ARM-NEON-technology-and-other-implementations/Comparison-between-NEON-technology-and-other-SIMD-solutions?lang=en - NEON Programmer’s Guide
https://documentation-service.arm.com/static/63299276e68c6809a6b41308 - 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/ - Other Built-in Functions Provided by GCC
https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html - GCC: 6.60 Built-in Functions Specific to Particular Target Machines
https://gcc.gnu.org/onlinedocs/gcc/Target-Builtins.html#Target-Builtins - Advanced Vector Extensions
https://en.wikipedia.org/wiki/Advanced_Vector_Extensions - Top 10 Craziest Assembly Language Instructions
https://www.youtube.com/watch?v=Wz_xJPN7lAY - Intel x86: let's take a look at one of the most complex instruction set!
https://www.youtube.com/watch?v=KBLy23B38-c - x64 Assembly Tutorial 58: Intro to AVX
https://www.youtube.com/watch?v=yAvuHd8cBJY - AVX512 (1 of 3): Introduction and Overview
https://www.youtube.com/watch?v=D-mM6X5×nTY - AVX512 (2 of 3): Programming AVX512 in 3 Different Ways
https://www.youtube.com/watch?v=I3efQKLgsjM - AVX512 (3 of 3): Deep Dive into AVX512 Mechanisms
https://www.youtube.com/watch?v=543a1b-cPmU - AVX-512
https://en.wikipedia.org/wiki/AVX-512 - AVX-512
https://iq.opengenus.org/avx512/ - SIMD Algorithms Youtube course
https://denisyaroshevskiy.github.io/presentations/ - Compiler explorer
https://godbolt.org/ - Restricting pointers
https://gcc.gnu.org/onlinedocs/gcc/Restricted-Pointers.html - Does the restrict keyword provide significant benefits in gcc/g++
https://stackoverflow.com/questions/1965487/does-the-restrict-keyword-provide-significant-benefits-in-gcc-g - Demystifying The Restrict Keyword
https://cellperformance.beyond3d.com/articles/2006/05/demystifying-the-restrict-keyword.html - Basics of Vectorization for Fortran Applications
https://inria.hal.science/hal-01688488/document - What does the restrict keyword mean in C++?
https://stackoverflow.com/questions/776283/what-does-the-restrict-keyword-mean-in-c - restrict keyword (Wikipedia)
https://en.wikipedia.org/wiki/Restrict - Reduction operator
https://en.wikipedia.org/wiki/Reduction_operator - The Power of the Dot Product in Artificial Intelligence
https://medium.com/data-science/the-power-of-the-dot-product-in-artificial-intelligence-c002331e1829 - Can any one explain why dot product is used in neural network and what is the intitutive thought of dot product
https://stats.stackexchange.com/questions/291680/can-any-one-explain-why-dot-product-is-used-in-neural-network-and-what-is-the-in - Aligned and unaligned memory accesses?
https://stackoverflow.com/questions/1063809/aligned-and-unaligned-memory-accesses - Go finally gets SIMD in 1.26
https://hexfusion.io/posts/go-simd-art/ - simd/archsimd: architecture-specific SIMD intrinsics under a GOEXPERIMENT #73787
https://github.com/golang/go/issues/73787 - Integer overflow
https://en.wikipedia.org/wiki/Integer_overflow - Saturation arithmetic
https://en.wikipedia.org/wiki/Saturation_arithmetic - DPPS — Dot Product of Packed Single Precision Floating-Point Values
https://www.felixcloutier.com/x86/dpps - DPPD — Dot Product of Packed Double Precision Floating-Point Values
https://www.felixcloutier.com/x86/dppd - VDPBF16PS — Dot Product of BF16 Pairs Accumulated Into Packed Single Precision
https://www.felixcloutier.com/x86/vdpbf16ps - 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/
