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
- Podpora instrukcí z podmnožiny FMA
- Demonstrační příklad: základní varianta operace fused multiply-add
- Další dvě varianty operace fused multiply-add
- Operace s vektory obsahujícími celočíselné prvky se znaménkem
- Demonstrační příklady: součet vektorů s celočíselnými prvky
- Součet a rozdíl s přetečením vs. se saturací
- Součet prvků vektorů se saturací
- Operace s vektory obsahujícími celočíselné prvky bez znaménka
- Ukázka základních operací s vektory obsahujícími celočíselné prvky bez znaménka
- Součet a rozdíl se saturací v případě celočíselných prvků bez znaménka
- Částečný skalární součin
- Je podporován skutečný skalární součin?
- Permutace prvků vektorů
- Demonstrační příklady na permutaci prvků vektorů
- Spojení dvou vektorů s permutací prvků
- Obsah navazujícího článku
- Repositář s demonstračními příklady
- Seznam všech předchozích částí seriálu a článků o SIMD instrukcích
- Odkazy na Internetu
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 |
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
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 |
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:
Obrázek 1: Původní obrázek před úpravami.
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.
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í.
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]
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 |
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]
Čá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).
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)
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.
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.
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/test_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/vector_broadcast.go |
| 11 | vector_comparison.go | porovnání prvků dvou vektorů | https://github.com/tisnik/go-root/blob/master/simd/vector_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/vector_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/vector_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/vector_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_vectors_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_vectors_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_vectors_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_vectors_add_normal.go |
| 20 | int_vectors_add_saturated.go | součet vektorů se saturací | https://github.com/tisnik/go-root/blob/master/simd/int_vectors_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/uint_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/uint_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/uint_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/uint_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:
- Užitečné rozšíření GCC: podpora SIMD (vektorových) instrukcí
https://www.root.cz/clanky/uzitecne-rozsireni-gcc-podpora-simd-vektorovych-instrukci/ - 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/ - Podpora SIMD (vektorových) instrukcí na RISCových procesorech
https://www.root.cz/clanky/podpora-simd-vektorovych-instrukci-na-riscovych-procesorech/ - 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/ - 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/ - 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/ - 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/ - 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/ - SIMD instrukce na platformě 80×86: instrukční sada MMX
https://www.root.cz/clanky/simd-instrukce-na-platforme-80×86-instrukcni-sada-mmx/ - 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/ - SIMD instrukce v rozšíření SSE
https://www.root.cz/clanky/simd-instrukce-v-rozsireni-sse/ - SIMD instrukce v rozšíření SSE (2. část)
https://www.root.cz/clanky/simd-instrukce-v-rozsireni-sse-2-cast/ - Pokročilejší SSE operace: přeskupení, promíchání a rozbalování prvků vektorů
https://www.root.cz/clanky/pokrocilejsi-sse-operace-preskupeni-promichani-a-rozbalovani-prvku-vektoru/ - Od instrukční sady SSE k sadě SSE2
https://www.root.cz/clanky/od-instrukcni-sady-sse-k-sade-sse2/ - 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/ - 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/ - 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
- Go 1.27 Release Notes
https://go.dev/doc/go1.27 - New experimental simd package
https://go.dev/doc/go1.27#simd - Balíček simd
https://pkg.go.dev/simd - Balíček archsimd
https://pkg.go.dev/simd/archsimd - Experimental SIMD with simd/archsimd
https://go-cookbook.com/snippets/performance/experimental-simd-archsimd - Go Generic Methods: A Hands-On Go 1.27 Tutorial
https://www.danilchenko.dev/posts/go-generic-methods/ - Auto-vectorization in GCC
https://gcc.gnu.org/projects/tree-ssa/vectorization.html - GCC documentation: Extensions to the C Language Family
https://gcc.gnu.org/onlinedocs/gcc/C-Extensions.html#C-Extensions - GCC documentation: Using Vector Instructions through Built-in Functions
https://gcc.gnu.org/onlinedocs/gcc/Vector-Extensions.html - SSE (Streaming SIMD Extentions)
http://www.songho.ca/misc/sse/sse.html - Timothy A. Chagnon: SSE and SSE2
http://www.cs.drexel.edu/~tc365/mpi-wht/sse.pdf - Intel corporation: Extending the Worldr's Most Popular Processor Architecture
http://download.intel.com/technology/architecture/new-instructions-paper.pdf - SIMD architectures:
http://arstechnica.com/old/content/2000/03/simd.ars/ - Tour of the Black Holes of Computing!: Floating Point
http://www.cs.hmc.edu/~geoff/classes/hmc.cs105…/slides/class02_floats.ppt - 3Dnow! Technology Manual
AMD Inc., 2000 - Intel MMXTM Technology Overview
Intel corporation, 1996 - MultiMedia eXtensions
http://softpixel.com/~cwright/programming/simd/mmx.phpi - AMD K5 („K5“ / „5k86“)
http://www.pcguide.com/ref/cpu/fam/g5K5-c.html - Sixth Generation Processors
http://www.pcguide.com/ref/cpu/fam/g6.htm - Great Microprocessors of the Past and Present
http://www.cpushack.com/CPU/cpu1.html - Very long instruction word (Wikipedia)
http://en.wikipedia.org/wiki/Very_long_instruction_word - CPU design (Wikipedia)
http://en.wikipedia.org/wiki/CPU_design - Bulldozer (microarchitecture)
https://en.wikipedia.org/wiki/Bulldozer_(microarchitecture) - SIMD Instructions Considered Harmful
https://www.sigarch.org/simd-instructions-considered-harmful/ - GCC Compiler Intrinsics
https://iq.opengenus.org/gcc-compiler-intrinsics/ - Scalable_Vector_Extension_(SVE)
https://en.wikipedia.org/wiki/AArch64#Scalable_Vector_Extension_(SVE) - Improve the Multimedia User Experience
https://www.arm.com/technologies/neon - NEON Technology (stránky ARM)
https://developer.arm.com/technologies/neon - SIMD Assembly Tutorial: ARM NEON – Xiph.org
https://people.xiph.org/~tterribe/daala/neon_tutorial.pdf - Ne10
http://projectne10.github.io/Ne10/ - NEON and Floating-Point architecture
http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.den0024a/BABIGHEB.html - An Introduction to ARM NEON
http://peterdn.com/post/an-introduction-to-ARM-NEON.aspx - ARM NEON Intrinsics Reference
http://infocenter.arm.com/help/topic/com.arm.doc.ihi0073a/IHI0073A_arm_neon_intrinsics_ref.pdf - Arm Neon Intrinsics vs hand assembly
https://stackoverflow.com/questions/9828567/arm-neon-intrinsics-vs-hand-assembly - ARM NEON Optimization. An Example
http://hilbert-space.de/?p=22 - AArch64 NEON instruction format
https://developer.arm.com/docs/den0024/latest/7-aarch64-floating-point-and-neon/73-aarch64-neon-instruction-format - ARM SIMD instructions
https://developer.arm.com/documentation/dht0002/a/Introducing-NEON/What-is-SIMD-/ARM-SIMD-instructions - Learn the architecture – Migrate Neon to SVE Version 1.0
https://developer.arm.com/documentation/102131/0100/?lang=en - 1.2.2. Comparison between NEON technology and other SIMD solutions
https://developer.arm.com/documentation/den0018/a/Introduction/Comparison-between-ARM-NEON-technology-and-other-implementations/Comparison-between-NEON-technology-and-other-SIMD-solutions?lang=en - NEON Programmer’s Guide
https://documentation-service.arm.com/static/63299276e68c6809a6b41308 - Brain Floating Point – nový formát uložení čísel pro strojové učení a chytrá čidla
https://www.root.cz/clanky/brain-floating-point-ndash-novy-format-ulozeni-cisel-pro-strojove-uceni-a-chytra-cidla/ - Other Built-in Functions Provided by GCC
https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html - GCC: 6.60 Built-in Functions Specific to Particular Target Machines
https://gcc.gnu.org/onlinedocs/gcc/Target-Builtins.html#Target-Builtins - Advanced Vector Extensions
https://en.wikipedia.org/wiki/Advanced_Vector_Extensions - Top 10 Craziest Assembly Language Instructions
https://www.youtube.com/watch?v=Wz_xJPN7lAY - Intel x86: let's take a look at one of the most complex instruction set!
https://www.youtube.com/watch?v=KBLy23B38-c - x64 Assembly Tutorial 58: Intro to AVX
https://www.youtube.com/watch?v=yAvuHd8cBJY - AVX512 (1 of 3): Introduction and Overview
https://www.youtube.com/watch?v=D-mM6X5×nTY - AVX512 (2 of 3): Programming AVX512 in 3 Different Ways
https://www.youtube.com/watch?v=I3efQKLgsjM - AVX512 (3 of 3): Deep Dive into AVX512 Mechanisms
https://www.youtube.com/watch?v=543a1b-cPmU - AVX-512
https://en.wikipedia.org/wiki/AVX-512 - AVX-512
https://iq.opengenus.org/avx512/ - SIMD Algorithms Youtube course
https://denisyaroshevskiy.github.io/presentations/ - Compiler explorer
https://godbolt.org/ - Restricting pointers
https://gcc.gnu.org/onlinedocs/gcc/Restricted-Pointers.html - Does the restrict keyword provide significant benefits in gcc/g++
https://stackoverflow.com/questions/1965487/does-the-restrict-keyword-provide-significant-benefits-in-gcc-g - Demystifying The Restrict Keyword
https://cellperformance.beyond3d.com/articles/2006/05/demystifying-the-restrict-keyword.html - Basics of Vectorization for Fortran Applications
https://inria.hal.science/hal-01688488/document - What does the restrict keyword mean in C++?
https://stackoverflow.com/questions/776283/what-does-the-restrict-keyword-mean-in-c - restrict keyword (Wikipedia)
https://en.wikipedia.org/wiki/Restrict - Reduction operator
https://en.wikipedia.org/wiki/Reduction_operator - The Power of the Dot Product in Artificial Intelligence
https://medium.com/data-science/the-power-of-the-dot-product-in-artificial-intelligence-c002331e1829 - Can any one explain why dot product is used in neural network and what is the intitutive thought of dot product
https://stats.stackexchange.com/questions/291680/can-any-one-explain-why-dot-product-is-used-in-neural-network-and-what-is-the-in - Aligned and unaligned memory accesses?
https://stackoverflow.com/questions/1063809/aligned-and-unaligned-memory-accesses - Go finally gets SIMD in 1.26
https://hexfusion.io/posts/go-simd-art/ - simd/archsimd: architecture-specific SIMD intrinsics under a GOEXPERIMENT #73787
https://github.com/golang/go/issues/73787 - Integer overflow
https://en.wikipedia.org/wiki/Integer_overflow - Saturation arithmetic
https://en.wikipedia.org/wiki/Saturation_arithmetic - DPPS — Dot Product of Packed Single Precision Floating-Point Values
https://www.felixcloutier.com/x86/dpps - DPPD — Dot Product of Packed Double Precision Floating-Point Values
https://www.felixcloutier.com/x86/dppd - VDPBF16PS — Dot Product of BF16 Pairs Accumulated Into Packed Single Precision
https://www.felixcloutier.com/x86/vdpbf16ps - Brain Floating Point – nový formát uložení čísel pro strojové učení a chytrá čidla
https://www.root.cz/clanky/brain-floating-point-ndash-novy-format-ulozeni-cisel-pro-strojove-uceni-a-chytra-cidla/
