Dlouho očekávaná podpora SIMD operací v programovacím jazyku Go (dokončení)

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

Sdílet

Autor: Go lang
Budeme se zabývat popisem experimentálního balíčku nazvaného simd. Ten nabízí operace s vektory, jejichž bitová šířka (a tím i počet prvků) je odvozena z možností použité architektury mikroprocesorů: 256 či 512 bitů.

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
  1. Datové typy definované v rámci balíčku simd
  2. Součet dvou vektorů: rozdíl mezi použitím balíčku simd a simd/archsimd
  3. Operace prováděné se všemi prvky vektoru
  4. Operace s dvojicemi vektorů stejného typu
  5. Porovnání dvou vektorů
  6. Operace MADD (Multiply-Add)
  7. Jaké délky vektorů jsou podporovány?
  8. Dobře, jaká je tedy bitová šířka vektorů?
  9. Lze nakonfigurovat šířku vektorů?
  10. Kompatibilita různých typů vektorů
  11. Základní manipulace s maskami
  12. Konstrukce masky
  13. Převod masky na plnohodnotný vektor
  14. Logické operace prováděné s dvojicí masek
  15. Výběr prvků z jednoho vektoru na základě masky
  16. Výběr prvků z dvojice vektorů na základě masky
  17. Způsob překladu do strojového kódu
  18. Repositář s demonstračními příklady
  19. 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ů.

Poznámka: jak si ověříme v dalším textu, není délka vektorů libovolná (a ani není násobkem nějaké základní délky), což může být zpočátku matoucí.

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]
Poznámka: povšimněte si, že pokus o výpočet druhé odmocniny ze záporného čísla nevede k běhové chybě, ale k uložení hodnoty NaN (Not a Number).

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}
Poznámka: poměrně často je vyžadováno provedení porovnání všech prvků vektoru se stejnou hodnotou (například s nulou). V takovém případě lze využít operaci broadcast, která naplní vektor prvky stejné hodnoty. A tento vektor bude vstupovat do operace porovnání, typicky jako druhý operand.

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
Poznámka: to přesně odpovídá, protože 256/sizeof(float32)=256/32=8.

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
Poznámka: povšimněte si, že porovnání na nerovnost u vektorů s osmibitovými celočíselnými hodnotami bez znaménka není podporováno.

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]
Poznámka: proč jsou vytištěny záporné hodnoty? Prvky výsledného vektoru mají všechny bity buď vynulovány nebo naopak nastaveny na samé jedničky. A samé jedničky u celočíselných typů se znaménkem znamenají právě –1 (ve dvojkovém doplňku).

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:

Wazuh školení

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)
Poznámka: na druhou stranu je nutné upozornit na to, že zmenšení bitové šířky vektorů přes proměnnou prostředí nemá vliv na přeložený kód, resp. přesněji řečeno na tu část, která volá vektorové (SIMD) operace. Pouze se změní způsob konverzí řezů na vektory a naopak (ovšem tato část bude možná v dalších verzích jazyka Go modifikována, protože nynější chování není příliš intuitivní).

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:

# 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/simd/test_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/simd/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/simd/vector_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/simd/vector_add_2.go
5 vector_add_3.go součet dvou vektorů typu Float32×8 https://github.com/tisnik/go-root/blob/master/simd/vector_add_3.go
6 vector_add_4.go součet dvou vektorů typu Float32×16 https://github.com/tisnik/go-root/blob/master/simd/vector_add_4.go
       
7 vector_unary.go základní unární operace s vektory https://github.com/tisnik/go-root/blob/master/simd/vector_unary.go
8 vector_binary.go základní binární operace s vektory https://github.com/tisnik/go-root/blob/master/simd/vector_binary.go
9 vector_round.go zaokrouhlení a podobné operace s vektory https://github.com/tisnik/go-root/blob/master/simd/vector_round.go
10 vector_broadcast.go naplnění všech prvků vektoru stejnou hodnotou https://github.com/tisnik/go-root/blob/master/simd/vec­tor_broadcast.go
       
11 vector_comparison.go porovnání prvků dvou vektorů https://github.com/tisnik/go-root/blob/master/simd/vec­tor_comparison.go
12 vector_masked.go základní operace s maskou https://github.com/tisnik/go-root/blob/master/simd/vector_masked.go
       
13 vector_mul_add_1.go základní varianta operace fused multiply-add https://github.com/tisnik/go-root/blob/master/simd/vec­tor_mul_add_1.go
14 vector_mul_add_2.go další varianty operace fused multiply-add https://github.com/tisnik/go-root/blob/master/simd/vec­tor_mul_add_2.go
15 vector_mul_add_3.go další varianty operace fused multiply-add https://github.com/tisnik/go-root/blob/master/simd/vec­tor_mul_add_3.go
       
16 int_vectors_add_1.go součet vektorů s celočíselnými prvky (4 prvky typu int32) https://github.com/tisnik/go-root/blob/master/simd/int_vec­tors_add_1.go
17 int_vectors_add_2.go součet vektorů s celočíselnými prvky (16 prvků typu int8) https://github.com/tisnik/go-root/blob/master/simd/int_vec­tors_add_2.go
18 int_vectors_add_3.go součet vektorů s celočíselnými prvky (32 prvků typu int8) https://github.com/tisnik/go-root/blob/master/simd/int_vec­tors_add_3.go
       
19 int_vectors_add_normal.go součet vektorů s přetečením hodnot https://github.com/tisnik/go-root/blob/master/simd/int_vec­tors_add_normal.go
20 int_vectors_add_saturated.go součet vektorů se saturací https://github.com/tisnik/go-root/blob/master/simd/int_vec­tors_add_saturated.go
21 uint_vectors_add_normal.go součet vektorů s prvky bez znaménka s přetečením hodnot https://github.com/tisnik/go-root/blob/master/simd/uin­t_vectors_add_normal.go
22 uint_vectors_add_saturated.go součet vektorů s prvky bez znaménka se saturací https://github.com/tisnik/go-root/blob/master/simd/uin­t_vectors_add_saturated.go
23 uint_vectors_sub_normal.go rozdíl vektorů s prvky bez znaménka s přetečením hodnot https://github.com/tisnik/go-root/blob/master/simd/uin­t_vectors_sub_normal.go
24 uint_vectors_sub_saturated.go rozdíl vektorů s prvky bez znaménka se saturací https://github.com/tisnik/go-root/blob/master/simd/uin­t_vectors_sub_saturated.go
       
25 dot_product_1.go částečný skalární součin dvou vektorů https://github.com/tisnik/go-root/blob/master/simd/dot_product_1.go
26 dot_product_2.go částečný skalární součin dvou vektorů dvojnásobné délky https://github.com/tisnik/go-root/blob/master/simd/dot_product_2.go
       
27 permute_1.go permutace vektorů s využitím vektorů indexů [0, 1, 2, 3, 4, 5, 6, 7] https://github.com/tisnik/go-root/blob/master/simd/permute_1.go
28 permute_2.go permutace vektorů s využitím vektorů indexů [7, 6, 5, 4, 3, 2, 1, 0] https://github.com/tisnik/go-root/blob/master/simd/permute_2.go
29 permute_3.go permutace vektorů s využitím vektorů indexů [0, 0, 7, 7, 0, 0, 7, 7] https://github.com/tisnik/go-root/blob/master/simd/permute_3.go
       
30 float32_add.go součet prvků vektorů https://github.com/tisnik/go-root/blob/master/simd/float32_add.go
31 float32_sub.go rozdíl prvků vektorů https://github.com/tisnik/go-root/blob/master/simd/float32_sub.go
32 float32_div.go podíl prvků vektorů (dělení nulou) https://github.com/tisnik/go-root/blob/master/simd/float32_div.go
33 float32_mul_add.go operace multiply-add s dvojicí vektorů https://github.com/tisnik/go-root/blob/master/simd/flo­at32_mul_add.go
       
34 float32_unary.go unární operace nad prvky jednoho vektoru https://github.com/tisnik/go-root/blob/master/simd/float32_unary.go
35 float32_binary.go binární operace nad prvky dvou vektorů https://github.com/tisnik/go-root/blob/master/simd/flo­at32_binary.go
36 float32_comparison.go porovnání prvků dvou vektorů https://github.com/tisnik/go-root/blob/master/simd/flo­at32_comparison.go
       
37 float32_length_1.go zjištění, jaké délky vektorů jsou podporovány, první příklad https://github.com/tisnik/go-root/blob/master/simd/flo­at32_length_1.go
38 float32_length_2.go zjištění, jaké délky vektorů jsou podporovány, druhý příklad https://github.com/tisnik/go-root/blob/master/simd/flo­at32_length_2.go
39 float32_length_3.go zjištění, jaké délky vektorů jsou podporovány, třetí příklad https://github.com/tisnik/go-root/blob/master/simd/flo­at32_length_3.go
       
39 vector_bit_size.go přečtení bitové šířky vektorů v runtime https://github.com/tisnik/go-root/blob/master/simd/vec­tor_bit_size.go
       
40 vector_compatibility_1.go test kompatibility různých typů vektorů, první příklad https://github.com/tisnik/go-root/blob/master/simd/vec­tor_compatibility_1.go
41 vector_compatibility_2.go test kompatibility různých typů vektorů, druhý příklad https://github.com/tisnik/go-root/blob/master/simd/vec­tor_compatibility_2.go
42 vector_compatibility_3.go test kompatibility různých typů vektorů, třetí příklad https://github.com/tisnik/go-root/blob/master/simd/vec­tor_compatibility_3.go
       
43 mask_create.go konstrukce masky https://github.com/tisnik/go-root/blob/master/simd/mask_create.go
44 mask_operations.go základní operace s dvojicí masek https://github.com/tisnik/go-root/blob/master/simd/mas­k_operations.go
45 mask_to_vector.go převod masky na vektor https://github.com/tisnik/go-root/blob/master/simd/mas­k_to_vector.go
46 mask_selection_1.go využití masky pro výběr prvků z jednoho vektoru https://github.com/tisnik/go-root/blob/master/simd/mas­k_selection_1.go
47 mask_selection_2.go využití masky pro výběr prvků ze dvou vektorů https://github.com/tisnik/go-root/blob/master/simd/mas­k_selection_2.go

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
  67. Integer overflow
    https://en.wikipedia.org/wi­ki/Integer_overflow
  68. Saturation arithmetic
    https://en.wikipedia.org/wi­ki/Saturation_arithmetic
  69. DPPS — Dot Product of Packed Single Precision Floating-Point Values
    https://www.felixcloutier­.com/x86/dpps
  70. DPPD — Dot Product of Packed Double Precision Floating-Point Values
    https://www.felixcloutier­.com/x86/dppd
  71. VDPBF16PS — Dot Product of BF16 Pairs Accumulated Into Packed Single Precision
    https://www.felixcloutier­.com/x86/vdpbf16ps
  72. 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/

Autor článku

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