Är en for-sats långsammare än att skriva kodrader efter varandra?

Permalänk

Är en for-sats långsammare än att skriva kodrader efter varandra?

Antar att vi ska göra beräkningar med lite trigonometri. Vi ska räkna distansen mellan varje x och y koordinat från centrum.

Vi kan göra detta med en for-sats som itererar. Eller så kan vi skriva kodraderna efter varandra.

Jag har hört att skriva kodrader efter varandra är snabbare än for-satser. Detta kallas vektorisering. Men stämmer detta?

Språket gäller: C

Permalänk
Medlem

Det du beskriver kallas loop unrolling, inte vektorisering, och lär göras av din kompilator om du slår på optimeringar.

Använd godbolt compiler explorer för att enkelt se vad olika kompilatorer väljer att göra med din kod.

Visa signatur

Spela Swemantle! Du vet att du vill.

Ibland har jag fel, men då är det någon annans fel.

Permalänk
Medlem
Skrivet av heretic16:

Antar att vi ska göra beräkningar med lite trigonometri. Vi ska räkna distansen mellan varje x och y koordinat från centrum.

Vi kan göra detta med en for-sats som itererar. Eller så kan vi skriva kodraderna efter varandra.

Jag har hört att skriva kodrader efter varandra är snabbare än for-satser. Detta kallas vektorisering. Men stämmer detta?

Språket gäller: C

Det heter "Loop Unrolling" (https://en.wikipedia.org/wiki/Loop_unrolling) och jag tror det var LÄNGE sen det ansågs som en optimering. Numera har CPUer cache (mer cache) och både CPUer och kompilatorer är smartare. Känns dumt att fylla upp kodcache med exakt samma instruktioner.

Men det finns säkert specialfall som någon annan kan fylla i här.

Permalänk
Medlem
Skrivet av heretic16:

Antar att vi ska göra beräkningar med lite trigonometri. Vi ska räkna distansen mellan varje x och y koordinat från centrum.

Vi kan göra detta med en for-sats som itererar. Eller så kan vi skriva kodraderna efter varandra.

Jag har hört att skriva kodrader efter varandra är snabbare än for-satser. Detta kallas vektorisering. Men stämmer detta?

Språket gäller: C

Det kallas loop unrolling. Vektorisering är när instruktioner som opererar på flera värden samtidigt används.

Förr var loop unrolling en vanlig optimering programmerarna gjorde, men nu för tiden är processorerna "bredare" och smartare så loop-instruktionerna tenderar att kunna exekvera samtidigt som instruktionerna som gör själva jobbet. Kompilatorn kan även tänkas göra sin egen loop unrolling (eller ännu bättre, vektorisering) om du är tydlig med att det fungerar ("restrict" t ex).

Men om det är verkligt prestandakritisk kod så är det bästa att prova olika alternativ och se vad som är snabbast, eller om det inte spelar någon roll.

Permalänk

Tack så mycket för svaren.
Men då vet jag att min kompilator sköter detta. Då behöver jag inte fundera på att själv göra en loop unrolling.

Permalänk
Skrivet av iXam:

Det heter "Loop Unrolling" (https://en.wikipedia.org/wiki/Loop_unrolling) och jag tror det var LÄNGE sen det ansågs som en optimering. Numera har CPUer cache (mer cache) och både CPUer och kompilatorer är smartare. Känns dumt att fylla upp kodcache med exakt samma instruktioner.

Men det finns säkert specialfall som någon annan kan fylla i här.

Nja, Loop Unrolling är fortfarande en högst relevant optimering, men kanske inte något man gör manuellt längre. En modern CPU med Out-of-Order-execution och spekulativ exekvering gör i stort sett loop unrolling i hårdvaran, men alla kompilatorer rullar upp loopar. Kör du exampelvis GCC på hög optimeringsnivå rullar den upp loopar ganska aggressivt. Det kan vara lärorikt att kika på den genererade koden i godbolt

Ursprungligen var tanken med unrolling att amortera av kostnaden för loop-overheaden på flera iterationer. Var det korta loopar med fix trip count kunde man bli av med loopvariabeln helt och hållet. Man fann dock snabbt att den stora finessen med loop unrolling var att unrolling exponerade flera kopior av loopkroppen och att detta öppnade för optimeringar mellan de olika iterationerna i loopen, exempelvis Common Subexpression Elimination, något som är svårt om kompilatorn bara ser en kopia av loopkroppen.

Vektorisering är en annan typ av optimering. Här handlar det om att kompilatorn upptäcker att de olika iterationerna i loopen är oberoende av varandra. Om accessmönstren stämmer kan kompilatorn lägga ut SIMD-instruktioner (AVX eller NEON/SVE) som köra flera iterationer av loopen parallellt. Sannolikheten för att kompilatorn ska kunna generera SIMD-kod ökar om du inte rullat upp loopen manuellt.

Det finns dock lägen när man vill göra en manuell loop unrolling. Exempelvis vid matrismultiplikationen kan man vilja göra en optimering som kallas Unroll-and-Jam om heuristiken i kompilatorn rullar upp fel loop.

Tag en skalär matrismultiplikation

for (i = 0, i < m; ++i) { for (j = 0, j < n; ++j) { tmp = c[i][j]; for (x = 0, x < k; ++x) { tmp += a[i][x] * b[x][j]; } c[i][j] = tmp; } }

Här beror den innersta loopen bara på i och j så de olika iterationerna i j-loopen är oberoende av varandra. Efter Unroll:

for (i = 0, i < m; ++i) { for (j = 0, i < n; j += 8) { tmp[0] = c[i][j + 0]; for (x = 0, x < k; ++x) { tmp[0] += a[i][x] * b[x][j + 0]; } c[i][j + 0] = tmp[0]; tmp[1] = c[i][j + 1]; for (x = 0, x < k; ++x) { tmp[1] += a[i][x] * b[x][j + 1]; } c[i][j + 1] = tmp[1]; ... tmp[7] = c[i][j + 7]; for (x = 0, x < k; ++x) { tmp[7] += a[i][x] * b[x][j + 7]; } c[i][j + 7] = tmp[7]; } }

Nu är det dags för Jam. Alla dessa loopkroppar är oberoende av varandra så vi kan köra alla i samma loop:

for (i = 0, i < m; ++i) { for (j = 0, i < n; j += 8) { tmp[0] = c[i][j + 0]; tmp[1] = c[i][j + 1]; ... tmp[7] = c[i][j + 7]; for (x = 0, x < k; ++x) { tmp[0] += a[i][x] * b[x][j + 0]; tmp[1] += a[i][x] * b[x][j + 1]; ... tmp[7] += a[i][x] * b[x][j + 7]; } c[i][j + 0] = tmp[0]; c[i][j + 1] = tmp[1]; ... c[i][j + 7] = tmp[7]; } }

Nu har vi fått en loop som läser och skriver 8 konsekutiva element ur b och c. Denna loop lämpar sig väl för just verktorisering. (Att vektorisera x-accesserna kan kompilatorn klara av på egen hand.)

Typo
Permalänk

Hur mycket brukar en kompilator optimera?

Permalänk
Medlem

Hur långt är ett snöre?

Visa signatur

The power of GNU compiles you!
"Often statistics are used as a drunken man uses lampposts -- for support rather than illumination."

Permalänk
Skrivet av kode:

Hur långt är ett snöre?

Nä. Jag håller inte med om detta svar.
Folk har jobbat på att optimera en kompilator i över 30 år och då skall det finnas ett sådant svar på hur mycket dom optimerar.

Tolkar som att du inte vet svaret.

Permalänk
Medlem
Skrivet av heretic16:

Nä. Jag håller inte med om detta svar.
Folk har jobbat på att optimera en kompilator i över 30 år och då skall det finnas ett sådant svar på hur mycket dom optimerar.

Det finns många kompilatorer. De optimerar olika saker olika bra. Och är överhuvudtaget olika bra på att optimera.
Hur mycket de försöker beror på vilka flaggor de ges. Hur bra de lyckas beror på hur källkoden ser ut.

Så en kompilator optimerar någonstans mellan ingenting alls och väldigt mycket.

För övrigt, vilken måttenhet vill du ha svar i?

Permalänk
Hedersmedlem

Ett sätt att mäta i är väl prestandaskillnad i ett gäng olika tester, som här: https://www.phoronix.com/review/gcc11-rocket-opts

Svaret då är att GCC 11 optimerar extremt mycket; -O0 ligger otroligt långt efter alla andra inställningar, ofta ger optimering 3x prestandan, men ibland långt mer ändå.

Visa signatur

Asus ROG STRIX B550-F / Ryzen 5800X3D / 48 GB 3200 MHz CL14 / Asus TUF 3080 OC / WD SN850 1 TB, Kingston NV1 2 TB + NAS / Corsair RM650x V3 / Acer XB271HU (1440p165) / LG C1 55"
Mobil: Moto G200

Permalänk
Medlem
Skrivet av heretic16:

Nä. Jag håller inte med om detta svar.
Folk har jobbat på att optimera en kompilator i över 30 år och då skall det finnas ett sådant svar på hur mycket dom optimerar.

Tolkar som att du inte vet svaret.

Men du har inte angett vilken kompilator du använder. Så rätt onödig/otrevlig kommentar från din sida.

Permalänk
Medlem
Skrivet av heretic16:

Nä. Jag håller inte med om detta svar.
Folk har jobbat på att optimera en kompilator i över 30 år och då skall det finnas ett sådant svar på hur mycket dom optimerar.

Tolkar som att du inte vet svaret.

Det var en väldigt vettig motfråga. Den typ av motfråga vet vi ju alla vad det innebär. Det går inte att besvara din fråga på det sättet du önskar utan det beror helt enkelt på kompilator samt om kompilatorn har olika möjliga flaggor som kan ändra beteende hos den specifika optimeringen du söker samt vad som är standardinställning.

Visa signatur

Windows 11 Pro | Intel i7 8700 | ASUS Prime Z370-P | Corsair 16GB 3000MHz | ASUS GTX 1080 | Fractal Design Define S | Corsair RM750x | Hyper 212 EVO

Permalänk
Medlem
Skrivet av heretic16:

Nä. Jag håller inte med om detta svar.
Folk har jobbat på att optimera en kompilator i över 30 år och då skall det finnas ett sådant svar på hur mycket dom optimerar.

Tolkar som att du inte vet svaret.

Vilken kompilator talar vi om? Är det gcc, LLVM, clang, intels egen eller Visual Studio? (Finns många fler!)
Folk har utvecklat bilar i 100 år, finns det ett svar på hur mycket fortare en bil går idag än i begynnelsen?
(Mäter vi hastighet eller restid, relevant då en del saker var bättre förr, innan trafikpropparna.)

Det finns switchar till kompilatorn som sparar den genererade assemblerkoden i läsbart skick. Det är väldigt givande att dyka ner där om man undrar vad de olika optimeringsstegen har för sig. Var beredd på att du inte kommer att känna igen din kod alls efter det att de högre optimeringsstegen har aktiverats.

Permalänk
Hedersmedlem
Skrivet av heretic16:

Hur mycket brukar en kompilator optimera?

Den intresserade kan ju också läsa dokumentationen (här för gcc).
Här står bland annat att -O1 aktiverar följande:

-fauto-inc-dec -fbranch-count-reg -fcombine-stack-adjustments -fcompare-elim -fcprop-registers -fdce -fdefer-pop -fdelayed-branch -fdse -fforward-propagate -fguess-branch-probability -fif-conversion -fif-conversion2 -finline-functions-called-once -fipa-modref -fipa-profile -fipa-pure-const -fipa-reference -fipa-reference-addressable -fmerge-constants -fmove-loop-invariants -fmove-loop-stores -fomit-frame-pointer -freorder-blocks -fshrink-wrap -fshrink-wrap-separate -fsplit-wide-types -fssa-backprop -fssa-phiopt -ftree-bit-ccp -ftree-ccp -ftree-ch -ftree-coalesce-vars -ftree-copy-prop -ftree-dce -ftree-dominator-opts -ftree-dse -ftree-forwprop -ftree-fre -ftree-phiprop -ftree-pta -ftree-scev-cprop -ftree-sink -ftree-slsr -ftree-sra -ftree-ter -funit-at-a-time

och det finns också kortare beskrivningar av dem.