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.