Pro programování s těmito věcmi jsem používal Intel Intrinsics a tuto klikací příručku: https://www.intel.com/content/www/us/en/docs/intrinsics-guide/
Některé vektorové algoritmy jsou implementovány v knihovně Volk https://www.libvolk.org/, většinou v SSE, AVX, s řešením nezarovnaného přístupu do paměti, a občas i v ARM Neonu. Ve zdrojáku se pak můžete inspirovat jak je to napsané: https://github.com/gnuradio/volk/blob/main/kernels/volk/volk_32fc_s32fc_x2_rotator_32fc.h
to by me zajimalo jak by to bylo s vypocetnim vykonem, kdyby se i jakekoliv scitani, odcitani atd. normalnich cisel (ne vektoru) udelalo s pouzitim simd instrukci :-)
jak nekdo udelal kompiler co vsecko prevede na mov instrukce, tak kompilator co vsecku matiku dela se simd.
6. 10. 2022, 08:36 editováno autorem komentáře
Vector<T> je abstrakcia nad SIMD instrukciami, ktore sa pouziju podla procesora (dalej je tam vector2, vector3, MatrixXY,...).
Instrukcie procesorov sa ale daju tiez vyuzit priamo (System.Runtime.Intrinsics.XY), napr: pre X86 architektury su tu: https://learn.microsoft.com/en-us/dotnet/api/system.runtime.intrinsics.x86?view=net-6.0 samozremje podporovane su aj army.
Hlavni problem pouziti techto instrukci vidim v tom, ze kazdy prekladac to ma jinak, navic je potreba resit fallbacky pro starsi architektury. Instrukce jsou dost podobne, casto jde pouze o to, ze novejsi zvladaji operace na vice prvcich, takze tech fallbacku muze byt docela hodne.
Samozrejme pro konkretni pouziti na konkretnim stroji to neni takovy problem, ale pro program, ktery obecne bezi kdekoliv je to fakt otrava. Pokud chci mit fakt jistotu, tak je nejlepsi moznost konkretni funkci napsat v NASM (nebo alternative), ale bohuzel stejne musim mit takovych funkci mit nekolik pro ruzne varianty.
V posledni dobe jsem to spise vzdal a pouzivam OpenCL a co jsem zkousel a mam z toho docela dobry pocit je OMP simd. Jeste jsem se dival na SyCL, ale v dobe, kdy jsem se na to dival to nemelo a mozna porad nema nejakou rozumnou implementaci.
Tak treba s takovym SSE se uz da pocitat vsude. Problematicky bude dneska uz jen AVX-512 rekl bych. Jako uplne nejlepsi reseni je binarka, co si nataha .so-cko podle moznosti te architektury, takze by tam bylo nejakych 3-5 konfiguraci max. To neni tak zly.
OpenCL ten problem neresi, protoze vyzaduje SSE3 ne?
V zásadě budou asi stačit ty 3 konfigurace - SSEx (2 nebo cokoliv je dneska standard), AVX-2 a AVX-512. Pro ARM Neon and SVE.
K předchozímu - není extra dobré to psát přímo v ASM, bo: (a) nemusí to být dobře integrované s kompilátorem, v rámci vyšších funkcí, kde se aplikuje inline, (b) v rámci inline je kompilátor lépe schopen distribuovat registry podle svých potřeb nebo udržet danou proměnnou v registru apod. Řešení je buď intrinsic, které se téměř přímo přeloží (občas je kompilátor ještě chytřejší - občas až moc, že je z toho bug), nebo to nechat přímo na kompilátor - umí vektorizaci už většinou dobře, pokud se mu dají volné ruce (třeba použití FMA může být problém, bo to vede ke správnějším výsledkům a musí se tedy explicitně povolit). To druhé má výhodu, že se kód vezme a jenom se třikrát přeloží pro různé architektury, takže je to bez práce.
Jestli vylepšit samotný kompilátor, aby uměl složitější funkce jako saturace - možná je to spíš lepší nechat na knihovnu, než cpát všechno do kompilátoru... Třeba GLM je pěkná na operace pro 3D grafiku.
To je pravda, nejrozsirenejsi architekturou na PC je x64 a tam uz je aspon zakladni SSE vzdycky.
S tim OpenCL mas taky pravdu, minimalne tam musis mit fallback pro pripad, ze dany host OpenCL nepodporuje (tady se samozrejme bavime o vektorizaci na CPU, normalne se da pocitat s tim, ze host bude mit i GPU).
Problem je ten, ze 3-5 konfiguraci je i tak dost, kdo to ma udrzovat? Typicky bych asi dal jako priklad TurboJPEG knihovnu, na vetsinu obrazku funguje naprosto spolehlive, ale dej mu na vstup nejakou nepouzivanou variantu a v nejlepsim pripade zrovna tohle nemaji akcelerovane, tak to trva, v horsim to vyplivne spatny obrazek a v nejhorsim to spadne. Dost takovych bugu vyresili, ale udrzovat to musi byt velky opruz. A to se jedna o knihovnu, kterou treba prohlizece dokazi a chteji zasponzorovat.
Ono je rozumné neimplementovat kód přímo v Intel intristic ale na GCC s využitím
typedef int v4si __attribute__ ((vector_size (16))); v4si a, b, c; c = a + b;
https://gcc.gnu.org/onlinedocs/gcc-12.2.0/gcc/Vector-Extensions.html#Vector-Extensions
Bude to chodit jak pro x86, ARM, RISC-V a GCC rovnou garantuje, že pokud je navolená při kompilaci architektura, která danou šířku neumí, tak operace realizuje sekvencemi užších až případně skalárních operací.
Obecně ale celý přístup se SIMD operacemi, u kterých je délka natvrdo zakódovaná do instrukce dobře použitelný jen pro vektory pevné délky, jako je třeba homogenní souřadnice 4x float atd... Pro urychlování smyček nad vektory s runtime definovanou délkou vychází kód ošklivě, vetšinou nějaký skalární start pro zarovnání, pak SIMD tělo a skalární dopočítání zbytku. To chodí použitelně pro dlouhé vektory, pro délku okolo 10, 20 prvků je to katastrofa a kompilátor často nemá jak poznat, jestli bude vektor 10 prvků nebo třeba ze vstupu po síti přijde 1000 prvků pole. Takže rozumná, úspěšná autovektorizce je na zastaralé architektury jako je x86 nemožná.
Proto se RISC-V inspirovalo spíše Seymoura Craye ze sedmesátých let a definuje vektorové instrukce s dynamicky nastavovanou celkovou délkou operace. HW pak rozdělí výpočet na skupiny podle aktuálně implementované šířky vektorové ALU s tím kód nepotřebuje rozhodování, jestli se vektorizace vyplatí, není žádný loop startup and finish specific kód a i program zkompilovaný pro starší nebo jen levnější implementaci s užší ALU při spuštění na širší automaticky využije plnou délku.
Viz https://github.com/riscv/riscv-v-spec/releases/tag/v1.0
Případně článek SIMD Instructions Considered Harmful
https://www.sigarch.org/simd-instructions-considered-harmful/
V zásadě to tedy není, že kompilátory nestíhají, ale že Intel a další opět předvedli, že táhnou rozvoj od sedmdesátých let špatným směrem, protože již dříve to bylo vyřešené správně. RISC-V a teď i AArch64 ARM přes SVE špatný směr napravují.
kód nepotřebuje rozhodování, jestli se vektorizace vyplatí, není žádný loop startup and finish specific kód
To není tak úplně pravda - i Aarch64 SVE2 má omezení, že operace probíhají na násobku 128-bit vektorů. Takže pro skutečně jakoukoliv velikost tam bude muset být ošetření modulo 4 (pro float) či modulo 2 (pro double). Navíc jsou tam další omezení - třeba multiply lane lze udělat s prvkem ze čtveřice float vektorů, ale už ne z jakékoli n-tice či s double. Pravda je, že na 3D grafiku to přesně sedí, takže tam to postačuje. RISC-V tak moc neznám.
Jestli měl Intel myslet víc dopředu - Cray přece jen mířil na jinou cílovou skupinu (i když o 20 let dřív), v té době jednoduché SSE asi nebyl špatný krok. Ostatně, i pozdější Aarch64 měl jenom jednoduchý Neon, SVE přišlo až později. Na druhé straně je třeba říct, že ty výsledky jsou o dost lepší - Graviton 3 (Aarch64 s SVE) mi počítal násobení matice vektorem (4*4 * 4) za půl taktu, zatímco x86-64 se mi povedlo dostat na 1.5 taktů. Možná se ale x86-64 s posledními Ryzen posunulo...
RISC-V SIMD je nejhorší SIMD co vůbec existuje, pravděpodobně navržený lidma, co SIMD nepoužívají.
Ono "spojovat" registry a definovat operace nad více registry je totálně nepraktická myšlenka, protože když mám 32 registrů, a udělám operace nad 4 registry (takže třeba ADD bude adresovat 4 + 4 + 4 registry), tak sice na oko zvětším šířku operace, ale efektivně se mi sníží počet registrů na 8, protože budu používat skupiny po 4 registrech. Ani radši nemluvím o tom, že SIMD kód potřebuje konstanty, a většinou něco netriviálního hodně konstant, takže toto celé je úplně k ničemu.
Radši ani nemluvím o tom, že RISC-V V má jen jeden maskovací registr, který je ještě sdílený se SIMD registrem - opět něco co nenajdeme v AVX-512 nebo SVE.
RISC-V V je prostě instrukční sada ušitá horkou jehlou, a úplně mám pocit, že snad Intel nebo někdo to úmyslně sabotoval :)
Zdá se, že jsou programátoři, kteří si myslí, že ten návrh je lepší než stovky SIMD s pevnou délkou a mě to také tak připadá i po tom, co jsem si něco na x86 zkusil napsat, kde zrovna délka prvku nebyla mocninou dvou a ani data nebyla zarovnaná. Ty začátky a konce daly dost zabrat a možnost nastavit, že se má pracovat jen s částí délky registru by hodně pomohla...
https://itnext.io/advantages-of-risc-v-vector-processing-over-x86-simd-c1b72f3a3e82
Pavle diky za clanek.
Zrovna nedavno jsem takovym zvlastnim zpusobem narazil na instrukce FMA4. Ze sady urcitych aplikaci nektere proste nevysvetlitelne umiraly na segfault. Trasovanim jsem nasel funkci v knihovne ktera to mela zpusobovat, ale na nic jsem neprisel. Uz jsem byl zoufaly a tak jsem nechal debuger at to diasembluje. Tak jsem zjistil ze to umira na instrukci VFNMADD. Kdyz jsem s pomoci Wikipedie zjistil ze FMA4 byly na Buldozerech spadla mi celist az na zem. Buildovani totiz probihalo na Ryzenu.
Vysvetleni se naslo. Slo o zapomenutou globalne nastavene CFLAGS a CPPFLAGS :-D
6. 10. 2022, 20:36 editováno autorem komentáře
Na SIMD je nejlepší použít nějakou knihovnu nebo intrinsics. Toto je cesta do pekla, protože mít jen základní aritmetiku je skoro na nic. Díky tomu, že člověk pracuje s vektorem je potřeba mnohem víc, různé konverze, shuffling, packing/unpacking, maskování, atd... A každá architektura to má trochu jiné (třeba shuffling na ARMu je totální noční můra).
Jinak jo, super článek
Těch knihoven jsou dnes už desítky. Mnoho z nich má i různé další funkce jako trigo, exp, atd... - a toto je zrovna něco co trvá hodně dlouho napsat a odladit.
Na druhou stranu intrinsics jsou někdy jediný způsob jak použít něco co není zrovna univerzální mezi různýma architekturama a nemá to wrappery v těch SIMD knihovnách. Třeba x86 architektura má hodně různých zajímavých instrukcí, které nemají ekvivalent na AArch64 a jiných architekturách.