To s tim restrict je asi uzitecne vedet. Sice presne nevim, jak to dokaze prekladac zajistit - asi nedokaze vedet, ze nepredavam stejne pole ze? Ale dokazu si predstavit mit zjednodusene memcpy, strcmp a ja nevim co jeste.
Jinak netusite jak se dnes prekladaji veci typu FF, kodeky atd. v linux distrech? U kodeku je asi brutalni optimalizace, ale vektorizuji se i jine veci? sam vim jen o NumPy.
C++ překladač nedokáže zajisit, že je restrict opravdu restrict, prostě se věří programátorovi, že to udělal správně a pointery nealiasují stejnou proměnnou. Pokud by se programátor zmýlil, je to undefined behavior.
Rust překladač to zajistit dokáže, protože má informace o lifetime a aliasingu proměnných. V Rustu je všechno restrict by default, zjednodušeně řečeno dvě write reference nemohou aliasovat stejnou proměnnou, takže Rust překladač může označit všechno jako restrict a z toho důvodu může být Rust kód autovektorizován lépe.
Zjistit to bohužel také nedokáže, jen přepodkládá, že to tak je. Takže, když napíšete program, kde to neplatí, tak máte nedefinované chování.
Takový program jde v Rustu napsat jen s použitím unsafe. Bez unsafe není možné v Rustu napsat program, kde by byla jedna proměnná aliasovaná dvěma write referencemi nebo read a write referencí současně. Takže Rust, narozdíl od C++ dokáže zajistit, že je všechno restrict.
Je to reseno bidne.. pokud se dela rucni optimalizace (v asm) tak je to prevazne na nejaky typicky bottleneck - kritickou smycku.
Viz libjpeg-turbo - ma sice hezky SIMD framework s optionalitou per funkci - ale implementovana je v asm pouze baseline (8bit) cast kodedku, a 12-bit extended neni podporovana vubec (je to tvrde vypnuto s #define SIMD 0).. jsme po tom patrali a duvod je, ze C implementace je spolecna (napr. DCT/iDCT), ale meni se datove typy - jednou to je char, jednou to je short - se to prelozi dvakrat, prejmenuji exporty a pak se dva objekty slinkuji, pokud ma libka podporovat oba profily. A nikdo si nedal praci, aby tu simd cas udelal se shorty. Pak je na nekterych platformach float varianta, ale znova povolena jen pro 8-bit.
Uz to dualni kompilovani je fuj.. a ted od takoveho projektu chcete, aby se spravne prekladal napr. pro multi-arch na macu (x86 + arm64)... neni to mozne.
Nektere funkce jsme se snazili prepsat do SIMD asm - ale pak jsme zjistili, ze je lepsi nechat prekladac optimalizovat ten kod a udelat auto-vektorizaci - vykon byl cca o 20% lepsi pro "C" kod, nez asm. Ono totiz v asm to napisete naivne - treba tak, ze pouzivate stejny registr.. ale prekladac udela neco, co pouziva zdanlive vice registru.. ale pak cpu je z toho nadsenej, protoze muze vyuzit tu miliardu tranzistoru a delat register rename - v kodu od prekladace je mene dependencies, ktere pri rucni praci snadno prehlidnete.
Je to reseno bidne.. pokud se dela rucni optimalizace (v asm) tak je to prevazne na nejaky typicky bottleneck - kritickou smycku.
Osvědčilo se mi používat SIMD intrinsic místo ASM. Je to dostatečně blízko CPU, kdy v podstatě píšu, jakou chci použít instrukci, a zároveň to dává překladači velký prostor k optimalizacím. Alokaci registrů i instruction re-scheduling dělá překladač a tohle typicky umí udělat lépe než člověk. clang navíc dokáže SIMD kód někdy transformovat do úplně jiných SIMD instrukcí, pokud si myslí, že to bude rychlejší (a typicky je to rychlejší).
Z mých zkušeností je na optimalizaci SIMD kódu clang lepší než gcc a generuje rychlejší kód a naopak na optimalizaci obyčejného kód bez SIMD je zase lepší gcc.
Priklady kodu, ktere clang vektorizuje lepe nez GCC jsou vitany v GCC bugzille.
Tady to máte: https://godbolt.org/z/d4oE11red
Není to o autovektorizaci, ale o optimalizaci SIMD kódu. Nejdřív to dobré. Oba překladače udělají loop unrolling, to se tak nějak předpokládá. Dál už je ale ve všem lepší clang:
Instruction scheduling
clang se snaží dávat všechny loady hodně na začátek kódu, což je dobré pro skrytí latence RAM, když data nejsou v cache. Tohle zvládne i po loop unrollingu, umí dát loady z unrollovaného cyklu na začátek. gcc to udělá tak nějak napůl, loop unrolling tam je, ale ne moc chytře, gcc to prostě dá všechno za sebe. Nějaký pokus o následný instruction reordering je i v případě gcc, ale není to tak dobré, jako u clangu.
Optimalizace
gcc udělá, co je ve zdrojáku napsáno, použije celkem 8x 512-bitový load z RAM (vmovdqu64) a pak data transformuje v registrech. clang pochopí, co ten kód dělá a místo 8x 512-bitového loadu použije 16x 256-bitový packed move with sign extend load (vpmovsxbw), čímž si ušetří pozdější transformace dat v registrech. Tohle se zdá kontraintuitivní, proč by mělo být 16 loadů lepších než 8 loadu, ale skutečně je, takový kód se vykoná rychleji a clang to ví a dokáže SIMD kód takto transformovat.
Ve výsledku je kód generovaný clangem výrazně rychlejší a také kratší.
Bug si na to založte sami, pokud vám to za to stojí. Za mě to není bug, generovaný kód v obou případech funguje, jen ten z clangu je lepší.
Toto ale není jednoznačné - v tomto případě clang používá mnohem víc memory loads, takže komplexnější benchmark může klidně v tomto případě ztrácet, protože to může brzdit další thread, který ten memory access potřebuje. Ale toto je naprosto nemožné zjistit nějakým microbenchmarkem, který testuje pořád jen tento kód.
Obecně souhlasím s tím, že menší kód je lepší, protože zase zabere míň v instruction cache, ale fakt záleží na tom, kolik cyklů se kde spáli a kolik threadů bojuje o zdroje.
Ještě bych doplnil, že v tomto případě dělá GCC přesně to co se po něm chce, a clang ten SIMD kód dál transformuje, což může být přesně to, co se po něm nechce. A toto je jeden z důvodů, proč v poslední době radší píšu přímo asm než instrinsics, v případech, kde mi na tom fakt záleží. Překladače se mě snaží přechytračit, ale ne vždycky je ten výsledek dobrý.
19. 3. 2025, 09:48 editováno autorem komentáře
V mých testech do vycházelo jednoznačně, clangem generovaný kód byl asi o 30% rychlejší než kód z gcc a to i v případě mnoha threadů (multithread processing je náš standardní use-case).
Jinak tohle je spíš příklad, že clang dokáže udělat celkem pokročilé transformace SIMD kódu, kdy klidně použije úplně jiné instrukce, než jaké jsou ve zdrojáku. U gcc jsem něco takového nikdy neviděl.
U těch intrincisc je skutečně otázka, jestli se od překladače čeká, že přeloží to, co uživatel napsal a nebo jestli to má dále transformovat. GCC ale také provádí další transformace na intrincisc, ale jinak než LLVM.
Ale benchmarků s ručně psanými vektory je málo a tak jsem vyplnil jsem bug https://gcc.gnu.org/bugzilla/show_bug.cgi?id=119368
Kód co běží správně, ale je pomalý, nebo velký, nebo se překládá nesmyslně dlouho je také považován za bug.
19. 3. 2025, 10:54 editováno autorem komentáře
Já tento případ ale jako bug nevidím. Mám radši překladač, co dělá, co po něm chci. Když chci 512-bit memory load, tak asi nechci 2x256-bit memory load, a třeba k tomu mám své důvody.
S clang začínám mít právě tyto problémy, kde kód je pomalejší jen proto, že clang se rozhodl, že něco takto transformuje, ale já to vůbec nechci. Největši peklo je unrolling cyklů, které jsou pro alignment nebo tail loops, to by člověk nevěřil, co ten překladač je schopný udělat.
Knihovna Eigen ma taky vektorizovane operace, pokud je pouziti simd povoleno. Ale zavisi to na downstream programu, jestli vektorizaci povoli. A co se tyce treba Ubuntu, tak tam skoro nic povolene neni, neb se stale jede architektura x86-64-v1. Blbe pak je, ze nejde kombinovat knihovny se simd a bez nich, pokud je Eigen v public API (kvuli zarovnani promennych a pameti).
Nejake optimalizace taky ma OpenCV a PCL knihovna. Zkratka vse, kde se musi pracovat s velkymi daty.
restrict říká, že pointery neodkazují na stejné místo v paměti.
Pokud dělám s každým prvkem operaci a[i] += b[i], tak
je docela problém pokud pointery inicializuji jako
```
void add_array2_to_array1(int* arr1, const int* arr2, size_t len) {
for (size_t i = 0; i < len; ++i) {
arr1[i] += arr2[i];
}
}
int a[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
add_array2_to_array1(a+1, a, 9);
```
protože, každá operace přepíše následující prvek v poli.
Překladač musí pesimisticky počítat s tím, že programátor funkci přesně tímto způsobem zavolá a nemůže přičtení druhého pole vektorizovat.
Restrict říká, že se pole nepřekrývají.
Co vím, tak je přes SSE optimalizovaná knihovna Eigen a přes SSE/AVX pak části OpenCV (případně knihovny Intel MKL, Intel Performance Primitives, FFTW), ale nevím jak široce se používají.