Hur mycket optimerar en C-kompilator koden?

Permalänk

Hur mycket optimerar en C-kompilator koden?

En liten svår fråga. Men vi säger att jag har en kod som kopierar samma array 100 gånger, än fast det behövs bara en gång. Om jag sätter en optimeringsflagga, kommer då koden ta bort all onödiga iterationer eller kodrader då?

Vi kan ta detta exempel. Här är två kodsnuttar. Dom gör exakt samma sak. Skillnaden är att en är mer optimerad. Dock så visar Godbolt att angleanalysis2 genererar mindre assemblerkod än angleanalysis1.

Jag vet att man ska prioritera att skriva läsbar kod, istället för superavancerad som är optimerad. Man ska alltså låta kompilator sköta denna del. Men ibland så kan det vara bra att optimera för hand också igenom att minska på antalet iterationer.

Men vad kommer gå snabbast här enligt er?

void angleanalysis1(const float X[], const size_t row, const size_t column) { /* Constants and variables */ const size_t column_bytes = column * sizeof(float); const size_t row_minus_1 = row - 1; /* Create two vectors */ float* vector_A = (float*)malloc(column_bytes); float* vector_B = (float*)malloc(column_bytes); /* Find the angles between vectors from each cluster point */ size_t i, j, k; for (i = 0; i < row; i++) { /* Select the "centroid" */ float* centroid = &X[i * column]; /* Select vector A */ if (i < row_minus_1) { memcpy(vector_A, &X[(i+1) * column], column_bytes); /* Center the vector */ for (k = 0; k < column; k++) { vector_A[k] -= centroid[k]; } } /* Iterate the rest of the points */ for (j = 0; j < row; j++) { if (i != j) { /* Select vector B */ memcpy(vector_B, &X[j * column], column_bytes); /* Center the vector */ for (k = 0; k < column; k++) { vector_B[k] -= centroid[k]; } /* Compute the angle between vector A and vector B */ float angle = anglevector(vector_A, vector_B, column); } } } }

void angleanalysis2(const float X[], const size_t row, const size_t column) { /* Constants and variables */ const size_t column_bytes = column * sizeof(float); size_t i, j, k; /* Create two vectors */ float* vector_A = (float*)malloc(column_bytes); float* vector_B = (float*)malloc(column_bytes); /* Find the angles between vectors from each cluster point */ for (i = 0; i < row; i++) { /* Select the "centroid" */ float* centroid = &X[i * column]; /* Iterate the rest of the points */ for (j = 0; j < row; j++) { if (i != j) { /* Select vector A and vector B */ memcpy(vector_A, &X[i * column], column_bytes); memcpy(vector_B, &X[j * column], column_bytes); /* Center the vectors */ for (k = 0; k < column; k++) { vector_B[k] -= centroid[k]; vector_A[k] -= centroid[k]; } /* Compute the angle between vector A and vector B */ float angle = anglevector(vector_A, vector_B, column); } } } }

Permalänk
Medlem

Onödigt att gissa. https://godbolt.org/

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:

En liten svår fråga. Men vi säger att jag har en kod som kopierar samma array 100 gånger, än fast det behövs bara en gång. Om jag sätter en optimeringsflagga, kommer då koden ta bort all onödiga iterationer eller kodrader då?

Vi kan ta detta exempel. Här är två kodsnuttar. Dom gör exakt samma sak. Skillnaden är att en är mer optimerad. Dock så visar Godbolt att angleanalysis2 genererar mindre assemblerkod än angleanalysis1

Mängd assemblerkod säger inte så mycket om hastigheten.
Om du vill veta vilken som blir snabbast så får du testa och mäta körtiden.

Hur bra en kompilator optimerar beror på kompilatorn, hårdvaran, och kompileringsflaggor.

Vad som är bäst sätt att skriva kod, och hur kompilatorer optimerar, och hur hårdvara fungerar har utvecklats parallellt.
Så vad som var bäst sätt att skriva kod för 10 år sedan - på den tidens hårdvara och kompilatorer - behöver inte vara bäst sätt att göra det idag, och vad som är bäst sätt att skriva kod idag - för dagens hårdvara och kompilatorer - behöver inte vara det bästa sättet om 10 år.

Så hur skall man skriva koden för att den skall bli så snabb som möjligt? Hur mycket optimerar kompilatorn? Hur långt är ett snöre?
Ja, det beror på, och det går inte att ge generalla och allmängiltiga svar.

Permalänk
Medlem
Skrivet av heretic16:

En liten svår fråga. Men vi säger att jag har en kod som kopierar samma array 100 gånger, än fast det behövs bara en gång. Om jag sätter en optimeringsflagga, kommer då koden ta bort all onödiga iterationer eller kodrader då?

Kompilatorn är faktiskt så smart att den transformerar om all din kod till att använda en optimal algoritm för ändamålet. Om du sätter optimeringsflaggan så kommer den t.ex. byta ut bubblesort mot quicksort, o.s.v.

Permalänk
Skrivet av Perkoff:

Kompilatorn är faktiskt så smart att den transformerar om all din kod till att använda en optimal algoritm för ändamålet. Om du sätter optimeringsflaggan så kommer den t.ex. byta ut bubblesort mot quicksort, o.s.v.

Så om jag anropar flera funktioner med olika argument så kommer den anropa en helt funktion med alla argument?

Dvs, allt på en gång.

Permalänk
Medlem
Skrivet av heretic16:

En liten svår fråga. Men vi säger att jag har en kod som kopierar samma array 100 gånger, än fast det behövs bara en gång. Om jag sätter en optimeringsflagga, kommer då koden ta bort all onödiga iterationer eller kodrader då?

Ja. Det är viktigt att tänka på sådana saker om du skriver benchmarkskod. Dagens kompilatorer tar effektivt bort allt dödkött och utför alla "fasta" beräkningar i din kod redan innan du kör programmet. Du måste skriva kod som inte kan optimeras, dvs resultatet av din kod skall vara "okänt".

Permalänk
Medlem
Skrivet av Perkoff:

Kompilatorn är faktiskt så smart att den transformerar om all din kod till att använda en optimal algoritm för ändamålet. Om du sätter optimeringsflaggan så kommer den t.ex. byta ut bubblesort mot quicksort, o.s.v.

Hmm, har du lust att utveckla detta lite mera? Jag känner inte alls igen vad du säger. Kompilatorer är duktiga på mikrooptimeringar, eller enkla implementeringar av vektoroperationer, men att byta hela algoritmer låter som ett suspekt uttalande..

Visa signatur

Archlinux, Sway och Rust, vad mer behövs?

Permalänk
Medlem

Det finns ju en nivå till på detta - hur din CPU kör spekulativa beräkningar för att snabba upp saker ytterligare. Det blir som en tvåstegsraket - kompilatorn kan optimera ordentligt och CPU:n kommer optimera/fuska ytterligare för att snabba upp beräkningen.

Visa signatur

There are two hard things in computer science: cache invalidation, naming things, and off-by-one errors.

@oscar:prutt.party / monotux@freenode

Permalänk
Medlem

Folk som faktiskt är i behov av att optimera, de tar tid på sina saker. Och ofta är det nån enstaka rutin det handlar om, och inte alltför sällan en databasläsning inblandad. Då har du SQL motorn att ta hänsyn till också.

Fast din fråga kanske var mer av akademisk karaktär och du är nyfiken på hur avancerade kompilatorer är idag?

Korta svaret är att inline assembler sällan gör det snabbare idag jämfört mot 20 år sedan då det användes frekvent. Att den byter ut hela algoritmer som nån ovan nämnde är jag mer skeptisk till, men läser gärna på om någon har info om det.

Visa signatur

Processor: Motorola 68000 | Klockfrekvens: 7,09 Mhz (PAL) | Minne: 256 kB ROM / 512 kB RAM | Bussbredd: 24 bit | Joystick: Tac2 | Operativsystem: Amiga OS 1.3

Permalänk
Medlem

Kul att tråden lever fortfarande. Funktionen returnerar void och tar bara const-parametrar. Gör den något alls (vettigt) förutom läcker minne? (malloc körs, ingen free)

Permalänk
Medlem

I embedded värden är det vanligt att inte köra med allt för mycket optimering.
De lägre optimerings flaggorna till gcc kan till och med göra assemblern läsbar *hint*

De högre kan göra väldigt mysko saker...

Du har lite issues i den där koden, några har påpekats.

Permalänk

Nu har jag sett en del uttalanden som jag känner att jag måste kommentera.

Skrivet av Perkoff:

Kompilatorn är faktiskt så smart att den transformerar om all din kod till att använda en optimal algoritm för ändamålet. Om du sätter optimeringsflaggan så kommer den t.ex. byta ut bubblesort mot quicksort, o.s.v.

Nej, det har jag svårt att tro. Kompilatorn har visserligen ganska mycket frihet att göra som den vill, bara den löser samma uppgift som den ursprungliga koden, men att byta en algoritm mot en annan är lite att ta i. Det finns gränser för hur mycket frihet kompilatorn har. Den genererade koden måste bete på samma sätt som den ursprungliga koden och om man skickar in "felaktiga" parametrar så skall den generade koden få fel på samma sätt. Att byta en Bubblesort mot en Quicksort är görbart, man kan mönstermatcha den inkommande koden och känna igen att det är en Bubblesort och stoppa in ett anrop till quicksort istället. Det kan tänkas att någon kompilatorskrivare tyckt det var ett cool hack och gjort det (på kul), men det ändrar på funktionens stackanvändning. I embeddedvärlden kan man ha en begränsad stack och valt en sämre sorteringsalgoritm just för att den använder lite stack. Då vill man inte att kompilatorn byter till en algoritm som resulterar i stack overflow.

Skrivet av talonmas:

Folk som faktiskt är i behov av att optimera, de tar tid på sina saker. Och ofta är det nån enstaka rutin det handlar om, och inte alltför sällan en databasläsning inblandad. Då har du SQL motorn att ta hänsyn till också.

Fast din fråga kanske var mer av akademisk karaktär och du är nyfiken på hur avancerade kompilatorer är idag?

Korta svaret är att inline assembler sällan gör det snabbare idag jämfört mot 20 år sedan då det användes frekvent. Att den byter ut hela algoritmer som nån ovan nämnde är jag mer skeptisk till, men läser gärna på om någon har info om det.

Du har helt rätt i att man skall mäta in innan börja handoptimera koden. Det spelar ingen roll om man snabbar upp en funktion 100x om man inte spederar mer än 1% av tiden i den. Om allt drunknar i databasoperationer kommer det inte att märkas på körtiden hur mycket du än filar på dina algoritmer. Då är det nog bättre att fundera på om man kan formulera om sina querys så de går fortare.

Här är verktyg som Intels VTune till stor hjälp. Det är lite tröskel att komma igång med VTune, men VTune kan mer än gamla vanliga profilers. Det kan vara värt att lägga lite tid på att lära sig.

Dock likställer du "optimering" med speed-optimering men kompilatorerna kan även optimera för size (-Os för GCC), dvs. generera så liten kod som möjligt. Det är viktigt inom embedded-industrin att hålla ner kodstorleken. Om koden sväller och inte ryms i den krets man valt är det problematiskt...

Lite förenklat kan vi se det som att valet står mellan att optimera mot ett så litet program som möjligt eller mot att exekvera så få instruktioner som möjligt. Oftast går speed och size hand i hand. Färre instruktioner tar kortare tid att köra. Klassiska optimeringar som

  • Dead Code Elimination - Kommer ingen använda resultatet av denna uträkning finns det ingen anledning att utföra den.

  • Constant Folding/Constant Propagation - Om kompilatorn vet att operanderna till en operation har konstanta värden kan man räkna ut resultatet vid compile time och slippa göra saker under runtime

  • Peephole Optimizations - Leta efter fall där det finns ett effektivare sätt att utföra en instruktionssekvens. Exempelvis adresserings-moder med pre- eller post-inkrement. Två flugor med en smäll om man kan göra inkrementet samtidigt som minnesaccessen.

handlar typiskt om att inte göra saker i onödan. Den onödiga instruktionen som kompilatorn lyckas optimera bort sparar både exekveringstid och kodstorlek.

En ren size-optimering är Unreachble Code Removal. Hittar kompilatorn kod som den bevisa att den aldrig kommer exekveras kan den ta bort den koden. Man kan tycka att det inte borde finnas onåbar kod i ett program. Ingen borde skriva kod efter return eller ta med en case-sats för värden som aldrig kommer förekomma, men efter att andra optimeringar gjort sitt jobb visar det sig ofta att kod blir onåbar.

Vi har andra optimeringar som är kodstorleksneutrala men syftar till att dynamiskt köra färre instruktioner. Exempel på en sådan är Loop-invariant code hosting. Om kompilatorn kan identifiera att (del-)uttyck i en loop inte beror på loop-variabeln kan man räkna ut dessa utanför loopen. Exempelvis i

int a[...][...]; for (int i = 0; i < x; ++i) { for (int j = 0; j < y; ++j) { a[i][j] = ... } }

är a[i] bara beroende av i och kan utföras utanför den innersta loopen:

int a[...][...]; for (int i = 0; i < x; ++i) { int *tmp = &a[i][0]; for (int j = 0; j < y; ++j) { tmp[j] = ... } }

(Denna optimering höjer registertrycket lite så om tmp tränger ut någon annan variabel från ett register är optimeringen inte helt kodstorleksneutral.)

Exempel på optimeringar som offrar size för speed är

  • Function Inlining - Här byter man ut funktionsanropet mot kroppen på den anropade funktionen. Detta möjliggör att funktionskroppen kan skräddarsys för de aktuella parametrarna. Om en del av parametrarna var konstanter kommer man kunna applicera Constant Folding, förenkla uttryck och kanske upptäcka att ett villkor blir sant/falskt och en halva av en if-else-sats blir unreachable. Om man tog adressen av en parametrarna kanske den inte längre har adresen tagen och man kan registerallokera variablen istället för att ha den på stacken.

  • Loop Unrolling - Vid loop unrolling lägger man flera kopior av loopkroppen efter varandra. Detta ger kompilatorn möjlighet att optimera mellan loopkropparna vilket kan ge utdelning ibland. Viktigare är dock att det ger en modern CPU med Out-of-Order-execution längre sammanhängande sektioner med rak kod.

Man kan även offra speed för size. Exempelvis genom att hitta likadana instruktionssekvenser på olika ställen i koden. Om det är i slutet av ett antal funktioner, där man typiskt skall poppa ett antal värden från stacken och returnera, kan man helt enkelt byta ut den sekvensen mot ett hopp rakt in i en annan funktion som kör samma instruktionenssekvens. Om instruktionssekvensen är tillräckligt stor kan dett vara värt att bryta ut den till en separat subrutin och göra funktionsanrop.

Skrivet av riche:

I embedded värden är det vanligt att inte köra med allt för mycket optimering.
De lägre optimerings flaggorna till gcc kan till och med göra assemblern läsbar *hint*

De högre kan göra väldigt mysko saker...

Du har lite issues i den där koden, några har påpekats.

Japp, den optimerade koden har ofta förvandlats till något som inte alls ser ut som källkoden, men beroende på vilken kompilator du kör finns det ofta kommentarer insprängda i den genererade koden som "pekar tillbaka" till källkoden. Det är dock inte säkert att hjälper när man försöker förstå vad som egentligen händer

Om du tillhör "embeddedvärlden" rekommenderar jag att du kör med size-optimering påslaget. Man kör i princip alla optimeringar utom de som offrar size för speed. De klassiska kompilatoroptimeringarna ger ofta märkbar förbättring av både kodstorlek och exekeringstid.

Skrivet av heretic16:

En liten svår fråga. Men vi säger att jag har en kod som kopierar samma array 100 gånger, än fast det behövs bara en gång. Om jag sätter en optimeringsflagga, kommer då koden ta bort all onödiga iterationer eller kodrader då?

Vi kan ta detta exempel. Här är två kodsnuttar. Dom gör exakt samma sak. Skillnaden är att en är mer optimerad. Dock så visar Godbolt att angleanalysis2 genererar mindre assemblerkod än angleanalysis1.

Jag vet att man ska prioritera att skriva läsbar kod, istället för superavancerad som är optimerad. Man ska alltså låta kompilator sköta denna del. Men ibland så kan det vara bra att optimera för hand också igenom att minska på antalet iterationer.

Och nu till TS fråga: Hur mycket optimerar en kompilator? En modern kompilator optimerar mycket. Du skall dock ha realistiska förväntningar. Kompilatorn måste hålla sig till språkstandarden och vara beredd på att en variabel kan innehålla vilka värden som helst. Du som programmerare vet nästan alltid mer än kompilatorn. Du vet saker som att värdena kommer hålla sig i en viss range och att a alltid kommer vara mindre än b och c. Detta gör att man kanske förväntar sig att kompilatorn skall optimera saker som den inte får göra om den skall hålla sig till språkstandarden och kunna hantera alla randvärden korrekt.

Kompilatorn kommer inte att byta ut en kass algoritm mot en bättre.

Dina två exempel gör inte exakt samma sak. Detta är ett exempel på när du vet mer än kompilatorn. Finns anglevector i en annan modul vet inte kompilatorn något om den funktionen annat än den info som finns i prototypen.
Kompilatorn kan tänkas upptäcka att saker du gör i en inre loop är invariant och lyfta ut detta ut loopen men det kan bara ske om den kan bevisa att anglevector inte ändrar i minnesblocket vector_A pekar på. Annars måste memcpy(vector_A, ...); utföras för att samma värden skall skickas in till anglevector. Svaret på din fråga är alltså: det beror på. Har du sagt restrict i prototypen? Är det pekare till const-data? Du får helt enkelt testa och se vilken kod som den kompilator genererar.

Men som @talonmas så klokt påpekade, har du mätt och konstaterat att detta är ett problem? Om det inte är tidskritiskt, skriv så enkel och lättläst kod som möjligt.

För den som är intresserad av kompilatoroptimeringar finns mycket mer på Wikipedia.

Vill ni pröva på lite lågnivåprogrammering rekommenderar jag Human Resource Machine. Man skall skriva små program som styr en arbetare (CPU). Har för mig att det var ett femtiotal uppgifter att lösa och det finns både speed- och size-challenges. Kostar en 50-lapp på Steam och är väl värt sina pengar.

Permalänk
Medlem
Skrivet av Ingetledigtnamn:

Nu har jag sett en del uttalanden som jag känner att jag måste kommentera.

Nej, det har jag svårt att tro. Kompilatorn har visserligen ganska mycket frihet att göra som den vill, bara den löser samma uppgift som den ursprungliga koden, men att byta en algoritm mot en annan är lite att ta i. Det finns gränser för hur mycket frihet kompilatorn har. Den genererade koden måste bete på samma sätt som den ursprungliga koden och om man skickar in "felaktiga" parametrar så skall den generade koden få fel på samma sätt. Att byta en Bubblesort mot en Quicksort är görbart, man kan mönstermatcha den inkommande koden och känna igen att det är en Bubblesort och stoppa in ett anrop till quicksort istället. Det kan tänkas att någon kompilatorskrivare tyckt det var ett cool hack och gjort det (på kul), men det ändrar på funktionens stackanvändning. I embeddedvärlden kan man ha en begränsad stack och valt en sämre sorteringsalgoritm just för att den använder lite stack. Då vill man inte att kompilatorn byter till en algoritm som resulterar i stack overflow.

Det är visserligen görbart att (i de flesta fall) identifiera en Bubblesort och byta mot en Quicksort, men det är inte en optimering en kompilator bör göra då algoritmerna inte har ekvivalenta resultat. Bubblesort är en stabil sorteringsalgoritm till skillnad från Quicksort, och i en hel del fall kan det spela roll.

Permalänk
Skrivet av Erik_T:

Det är visserligen görbart att (i de flesta fall) identifiera en Bubblesort och byta mot en Quicksort, men det är inte en optimering en kompilator bör göra då algoritmerna inte har ekvivalenta resultat. Bubblesort är en stabil sorteringsalgoritm till skillnad från Quicksort, och i en hel del fall kan det spela roll.

Tyckte jag sade att det var en optimering som en komplator inte skulle göra. Att den inte är stabil är en anledning till att den definitivt inte får göra den då det ändrar funktionens resultat.

Stavfel
Permalänk
Medlem
Skrivet av Ingetledigtnamn:

Nu har jag sett en del uttalanden som jag känner att jag måste kommentera.

Nej, det har jag svårt att tro. Kompilatorn har visserligen ganska mycket frihet att göra som den vill, bara den löser samma uppgift som den ursprungliga koden, men att byta en algoritm mot en annan är lite att ta i. Det finns gränser för hur mycket frihet kompilatorn har. Den genererade koden måste bete på samma sätt som den ursprungliga koden och om man skickar in "felaktiga" parametrar så skall den generade koden få fel på samma sätt. Att byta en Bubblesort mot en Quicksort är görbart, man kan mönstermatcha den inkommande koden och känna igen att det är en Bubblesort och stoppa in ett anrop till quicksort istället. Det kan tänkas att någon kompilatorskrivare tyckt det var ett cool hack och gjort det (på kul), men det ändrar på funktionens stackanvändning. I embeddedvärlden kan man ha en begränsad stack och valt en sämre sorteringsalgoritm just för att den använder lite stack. Då vill man inte att kompilatorn byter till en algoritm som resulterar i stack overflow.

Du har helt rätt i att man skall mäta in innan börja handoptimera koden. Det spelar ingen roll om man snabbar upp en funktion 100x om man inte spederar mer än 1% av tiden i den. Om allt drunknar i databasoperationer kommer det inte att märkas på körtiden hur mycket du än filar på dina algoritmer. Då är det nog bättre att fundera på om man kan formulera om sina querys så de går fortare.

Här är verktyg som Intels VTune till stor hjälp. Det är lite tröskel att komma igång med VTune, men VTune kan mer än gamla vanliga profilers. Det kan vara värt att lägga lite tid på att lära sig.

Dock likställer du "optimering" med speed-optimering men kompilatorerna kan även optimera för size (-Os för GCC), dvs. generera så liten kod som möjligt. Det är viktigt inom embedded-industrin att hålla ner kodstorleken. Om koden sväller och inte ryms i den krets man valt är det problematiskt...

Lite förenklat kan vi se det som att valet står mellan att optimera mot ett så litet program som möjligt eller mot att exekvera så få instruktioner som möjligt. Oftast går speed och size hand i hand. Färre instruktioner tar kortare tid att köra. Klassiska optimeringar som

  • Dead Code Elimination - Kommer ingen använda resultatet av denna uträkning finns det ingen anledning att utföra den.

  • Constant Folding/Constant Propagation - Om kompilatorn vet att operanderna till en operation har konstanta värden kan man räkna ut resultatet vid compile time och slippa göra saker under runtime

  • Peephole Optimizations - Leta efter fall där det finns ett effektivare sätt att utföra en instruktionssekvens. Exempelvis adresserings-moder med pre- eller post-inkrement. Två flugor med en smäll om man kan göra inkrementet samtidigt som minnesaccessen.

handlar typiskt om att inte göra saker i onödan. Den onödiga instruktionen som kompilatorn lyckas optimera bort sparar både exekveringstid och kodstorlek.

En ren size-optimering är Unreachble Code Removal. Hittar kompilatorn kod som den bevisa att den aldrig kommer exekveras kan den ta bort den koden. Man kan tycka att det inte borde finnas onåbar kod i ett program. Ingen borde skriva kod efter return eller ta med en case-sats för värden som aldrig kommer förekomma, men efter att andra optimeringar gjort sitt jobb visar det sig ofta att kod blir onåbar.

Vi har andra optimeringar som är kodstorleksneutrala men syftar till att dynamiskt köra färre instruktioner. Exempel på en sådan är Loop-invariant code hosting. Om kompilatorn kan identifiera att (del-)uttyck i en loop inte beror på loop-variabeln kan man räkna ut dessa utanför loopen. Exempelvis i

int a[...][...]; for (int i = 0; i < x; ++i) { for (int j = 0; j < y; ++j) { a[i][j] = ... } }

är a[i] bara beroende av i och kan utföras utanför den innersta loopen:

int a[...][...]; for (int i = 0; i < x; ++i) { int *tmp = &a[i][0]; for (int j = 0; j < y; ++j) { tmp[j] = ... } }

(Denna optimering höjer registertrycket lite så om tmp tränger ut någon annan variabel från ett register är optimeringen inte helt kodstorleksneutral.)

Exempel på optimeringar som offrar size för speed är

  • Function Inlining - Här byter man ut funktionsanropet mot kroppen på den anropade funktionen. Detta möjliggör att funktionskroppen kan skräddarsys för de aktuella parametrarna. Om en del av parametrarna var konstanter kommer man kunna applicera Constant Folding, förenkla uttryck och kanske upptäcka att ett villkor blir sant/falskt och en halva av en if-else-sats blir unreachable. Om man tog adressen av en parametrarna kanske den inte längre har adresen tagen och man kan registerallokera variablen istället för att ha den på stacken.

  • Loop Unrolling - Vid loop unrolling lägger man flera kopior av loopkroppen efter varandra. Detta ger kompilatorn möjlighet att optimera mellan loopkropparna vilket kan ge utdelning ibland. Viktigare är dock att det ger en modern CPU med Out-of-Order-execution längre sammanhängande sektioner med rak kod.

Man kan även offra speed för size. Exempelvis genom att hitta likadana instruktionssekvenser på olika ställen i koden. Om det är i slutet av ett antal funktioner, där man typiskt skall poppa ett antal värden från stacken och returnera, kan man helt enkelt byta ut den sekvensen mot ett hopp rakt in i en annan funktion som kör samma instruktionenssekvens. Om instruktionssekvensen är tillräckligt stor kan dett vara värt att bryta ut den till en separat subrutin och göra funktionsanrop.

Japp, den optimerade koden har ofta förvandlats till något som inte alls ser ut som källkoden, men beroende på vilken kompilator du kör finns det ofta kommentarer insprängda i den genererade koden som "pekar tillbaka" till källkoden. Det är dock inte säkert att hjälper när man försöker förstå vad som egentligen händer

Om du tillhör "embeddedvärlden" rekommenderar jag att du kör med size-optimering påslaget. Man kör i princip alla optimeringar utom de som offrar size för speed. De klassiska kompilatoroptimeringarna ger ofta märkbar förbättring av både kodstorlek och exekeringstid.

Och nu till TS fråga: Hur mycket optimerar en kompilator? En modern kompilator optimerar mycket. Du skall dock ha realistiska förväntningar. Kompilatorn måste hålla sig till språkstandarden och vara beredd på att en variabel kan innehålla vilka värden som helst. Du som programmerare vet nästan alltid mer än kompilatorn. Du vet saker som att värdena kommer hålla sig i en viss range och att a alltid kommer vara mindre än b och c. Detta gör att man kanske förväntar sig att kompilatorn skall optimera saker som den inte får göra om den skall hålla sig till språkstandarden och kunna hantera alla randvärden korrekt.

Kompilatorn kommer inte att byta ut en kass algoritm mot en bättre.

Dina två exempel gör inte exakt samma sak. Detta är ett exempel på när du vet mer än kompilatorn. Finns anglevector i en annan modul vet inte kompilatorn något om den funktionen annat än den info som finns i prototypen.
Kompilatorn kan tänkas upptäcka att saker du gör i en inre loop är invariant och lyfta ut detta ut loopen men det kan bara ske om den kan bevisa att anglevector inte ändrar i minnesblocket vector_A pekar på. Annars måste memcpy(vector_A, ...); utföras för att samma värden skall skickas in till anglevector. Svaret på din fråga är alltså: det beror på. Har du sagt restrict i prototypen? Är det pekare till const-data? Du får helt enkelt testa och se vilken kod som den kompilator genererar.

Men som @talonmas så klokt påpekade, har du mätt och konstaterat att detta är ett problem? Om det inte är tidskritiskt, skriv så enkel och lättläst kod som möjligt.

För den som är intresserad av kompilatoroptimeringar finns mycket mer på Wikipedia.

Vill ni pröva på lite lågnivåprogrammering rekommenderar jag Human Resource Machine. Man skall skriva små program som styr en arbetare (CPU). Har för mig att det var ett femtiotal uppgifter att lösa och det finns både speed- och size-challenges. Kostar en 50-lapp på Steam och är väl värt sina pengar.

Intressant läsning. Tack för att du tog dig tid att skriva en liten uppsats om ämnet

Visa signatur

Processor: Motorola 68000 | Klockfrekvens: 7,09 Mhz (PAL) | Minne: 256 kB ROM / 512 kB RAM | Bussbredd: 24 bit | Joystick: Tac2 | Operativsystem: Amiga OS 1.3