Dlouho očekávaná podpora SIMD operací v programovacím jazyku Go (2. část)

Dnes
Doba čtení: 34 minut

Sdílet

Autor: Go lang
Dokončíme popis dnes stále ještě experimentálního balíčku simd/archsimd, ve kterém jsou implementovány všechny nízkoúrovňové SIMD operace, například včetně permutace vektorů apod.

Na úvodní článek o podpoře SIMD operací v programovacím jazyku Go dnes navážeme. Dokončíme popis (dnes stále ještě experimentálního) balíčku simd/archsimd, ve kterém jsou implementovány všechny nízkoúrovňové SIMD operace. Většina těchto operací je provedena jednou instrukcí spadající do rozšíření instrukční sady SSE (Streaming SIMD Extensions), SSE2 (popř. SSE3 či SSE4), AVX (Advanced Vector Extensions), AVX2 a v případě širokých vektorů nebo sofistikovanějších operací pak AVX-512. V navazujícím článku pak celé téma dokončíme, protože si popíšeme balíček simd (už bez /archsimd), který obsahuje vysokoúrovňové SIMD operace, u nichž již většinou neexistuje přímá vazba mezi volanou metodou a instrukcí provedenou mikroprocesorem.

Co se dozvíte v článku
  1. Podpora instrukcí z podmnožiny FMA
  2. Demonstrační příklad: základní varianta operace fused multiply-add
  3. Další dvě varianty operace fused multiply-add
  4. Operace s vektory obsahujícími celočíselné prvky se znaménkem
  5. Demonstrační příklady: součet vektorů s celočíselnými prvky
  6. Součet a rozdíl s přetečením vs. se saturací
  7. Součet prvků vektorů se saturací
  8. Operace s vektory obsahujícími celočíselné prvky bez znaménka
  9. Ukázka základních operací s vektory obsahujícími celočíselné prvky bez znaménka
  10. Součet a rozdíl se saturací v případě celočíselných prvků bez znaménka
  11. Částečný skalární součin
  12. Je podporován skutečný skalární součin?
  13. Permutace prvků vektorů
  14. Demonstrační příklady na permutaci prvků vektorů
  15. Spojení dvou vektorů s permutací prvků
  16. Obsah navazujícího článku
  17. Repositář s demonstračními příklady
  18. Seznam všech předchozích částí seriálu a článků o SIMD instrukcích
  19. Odkazy na Internetu
Poznámka: jen pro připomenutí. Samotný jazyk Go vyžaduje mikroprocesor, který podporuje jak rozšíření instrukční sady SSE, tak i SSE2, i když se tato rozšíření využívají pouze pro skalární operace. Vzhledem k tomu, že tyto technologie existují již celé čtvrtstoletí, neměl by být s jejich podporou prakticky žádný problém (dokonce tato rozšíření podporují i procesory Atom).

Podpora instrukcí z podmnožiny FMA

První rozšíření instrukční sady o SIMD instrukce, se kterým se v dnešním článku setkáme, se jmenuje FMA, což je zkratka odvozená z celého názvu Fused Multiply–Add. Z tohoto názvu je možné odvodit, že v tomto rozšíření nalezneme instrukci pro provedení operace a·b + c. To ale není vše, protože v rámci tohoto rozšíření byly přidány i další varianty této instrukce, v nichž se navíc mění znaménka jednotlivých operandů popř. pořadí operandů (to má význam především ve chvíli, kdy je jeden z operandů uložen v operační paměti a nikoli v pracovním registru).

Účelem tohoto rozšíření je (relativně nepatrně) zvýšit výpočetní rychlost, ale především provést výpočet bez mezizaokrouhlení výsledků. Výpočty probíhají nad typem single či double (skalární varianta, která nás ovšem v kontextu dnešního článku nezajímá) popř. nad vektory s typy single a double.

Ve skutečnosti rozšíření FMA existuje ve dvou variantách, které jsou pojmenovány FMA3 a FMA4. V dnešním článku se zaměříme na FMA3, protože toto rozšíření je v současnosti podporováno jak čipy od AMD, tak i od Intelu (na rozdíl od FMA4, které je prozatím podporováno jen čipy od AMD).

V rozšíření FMA3 lze nalézt jen relativně malé množství nových instrukcí, k jejichž jménům je ještě přidán postfixový kód popsaný níže:

Instrukce Stručný popis instrukce Poznámka
VFMADD x = +a · b + c klasická operace multiply-add
VFNMADD x = -a · b + c N – negate
VFMSUB x = +a · b – c  
VFMADDSUB x = +a · b + c nebo x = +a · b – c provádí se otočení znaménka u vybraných prvků vektoru c
VFMSUBADD x = +a · b − c nebo x = +a · b + c provádí se otočení znaménka u vybraných prvků vektoru c

U konkrétních instrukcí zavedených rozšířením FMA3 se uvádí i postfixový kód, který určuje pořadí operandů. Tento postfix uvidíme i v následujících kapitolách:

Postfixový kód Význam
132 a = a · c + b
213 a = b · a + c
231 a = b · c + a
Poznámka: z tabulky je patrné, že kód uvádí pořadí operandů oproti standardnímu pořadí, které by se zapsalo postfixem 123.

Demonstrační příklad: základní varianta operace fused multiply-add

Podívejme se nyní na základní způsob použití operace fused multiply-add. Vstupem bude trojice vektorů v1, v2 a v3, z nichž každý obsahuje čtyři hodnoty typu single resp. float32. Zavoláme přitom metodu nazvanou MulAdd vztaženou k příjemci v1 (první vektor) a předáme této metodě ostatní dva vektory v2 a v3:

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

Výsledky, které získáme po překladu a spuštění tohoto programu, by měly vypadat následovně:

$ go run vector_mul_add_1.go
 
a1 =        [1 2 3 4]
a2 =        [2 2 2 2]
a3 =        [100 100 100 100]
a1*a2+a3 =  [102 104 106 108]

První prvek výsledného vektoru byl získán výpočtem 1*2+100=102 atd.

Ještě nás bude zajímat, jaká instrukce byla použita pro výpočet:

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

Ukázka části implementace funkce main:

0x01d0 00464 (/tmp/ramdisk/go/bin/vector_mul_add_1.go:24)       VMOVDQU main.a1+72(SP), X0
0x01d6 00470 (/tmp/ramdisk/go/bin/vector_mul_add_1.go:25)       VMOVDQU main.a2+56(SP), X1
0x01dc 00476 (/tmp/ramdisk/go/bin/vector_mul_add_1.go:29)       VFMADD213PS     main.a3+40(SP), X1, X0
0x01e3 00483 (/tmp/ramdisk/go/bin/vector_mul_add_1.go:29)       MOVUPS  X0, main..autotmp_64+88(SP)
0x01e8 00488 (/tmp/ramdisk/go/bin/vector_mul_add_1.go:32)       LEAQ    type:float32(SB), AX
0x01ef 00495 (/tmp/ramdisk/go/bin/vector_mul_add_1.go:32)       MOVL    $4, BX
0x01f4 00500 (/tmp/ramdisk/go/bin/vector_mul_add_1.go:32)       MOVL    BX, CX
0x01f6 00502 (/tmp/ramdisk/go/bin/vector_mul_add_1.go:32)       CALL    runtime.makeslice(SB)
0x01fb 00507 (/tmp/ramdisk/go/bin/vector_mul_add_1.go:35)       XCHGL   AX, AX
Poznámka: je tedy použita varianta „213“ s prohozeným prvním a druhým operandem.

Další dvě varianty operace fused multiply-add

Prozatím jsme si ukázali operaci nazvanou MulAdd, ovšem Go nabízí i metody se jmény MulAddEvenSubOdd a MulAddOddSubEven. Tyto metody se od původní operace MulAdd odlišují tím, že se sudé popř. liché prvky výsledného vektoru počítají odlišným způsobem: namísto x=+a · b + c se provádí výpočet x = +a · b – c. Ostatně si to můžeme velmi snadno otestovat. První z příkladů volá metodu MulAddEvenSubOdd:

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

Výsledky:

$ go run vector_mul_add_2.go
 
a1 =        [1 2 3 4]
a2 =        [2 2 2 2]
a3 =        [100 100 100 100]
a1*a2±a3 =  [-98 104 -94 108]

Zatímco v příkladu druhém se volá metoda MulAddOddSubEven:

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

Výsledky budou pochopitelně odlišné, konkrétně:

$ go run vector_mul_add_3.go
 
a1 =        [1 2 3 4]
a2 =        [2 2 2 2]
a3 =        [100 100 100 100]
a1*a2±a3 =  [102 -96 106 -92]

Pro úplnost se ještě podívejme na způsob překladu do strojového kódu.

První příklad s metodou MulAddEvenSubOdd:

0x01d0 00464 (/tmp/ramdisk/go/bin/vector_mul_add_2.go:24)       VMOVDQU main.a1+72(SP), X0
0x01d6 00470 (/tmp/ramdisk/go/bin/vector_mul_add_2.go:25)       VMOVDQU main.a2+56(SP), X1
0x01dc 00476 (/tmp/ramdisk/go/bin/vector_mul_add_2.go:29)       VFMADDSUB213PS main.a3+40(SP), X1, X0
0x01e3 00483 (/tmp/ramdisk/go/bin/vector_mul_add_2.go:29)       MOVUPS  X0, main..autotmp_64+88(SP)
0x01e8 00488 (/tmp/ramdisk/go/bin/vector_mul_add_2.go:32)       LEAQ    type:float32(SB), AX
0x01ef 00495 (/tmp/ramdisk/go/bin/vector_mul_add_2.go:32)       MOVL    $4, BX
0x01f4 00500 (/tmp/ramdisk/go/bin/vector_mul_add_2.go:32)       MOVL    BX, CX
0x01f6 00502 (/tmp/ramdisk/go/bin/vector_mul_add_2.go:32)       CALL    runtime.makeslice(SB)
0x01fb 00507 (/tmp/ramdisk/go/bin/vector_mul_add_2.go:35)       XCHGL   AX, AX

Druhý příklad s metodou MulAddOddSubEven:

0x01d0 00464 (/tmp/ramdisk/go/bin/vector_mul_add_3.go:24)       VMOVDQU main.a1+72(SP), X0
0x01d6 00470 (/tmp/ramdisk/go/bin/vector_mul_add_3.go:25)       VMOVDQU main.a2+56(SP), X1
0x01dc 00476 (/tmp/ramdisk/go/bin/vector_mul_add_3.go:29)       VFMSUBADD213PS main.a3+40(SP), X1, X0
0x01e3 00483 (/tmp/ramdisk/go/bin/vector_mul_add_3.go:29)       MOVUPS  X0, main..autotmp_64+88(SP)
0x01e8 00488 (/tmp/ramdisk/go/bin/vector_mul_add_3.go:32)       LEAQ    type:float32(SB), AX
0x01ef 00495 (/tmp/ramdisk/go/bin/vector_mul_add_3.go:32)       MOVL    $4, BX
0x01f4 00500 (/tmp/ramdisk/go/bin/vector_mul_add_3.go:32)       MOVL    BX, CX
0x01f6 00502 (/tmp/ramdisk/go/bin/vector_mul_add_3.go:32)       CALL    runtime.makeslice(SB)
0x01fb 00507 (/tmp/ramdisk/go/bin/vector_mul_add_3.go:35)       XCHGL   AX, AX

Operace s vektory obsahujícími celočíselné prvky se znaménkem

Prozatím jsme si ukazovali operace s vektory, které obsahovaly prvky typu single či double resp. v jazyku Go typu float32 a float64. Ovšem velmi často se setkáme i s požadavkem na provádění „vektorových“ operací s vektory obsahujícími celočíselné hodnoty. Konkrétně jsou v balíčku simd/archsimd podporovány operace s vektory těchto typů:

Struktura Typ vektoru, který struktura reprezentuje Šířka vektoru (bity)
Int8×16 int8 × 16 prvků 8×16=128
Int8×32 int8 × 32 prvků 8×32=256
Int8×64 int8 × 64 prvků 8×64=512
     
Int16×8 int16 × 8 prvků 16×8=128
Int16×16 int16 × 16 prvků 16×16=256
Int16×32 int16 × 32 prvků 16×32=512
     
Int32×4 int32 × 4 prvky 32×4=128
Int32×8 int32 × 8 prvků 32×8=256
Int32×16 int32 × 16 prvků 32×16=512
     
Int64×2 int64 × 2 prvky 64×2=128
Int64×4 int64 × 4 prvky 64×4=256
Int64×8 int64 × 8 prvků 64×8=512
Poznámka: vektory s šířkou 512 bitů vyžadují, aby cílový mikroprocesor podporoval AVX-512.

Demonstrační příklady: součet vektorů s celočíselnými prvky

Začněme velmi jednoduchým příkladem, konkrétně součtem dvou vektorů, z nichž každý obsahuje čtyři prvky typu int32. Šířka těchto vektorů je tedy 32×4=128:

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

Výsledek:

$ go run int_vectors_add_1.go
 
a1    [1 2 3 4]
a2    [5 6 7 8]
a1+a2 [6 8 10 12]

Můžeme ovšem pracovat například i s vektory, které obsahují šestnáct prvků typu Int8. Šířka těchto vektorů je taktéž 128 bitů:

// ----------------------------------------------------------------------
// Podpora nativních SIMD operací v experimentálním balíčku simd/archsimd
// ----------------------------------------------------------------------
 
package main
 
import (
        "fmt"
        "simd/archsimd"
)
 
func main() {
        // konstrukce a inicializace pole s odvozením počtu prvků
        a1 := [...]int8{1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8}
        a2 := [...]int8{1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8}
 
        // výpis obsahu polí
        fmt.Println("a1   ", a1)
        fmt.Println("a2   ", a2)
 
        // převod pole na vektor se šestnácti prvky
        v1 := archsimd.LoadInt8x16Array(&a1)
        v2 := archsimd.LoadInt8x16Array(&a2)
 
        // SIMD operace součtu prvků vektorů
        v3 := v1.Add(v2)
 
        // konstrukce řezu
        result := make([]int8, 16)
 
        // zápis prvků vektoru do řezu
        v3.Store(result)
 
        // výpis obsahu řezu
        fmt.Println("a1+a2", result)
}

Opět si ukažme výsledky, které vypíše tento demonstrační příklad:

$ go run int_vectors_add_2.go
 
a1    [1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8]
a2    [1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8]
a1+a2 [2 4 6 8 10 12 14 16 2 4 6 8 10 12 14 16]

Prodloužení vektorů na šířku 256 bitů je podporováno na všech mikroprocesorech s rozšířením instrukční sady AVX-2:

// ----------------------------------------------------------------------
// Podpora nativních SIMD operací v experimentálním balíčku simd/archsimd
// ----------------------------------------------------------------------
 
package main
 
import (
        "fmt"
        "simd/archsimd"
)
 
func main() {
        // konstrukce a inicializace pole s odvozením počtu prvků
        a1 := [...]int8{1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8}
        a2 := [...]int8{1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4}
 
        // výpis obsahu polí
        fmt.Println("a1   ", a1)
        fmt.Println("a2   ", a2)
 
        // převod pole na vektor s 32 prvky
        v1 := archsimd.LoadInt8x32Array(&a1)
        v2 := archsimd.LoadInt8x32Array(&a2)
 
        // SIMD operace součtu prvků vektorů
        v3 := v1.Add(v2)
 
        // konstrukce řezu
        result := make([]int8, 32)
 
        // zápis prvků vektoru do řezu
        v3.Store(result)
 
        // výpis obsahu řezu
        fmt.Println("a1+a2", result)
}

Otestování funkcionality:

$ go run int_vectors_add_3.go
 
a1    [1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8]
a2    [1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4]
a1+a2 [2 3 4 5 6 7 8 9 3 4 5 6 7 8 9 10 4 5 6 7 8 9 10 11 5 6 7 8 9 10 11 12]

Součet a rozdíl s přetečením vs. se saturací

V mnoha multimediálních aplikacích je nutné provádět aritmetické operace se saturací, tj. takovým způsobem, aby výsledná hodnota operace (například součtu) nepřetekla, což by vedlo k nepříjemným artefaktům v obrazu, lupancům ve zvuku atd. Ostatně si to můžeme ukázat na klasickém příkladu – zvýšení světlosti celého obrázku o zadanou konstantu:

Původní obrázek před úpravami.

 Obrázek 1: Původní obrázek před úpravami.

Autor: tisnik, podle licence: Rights Managed

Výsledek získaný po zvýšení hodnot všech pixelů o hodnotu 10. Operace součtu se prováděla s přetečením.

 Obrázek 2: Výsledek získaný po zvýšení hodnot všech pixelů o hodnotu 10. Operace součtu se prováděla s přetečením.

Autor: tisnik, podle licence: Rights Managed

Výsledek získaný po zvýšení hodnot všech pixelů o hodnotu 10. Operace součtu se prováděla se saturací.

 Obrázek 3: Výsledek získaný po zvýšení hodnot všech pixelů o hodnotu 10. Operace součtu se prováděla se saturací.

Autor: tisnik, podle licence: Rights Managed

Součet prvků vektorů se saturací

Součet prvků vektorů se saturací je možné realizovat metodou nazvanou AddSaturated. Ukažme si, jak se liší běžný součet (s přetečením) od součtu se saturací. Aby se přetečení či saturace projevila, je nutné, aby výsledné hodnoty prvků vektorů překročily rozsah hodnot příslušného datového typu. Například pro typ int16 je maximální reprezentovatelná hodnota rovna 215-1. Vyzkoušejme si nejprve provedení běžného součtu prvků dvou vektorů:

// ----------------------------------------------------------------------
// Podpora nativních SIMD operací v experimentálním balíčku simd/archsimd
// ----------------------------------------------------------------------
 
package main
 
import (
        "fmt"
        "simd/archsimd"
)
 
func main() {
        // konstrukce a inicializace pole s odvozením počtu prvků
        a1 := [...]int16{1<<15 - 1, 1<<15 - 1, 1<<15 - 1, 1<<15 - 1, 1<<15 - 1, 1<<15 - 1, 1<<15 - 1, 1<<15 - 1}
        a2 := [...]int16{-3, -2, -1, 0, 1, 2, 3, 4}
 
        // výpis obsahu polí
        fmt.Println("a1   ", a1)
        fmt.Println("a2   ", a2)
 
        // převod pole na vektor s osmi prvky
        v1 := archsimd.LoadInt16x8Array(&a1)
        v2 := archsimd.LoadInt16x8Array(&a2)
 
        // SIMD operace součtu prvků vektorů
        v3 := v1.Add(v2)
 
        // konstrukce řezu
        result := make([]int16, 8)
 
        // zápis prvků vektoru do řezu
        v3.Store(result)
 
        // výpis obsahu řezu
        fmt.Println("a1+a2", result)
}

Výsledky v případě, že se provádí součet s přetečením:

$ go run int_vectors_add_normal.go
 
a1    [32767 32767 32767 32767 32767 32767 32767 32767]
a2    [-3 -2 -1 0 1 2 3 4]
a1+a2 [32764 32765 32766 32767 -32768 -32767 -32766 -32765]
Poznámka: je zde jasně patrné, že se z původně kladných hodnot staly hodnoty záporné.

Nyní demonstrační příklad upravíme do takové podoby, aby se součet prováděl se saturací:

// ----------------------------------------------------------------------
// Podpora nativních SIMD operací v experimentálním balíčku simd/archsimd
// ----------------------------------------------------------------------
 
package main
 
import (
        "fmt"
        "simd/archsimd"
)
 
func main() {
        // konstrukce a inicializace pole s odvozením počtu prvků
        a1 := [...]int16{1<<15 - 1, 1<<15 - 1, 1<<15 - 1, 1<<15 - 1, 1<<15 - 1, 1<<15 - 1, 1<<15 - 1, 1<<15 - 1}
        a2 := [...]int16{-3, -2, -1, 0, 1, 2, 3, 4}
 
        // výpis obsahu polí
        fmt.Println("a1   ", a1)
        fmt.Println("a2   ", a2)
 
        // převod pole na vektor s osmi prvky
        v1 := archsimd.LoadInt16x8Array(&a1)
        v2 := archsimd.LoadInt16x8Array(&a2)
 
        // SIMD operace součtu prvků vektorů
        v3 := v1.AddSaturated(v2)
 
        // konstrukce řezu
        result := make([]int16, 8)
 
        // zápis prvků vektoru do řezu
        v3.Store(result)
 
        // výpis obsahu řezu
        fmt.Println("a1+a2", result)
}

Výsledky budou podle očekávání zcela odlišné:

$ go run int_vectors_add_saturated.go
 
a1    [32767 32767 32767 32767 32767 32767 32767 32767]
a2    [-3 -2 -1 0 1 2 3 4]
a1+a2 [32764 32765 32766 32767 32767 32767 32767 32767]

Operace s vektory obsahujícími celočíselné prvky bez znaménka

V páté kapitole jsme se zmínili o vektorech, které obsahují celočíselné prvky se znaménkem. S takovými vektory se setkáme například při zpracování audia. Ovšem například při zpracování rastrového obrazu může být výhodnější ukládat hodnoty pixelů do vektorů s celočíselnými prvky bez znaménka. I tyto operace jsou v balíčku simd/archsimd podporovány. Konkrétně lze pracovat s vektory těchto typů:

Struktura Typ vektoru, který struktura reprezentuje Šířka vektoru (bity)
Uint8×16 uint8 × 16 prvků 8×16=128
Uint8×32 uint8 × 32 prvků 8×32=256
Uint8×64 uint8 × 64 prvků 8×64=512
     
Uint16×8 uint16 × 8 prvků 16×8=128
Uint16×16 uint16 × 16 prvků 16×16=256
Uint16×32 uint16 × 32 prvků 16×32=512
     
Uint32×4 uint32 × 4 prvky 32×4=128
Uint32×8 uint32 × 8 prvků 32×8=256
Uint32×16 uint32 × 16 prvků 32×16=512
     
Uint64×2 uint64 × 2 prvky 64×2=128
Uint64×4 uint64 × 4 prvky 64×4=256
Uint64×8 uint64 × 8 prvků 64×8=512
Poznámka: opět platí, že vektory s šířkou 512 bitů vyžadují, aby cílový mikroprocesor podporoval AVX-512.

Ukázka základních operací s vektory obsahujícími celočíselné prvky bez znaménka

Vyzkoušejme si nyní dvojici operací. Jedná se o součet vektorů s prvky bez znaménka a dále o rozdíl vektorů, které mají taktéž prvky bez znaménka. V případě součtu jsou zvoleny takové hodnoty prvků vektorů, aby došlo k přetečení (tím si ověříme, jak se výpočty provádí při volání metody Add):

// ----------------------------------------------------------------------
// Podpora nativních SIMD operací v experimentálním balíčku simd/archsimd
// ----------------------------------------------------------------------
 
package main
 
import (
        "fmt"
        "simd/archsimd"
)
 
func main() {
        // konstrukce a inicializace pole s odvozením počtu prvků
        a1 := [...]uint16{65530, 65530, 65530, 65530, 65530, 65530, 65530, 65530}
        a2 := [...]uint16{1, 2, 3, 4, 5, 6, 7, 8}
 
        // výpis obsahu polí
        fmt.Println("a1   ", a1)
        fmt.Println("a2   ", a2)
 
        // převod pole na vektor s osmi prvky
        v1 := archsimd.LoadUint16x8Array(&a1)
        v2 := archsimd.LoadUint16x8Array(&a2)
 
        // SIMD operace součtu prvků vektorů
        v3 := v1.Add(v2)
 
        // konstrukce řezu
        result := make([]uint16, 8)
 
        // zápis prvků vektoru do řezu
        v3.Store(result)
 
        // výpis obsahu řezu
        fmt.Println("a1+a2", result)
}

Výsledky ukazují, jak se projeví přetečení (odlišně, než tomu bylo u prvků se znaménkem):

$ go run uint_vectors_add_normal.go
 
a1    [65530 65530 65530 65530 65530 65530 65530 65530]
a2    [1 2 3 4 5 6 7 8]
a1+a2 [65531 65532 65533 65534 65535 0 1 2]

U operace rozdílu dvou vektorů jsou opět hodnoty prvků zvoleny takovým způsobem, aby v některých případech došlo k přetečení:

// ----------------------------------------------------------------------
// Podpora nativních SIMD operací v experimentálním balíčku simd/archsimd
// ----------------------------------------------------------------------
 
package main
 
import (
        "fmt"
        "simd/archsimd"
)
 
func main() {
        // konstrukce a inicializace pole s odvozením počtu prvků
        a1 := [...]uint16{5, 5, 5, 5, 5, 5, 5, 5}
        a2 := [...]uint16{1, 2, 3, 4, 5, 6, 7, 8}
 
        // výpis obsahu polí
        fmt.Println("a1   ", a1)
        fmt.Println("a2   ", a2)
 
        // převod pole na vektor s osmi prvky
        v1 := archsimd.LoadUint16x8Array(&a1)
        v2 := archsimd.LoadUint16x8Array(&a2)
 
        // SIMD operace součtu prvků vektorů
        v3 := v1.Sub(v2)
 
        // konstrukce řezu
        result := make([]uint16, 8)
 
        // zápis prvků vektoru do řezu
        v3.Store(result)
 
        // výpis obsahu řezu
        fmt.Println("a1+a2", result)
}

Opět se podívejme na vypočtené výsledky, které ukazují vliv přetečení:

$ go run uint_vectors_sub_normal.go
 
a1    [5 5 5 5 5 5 5 5]
a2    [1 2 3 4 5 6 7 8]
a1+a2 [4 3 2 1 0 65535 65534 65533]

Součet a rozdíl se saturací v případě celočíselných prvků bez znaménka

Nyní provedeme součet a rozdíl stejných vektorů, nyní ovšem budou obě operace provedeny se saturací. Začneme součtem vektorů, přičemž hodnoty jejich prvků jsou zvoleny tak, aby se dosáhlo meze rozsahu typu uint16:

// ----------------------------------------------------------------------
// Podpora nativních SIMD operací v experimentálním balíčku simd/archsimd
// ----------------------------------------------------------------------
 
package main
 
import (
        "fmt"
        "simd/archsimd"
)
 
func main() {
        // konstrukce a inicializace pole s odvozením počtu prvků
        a1 := [...]uint16{65530, 65530, 65530, 65530, 65530, 65530, 65530, 65530}
        a2 := [...]uint16{1, 2, 3, 4, 5, 6, 7, 8}
 
        // výpis obsahu polí
        fmt.Println("a1   ", a1)
        fmt.Println("a2   ", a2)
 
        // převod pole na vektor s osmi prvky
        v1 := archsimd.LoadUint16x8Array(&a1)
        v2 := archsimd.LoadUint16x8Array(&a2)
 
        // SIMD operace součtu prvků vektorů
        v3 := v1.AddSaturated(v2)
 
        // konstrukce řezu
        result := make([]uint16, 8)
 
        // zápis prvků vektoru do řezu
        v3.Store(result)
 
        // výpis obsahu řezu
        fmt.Println("a1+a2", result)
}

Ze zobrazených výsledků je patrné, že došlo k saturaci některých prvků vektoru na maximální hodnotě 65535:

$ go run uint_vectors_add_saturated.go
 
a1    [65530 65530 65530 65530 65530 65530 65530 65530]
a2    [1 2 3 4 5 6 7 8]
a1+a2 [65531 65532 65533 65534 65535 65535 65535 65535]

Nakonec otestujeme výpočet rozdílu dvou vektorů s prvky opět zvolenými tak, aby bylo nutné saturaci provést (zde na nulové hodnotě):

// ----------------------------------------------------------------------
// Podpora nativních SIMD operací v experimentálním balíčku simd/archsimd
// ----------------------------------------------------------------------
 
package main
 
import (
        "fmt"
        "simd/archsimd"
)
 
func main() {
        // konstrukce a inicializace pole s odvozením počtu prvků
        a1 := [...]uint16{5, 5, 5, 5, 5, 5, 5, 5}
        a2 := [...]uint16{1, 2, 3, 4, 5, 6, 7, 8}
 
        // výpis obsahu polí
        fmt.Println("a1   ", a1)
        fmt.Println("a2   ", a2)
 
        // převod pole na vektor s osmi prvky
        v1 := archsimd.LoadUint16x8Array(&a1)
        v2 := archsimd.LoadUint16x8Array(&a2)
 
        // SIMD operace součtu prvků vektorů
        v3 := v1.SubSaturated(v2)
 
        // konstrukce řezu
        result := make([]uint16, 8)
 
        // zápis prvků vektoru do řezu
        v3.Store(result)
 
        // výpis obsahu řezu
        fmt.Println("a1+a2", result)
}

Výsledky budou vypadat následovně:

$ go run uint_vectors_sub_saturated.go
 
a1    [5 5 5 5 5 5 5 5]
a2    [1 2 3 4 5 6 7 8]
a1+a2 [4 3 2 1 0 0 0 0]
Poznámka: povšimněte si, že u posledních tří výsledků nikdy nedošlo k tomu, aby výsledek překročil hodnotu 0.

Částečný skalární součin

V balíčku simd/archsimd je pro některé typy vektorů k dispozici i metoda nazvaná poněkud záhadně DotProductPairs. Tato metoda vynásobí odpovídající si prvky vektorů (což je skutečně základ skalárního součinu) a následně sečte vždy dva vedle sebe ležící výsledky. To tedy znamená, že výsledný vektor bude mít poloviční počet prvků v porovnání s oběma vektory vstupními. Ukažme si příklad pro dvojici vstupních vektorů, z nichž každý má osm prvků:

$ go run dot_product_1.go
 
a1  [0 1 2 3 4 5 6 7]
a2  [0 1 2 3 4 5 6 7]
dot [1 13 41 85]

Například hodnota 41 byla vypočtena stylem: 4×4+5×5=16+25=41.

Dtto, ale pro vstupní vektory, které mají šestnáct prvků:

$ go run dot_product_2.go
 
a1  [0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15]
a2  [0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15]
dot [1 13 41 85 145 221 313 421]

Výsledkem je vektor s osmi prvky.

Následuje výpis obou demonstračních příkladů pro výpočet částečného skalárního součinu:

// ----------------------------------------------------------------------
// Podpora nativních SIMD operací v experimentálním balíčku simd/archsimd
// ----------------------------------------------------------------------
 
package main
 
import (
        "fmt"
        "simd/archsimd"
)
 
func main() {
        // konstrukce a inicializace pole s odvozením počtu prvků
        a1 := [...]int16{0, 1, 2, 3, 4, 5, 6, 7}
        a2 := [...]int16{0, 1, 2, 3, 4, 5, 6, 7}
 
        // výpis obsahu polí
        fmt.Println("a1 ", a1)
        fmt.Println("a2 ", a2)
 
        // převod pole na vektor s osmi prvky
        v1 := archsimd.LoadInt16x8Array(&a1)
        v2 := archsimd.LoadInt16x8Array(&a2)
 
        // SIMD operace součtu prvků vektorů
        v3 := v1.DotProductPairs(v2)
 
        // konstrukce řezu
        result := make([]int32, 4)
 
        // zápis prvků vektoru do řezu
        v3.Store(result)
 
        // výpis obsahu řezu
        fmt.Println("dot", result)
}
// ----------------------------------------------------------------------
// Podpora nativních SIMD operací v experimentálním balíčku simd/archsimd
// ----------------------------------------------------------------------
 
package main
 
import (
        "fmt"
        "simd/archsimd"
)
 
func main() {
        // konstrukce a inicializace pole s odvozením počtu prvků
        a1 := [...]int16{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
        a2 := [...]int16{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
 
        // výpis obsahu polí
        fmt.Println("a1 ", a1)
        fmt.Println("a2 ", a2)
 
        // převod pole na vektor s osmi prvky
        v1 := archsimd.LoadInt16x16Array(&a1)
        v2 := archsimd.LoadInt16x16Array(&a2)
 
        // SIMD operace součtu prvků vektorů
        v3 := v1.DotProductPairs(v2)
 
        // konstrukce řezu
        result := make([]int32, 8)
 
        // zápis prvků vektoru do řezu
        v3.Store(result)
 
        // výpis obsahu řezu
        fmt.Println("dot", result)
}

Je podporován skutečný skalární součin?

V rozšíření instrukčních sad SSE4 resp. AVX (podle šířky vektorů) nalezneme dvojici instrukcí DPPS a DPPD. Tyto dvě instrukce dokážou provést skutečný skalární součin. V instrukční sadě AVX-512 je navíc k dispozici instrukce VDPBF16PS pro skalární součin dvou vektorů s prvky typu brain floating point (viz též Brain Floating Point – nový formát uložení čísel pro strojové učení a chytrá čidla). Ovšem – alespoň prozatím – nemají tyto instrukce přímou obdobu v metodách nabízených v rámci experimentálního balíčku simd/archsimd, což je škoda, protože výpočet skalárního součinu se například v souvislosti s vyhledáváním vektorů na základě jejich podobnosti používá prakticky neustále. Snad se tedy dočkáme v další verzi jazyka Go.

Permutace prvků vektorů

Mezi nejdůležitější nové instrukce AVX patří instrukce provádějící operaci typu permutace prvků. Tyto operace byly dále rozšířeny instrukční sadou AVX-512. Permutací vznikne vektor se stejným počtem a typem prvků jako vektor původní, ovšem pořadí prvků může být odlišné a prvky se dokonce mohou opakovat. Permutace se realizuje voláním metody Permute, které se předá vektor s prvky typu uint32. Hodnoty těchto prvků představují indexy pro výběr prvků z původního vektoru.

Prováděnou operaci je možné symbolicky zapsat následovně:

z[i] = x[indices[i] % len(x)]

kde x je vstupní vektor, z je výstupní vektor (výsledek) a indices je vektor obsahující libovolné indexy v rozsahu 0..délka_vektoru-1 (vyšší hodnoty jsou ořezány operací podílu se zbytkem).

Poznámka: povšimněte si, že se nekladou žádná další omezení pro hodnoty indexů. Všechny hodnoty tedy mohou být totožné apod.

Způsob překladu do strojového kódu může vypadat následovně:

0x00c0 00192 (/tmp/ramdisk/go/bin/permute_1.go:22)      MOVL    $1, main..autotmp_26+76(SP)
0x00c8 00200 (/tmp/ramdisk/go/bin/permute_1.go:22)      MOVQ    $12884901890, CX
0x00d2 00210 (/tmp/ramdisk/go/bin/permute_1.go:22)      MOVQ    CX, main..autotmp_26+80(SP)
0x00d7 00215 (/tmp/ramdisk/go/bin/permute_1.go:22)      MOVQ    $21474836484, CX
0x00e1 00225 (/tmp/ramdisk/go/bin/permute_1.go:22)      MOVQ    CX, main..autotmp_26+88(SP)
0x00e6 00230 (/tmp/ramdisk/go/bin/permute_1.go:22)      MOVQ    $30064771078, CX
0x00f0 00240 (/tmp/ramdisk/go/bin/permute_1.go:22)      MOVQ    CX, main..autotmp_26+96(SP)
0x00f5 00245 (/tmp/ramdisk/go/src/simd/archsimd/slice_gen_amd64.go:204) VMOVDQU main..autotmp_26+72(SP), Y1
0x00fb 00251 (/tmp/ramdisk/go/bin/permute_1.go:25)      VPERMPS Y0, Y1, Y0
0x0100 00256 (/tmp/ramdisk/go/bin/permute_1.go:25)      VMOVDQU Y0, main..autotmp_48+104(SP)
Poznámka: konkrétně instrukce VPERMPS patří do instrukční sady AVX.

Demonstrační příklady na permutaci prvků vektorů

Podívejme se nyní na trojici demonstračních příkladů, které mají prakticky totožný zdrojový kód, ale liší se obsahem vektoru indices. Tyto příklady provádí následující trojici operací:

Zachování pořadí prvku původního vektoru:

a1       =  [1 2 3 4 5 6 7 8]
indices  =  [0 1 2 3 4 5 6 7]
permuted =  [1 2 3 4 5 6 7 8]

Otočení prvků původního vektoru:

a1       =  [1 2 3 4 5 6 7 8]
indices  =  [7 6 5 4 3 2 1 0]
permuted =  [8 7 6 5 4 3 2 1]

Výběr prvků takovým způsobem, že se bude opakovat první a poslední prvek:

a1       =  [1 2 3 4 5 6 7 8]
indices  =  [0 0 7 7 0 0 7 7]
permuted =  [1 1 8 8 1 1 8 8]

Pro úplnost následuje výpis zdrojových kódů všech tří příkladů:

// ----------------------------------------------------------------------
// Podpora nativních SIMD operací v experimentálním balíčku simd/archsimd
// ----------------------------------------------------------------------
 
package main
 
import (
        "fmt"
        "simd/archsimd"
)
 
func main() {
        // konstrukce a inicializace pole s odvozením počtu prvků
        a1 := [...]float32{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0}
 
        // výpis obsahu pole
        fmt.Println("a1       = ", a1)
 
        // převod pole na vektor s osmi prvky
        v1 := archsimd.LoadFloat32x8Array(&a1)
 
        indices := archsimd.LoadUint32x8([]uint32{0, 1, 2, 3, 4, 5, 6, 7})
 
        // výpočet permutace
        v2 := v1.Permute(indices)
 
        // konstrukce řezu
        result := make([]float32, 8)
 
        // zápis prvků vektoru do řezu
        v2.Store(result)
 
        // výpis obsahu řezu
        fmt.Println("permuted = ", result)
}
// ----------------------------------------------------------------------
// Podpora nativních SIMD operací v experimentálním balíčku simd/archsimd
// ----------------------------------------------------------------------
 
package main
 
import (
        "fmt"
        "simd/archsimd"
)
 
func main() {
        // konstrukce a inicializace pole s odvozením počtu prvků
        a1 := [...]float32{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0}
 
        // výpis obsahu pole
        fmt.Println("a1       = ", a1)
 
        // převod pole na vektor s osmi prvky
        v1 := archsimd.LoadFloat32x8Array(&a1)
 
        indices := archsimd.LoadUint32x8([]uint32{7, 6, 5, 4, 3, 2, 1, 0})
 
        // výpočet permutace
        v2 := v1.Permute(indices)
 
        // konstrukce řezu
        result := make([]float32, 8)
 
        // zápis prvků vektoru do řezu
        v2.Store(result)
 
        // výpis obsahu řezu
        fmt.Println("permuted = ", result)
}
// ----------------------------------------------------------------------
// Podpora nativních SIMD operací v experimentálním balíčku simd/archsimd
// ----------------------------------------------------------------------
 
package main
 
import (
        "fmt"
        "simd/archsimd"
)
 
func main() {
        // konstrukce a inicializace pole s odvozením počtu prvků
        a1 := [...]float32{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0}
 
        // výpis obsahu pole
        fmt.Println("a1       = ", a1)
 
        // převod pole na vektor s osmi prvky
        v1 := archsimd.LoadFloat32x8Array(&a1)
 
        indices := archsimd.LoadUint32x8([]uint32{0, 0, 7, 7, 0, 0, 7, 7})
 
        // výpočet permutace
        v2 := v1.Permute(indices)
 
        // konstrukce řezu
        result := make([]float32, 8)
 
        // zápis prvků vektoru do řezu
        v2.Store(result)
 
        // výpis obsahu řezu
        fmt.Println("permuted = ", result)
}

Spojení dvou vektorů s permutací prvků

V případě, že cílový počítač (na kterém se bude spouštět přeložený program) podporuje rozšíření instrukční sady AVX-512, lze pro dvojici vektorů typu Float64×2 zavolat i metodu nazvanou ConcatPermute, která má tuto hlavičku:

func (x Float64x2) ConcatPermute(y Float64x2, indices Uint64x2) Float64x2

Tato metoda nejdříve spojí oba vektory x (příjemce) i y (parametr) do dočasného vektoru s dvojnásobným počtem prvků a následně z tohoto vektoru vybere dvojici prvků na základě obsahu vektoru indices.

Poznámka: samozřejmě existují i stejně pojmenované metody pro další typy vektorů:
func (x Float32x16) ConcatPermute(y Float32x16, indices Uint32x16) Float32x16
func (x Float32x4) ConcatPermute(y Float32x4, indices Uint32x4) Float32x4
func (x Float32x8) ConcatPermute(y Float32x8, indices Uint32x8) Float32x8
func (x Float64x2) ConcatPermute(y Float64x2, indices Uint64x2) Float64x2
func (x Float64x4) ConcatPermute(y Float64x4, indices Uint64x4) Float64x4
func (x Float64x8) ConcatPermute(y Float64x8, indices Uint64x8) Float64x8
func (x Int16x16) ConcatPermute(y Int16x16, indices Uint16x16) Int16x16
func (x Int16x32) ConcatPermute(y Int16x32, indices Uint16x32) Int16x32
func (x Int16x8) ConcatPermute(y Int16x8, indices Uint16x8) Int16x8
func (x Int32x16) ConcatPermute(y Int32x16, indices Uint32x16) Int32x16
func (x Int32x4) ConcatPermute(y Int32x4, indices Uint32x4) Int32x4
func (x Int32x8) ConcatPermute(y Int32x8, indices Uint32x8) Int32x8
func (x Int64x2) ConcatPermute(y Int64x2, indices Uint64x2) Int64x2
func (x Int64x4) ConcatPermute(y Int64x4, indices Uint64x4) Int64x4
func (x Int64x8) ConcatPermute(y Int64x8, indices Uint64x8) Int64x8
func (x Int8x16) ConcatPermute(y Int8x16, indices Uint8x16) Int8x16
func (x Int8x32) ConcatPermute(y Int8x32, indices Uint8x32) Int8x32
func (x Int8x64) ConcatPermute(y Int8x64, indices Uint8x64) Int8x64
func (x Uint16x16) ConcatPermute(y Uint16x16, indices Uint16x16) Uint16x16
func (x Uint16x32) ConcatPermute(y Uint16x32, indices Uint16x32) Uint16x32
func (x Uint16x8) ConcatPermute(y Uint16x8, indices Uint16x8) Uint16x8
func (x Uint32x16) ConcatPermute(y Uint32x16, indices Uint32x16) Uint32x16
func (x Uint32x4) ConcatPermute(y Uint32x4, indices Uint32x4) Uint32x4
func (x Uint32x8) ConcatPermute(y Uint32x8, indices Uint32x8) Uint32x8
func (x Uint64x2) ConcatPermute(y Uint64x2, indices Uint64x2) Uint64x2
func (x Uint64x4) ConcatPermute(y Uint64x4, indices Uint64x4) Uint64x4
func (x Uint64x8) ConcatPermute(y Uint64x8, indices Uint64x8) Uint64x8
func (x Uint8x16) ConcatPermute(y Uint8x16, indices Uint8x16) Uint8x16
func (x Uint8x32) ConcatPermute(y Uint8x32, indices Uint8x32) Uint8x32
func (x Uint8x64) ConcatPermute(y Uint8x64, indices Uint8x64) Uint8x64

Obsah navazujícího článku

V navazujícím článku celé téma SIMD („vektorových“) operací dokončíme, protože si popíšeme balíček simd (už bez /archsimd), který obsahuje vysokoúrovňové SIMD operace, u nichž již většinou neexistuje přímá vazba mezi volanou metodou a instrukcí provedenou mikroprocesorem.

Školení Linux

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

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

# Příklad Stručný popis Cesta
1 test_avx.go runtime test, zda procesor podporuje instrukční sadu AVX https://github.com/tisnik/go-root/blob/master/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

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

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

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