Inline udvidelse - Inline expansion
I computing , inline ekspansion , eller indbygningen , er en manuel eller compiler optimering , der erstatter en funktion opkald websted med kroppen af den kaldte funktion. Inline-udvidelse svarer til makroudvidelse , men sker under kompilering uden at ændre kildekoden (teksten), mens makroudvidelse opstår inden kompilering og resulterer i anden tekst, der derefter behandles af compileren.
Inline er en vigtig optimering, men har komplicerede effekter på ydeevnen. Som en tommelfingerregel forbedrer noget indfletning hastigheden med meget mindre pladsomkostninger, men overskydende indfletning vil skade hastigheden på grund af inline-kode, der forbruger for meget af instruktionscachen , og koster også betydelig plads. En undersøgelse af den beskedne akademiske litteratur om inline fra 1980'erne og 1990'erne findes i Peyton Jones & Marlow 1999.
Oversigt
Inline-udvidelse svarer til makroudvidelse, da kompilatoren placerer en ny kopi af funktionen hvert sted, den kaldes. Inline-funktioner kører lidt hurtigere end de normale funktioner, da funktionsopkaldsudgifter gemmes, men der er en hukommelsesstraf. Hvis en funktion er angivet 10 gange, indsættes der 10 kopier af funktionen i koden. Derfor er indlejring bedst for små funktioner, der ofte kaldes. I C ++ er medlemsfunktionerne i en klasse, hvis de er defineret i klassedefinitionen, angivet som standard (ingen grund til at bruge det integrerede nøgleord); Ellers er nøgleordet nødvendigt. Compileren ignorerer muligvis programmørens forsøg på at integrere en funktion, især hvis den er særlig stor.
Inline-udvidelse bruges til at eliminere tidsomkostningerne (overskydende tid), når en funktion kaldes. Det bruges typisk til funktioner, der udføres ofte. Det har også en pladsfordel til meget små funktioner og er en muliggør transformation til andre optimeringer .
Uden integrerede funktioner bestemmer kompilatoren, hvilke funktioner der skal integreres. Programmøren har ringe eller ingen kontrol over, hvilke funktioner der er indlejret, og hvilke der ikke er. At give denne grad af kontrol til programmøren muliggør anvendelse af applikationsspecifik viden til valg af hvilke funktioner der skal integreres.
Normalt, når en funktion påberåbes, overføres kontrol til dens definition af en filial- eller opkaldsinstruktion. Med inlining falder kontrol direkte igennem til koden for funktionen uden en filial eller opkaldsinstruktion.
Kompilatorer implementerer normalt udsagn med inline. Loopforhold og looplegemer har brug for doven evaluering . Denne egenskab er opfyldt, når koden til beregning af loopbetingelser og looplegemer er angivet. Ydelsesovervejelser er en anden grund til at integrere udsagn.
Inden for funktionelle programmeringssprog efterfølges inline-ekspansion normalt af beta-reduktionstransformationen .
En programmør kan integrere en funktion manuelt gennem kopiering og indsætning af programmering som en engangsoperation på kildekoden . Imidlertid foretrækkes andre metoder til at kontrollere inline (se nedenfor), fordi de ikke udfælder fejl, der opstår, når programmøren overser en (muligvis modificeret) duplikeret version af den oprindelige funktionsdel, mens der rettes en fejl i den inline-funktion.
Effekt på ydeevne
Den direkte virkning af denne optimering er at forbedre tidseffektiviteten (ved at eliminere opkaldsomkostninger) på bekostning af forværret pladsforbrug (på grund af duplikering af funktionsorganet). Kodeudvidelsen på grund af duplikering af funktionsorganet dominerer bortset fra enkle tilfælde, og den direkte virkning af inline-ekspansion er således at forbedre tiden til prisen for pladsen.
Den primære fordel ved inline-udvidelse er imidlertid at muliggøre yderligere optimeringer og forbedret planlægning på grund af forøgelse af funktionslegemet, da bedre optimering er mulig på større funktioner. Den ultimative indvirkning af inline-udvidelse på hastigheden er kompliceret på grund af flere effekter på hukommelsessystemets ydeevne (primært instruktionscache ), som dominerer ydeevnen på moderne processorer: afhængigt af det specifikke program og cachen kan indlejring af bestemte funktioner øge eller mindske ydeevnen .
Virkningen af indlejring varierer afhængigt af programmeringssprog og program på grund af forskellige grader af abstraktion. På tvingende sprog på lavere niveau som C og Fortran er det typisk en hastighedsforøgelse på 10-20% med mindre indflydelse på kodestørrelse, mens det i mere abstrakte sprog kan være væsentligt mere vigtigt på grund af antallet af lag, der fjernes, med et ekstremt eksempel er Self , hvor en kompilator så forbedringsfaktorer på 4 til 55 ved at linie.
De direkte fordele ved at eliminere et funktionsopkald er:
- Det eliminerer instruktioner, som kræves for en funktion opkald , både i den kaldende funktion og i callee: placere argumenter på stakken eller i registre, funktionskaldet selv, funktion prolog , så i returnere funktionen epilog , den tilbagevenden redegørelse , og derefter få returværdien tilbage og fjernelse af argumenter fra stakke og gendannelse af registre (om nødvendigt).
- På grund af ikke behov for registre for at overføre argumenter, reducerer det registerspild .
- Det eliminerer at skulle passere referencer og derefter henføre dem, når du bruger opkald ved henvisning (eller opkald efter adresse eller opkald ved deling ).
Den primære fordel ved inline er dog de yderligere optimeringer, det giver mulighed for. Optimeringer, der krydser funktionsgrænser, kan udføres uden behov for interprocedurel optimering (IPO): når inline er udført, bliver yderligere intra proceduremæssige optimeringer ("globale optimeringer") mulige på det udvidede funktionsorgan. For eksempel:
- En konstant, der sendes som et argument, kan ofte spredes til alle forekomster af den matchende parameter, eller en del af funktionen kan "hejses ud" af en løkke (via loop-invariant kodebevægelse ).
- Registreringstildeling kan foretages på tværs af det større funktionsorgan.
- Optimeringer på højt niveau, såsom flugtanalyse og halen duplikering , kan udføres i et større omfang og være mere effektive, især hvis den kompilator, der implementerer disse optimeringer, primært er afhængig af intra-procedurel analyse.
Disse kan gøres uden indlejring, men kræver en betydeligt mere kompliceret kompilator og linker (hvis opkald og callee er i separate kompilationsenheder).
Omvendt kan en sprogspecifikation i nogle tilfælde tillade, at et program giver yderligere antagelser om argumenter for procedurer, som det ikke længere kan foretage, efter at proceduren er angivet, hvilket forhindrer nogle optimeringer. Smartere kompilatorer (som Glasgow Haskell Compiler ) vil spore dette, men naiv inlining mister disse oplysninger.
En yderligere fordel ved indlejring til hukommelsessystemet er:
- Fjernelse af grene og opbevaring af kode, der udføres tæt sammen i hukommelsen, forbedrer ydeevnen til instruktionscache ved at forbedre referencelokaliteten (rumlig lokalitet og sekvens af instruktioner). Dette er mindre end optimeringer, der specifikt er målrettet mod sekvens, men er betydningsfulde.
De direkte omkostninger ved indlejring er øget kodestørrelse på grund af duplikering af funktionsdelen på hvert opkaldssted. Det gør dog ikke altid gøre dette, nemlig i tilfælde af meget korte funktioner, hvor funktionen legeme er mindre end størrelsen af et funktionskald (ved den opkaldende, herunder indlæg og returværdi håndtering), såsom trivielle adgangsmetoder eller mutator metoder (getters og setters); eller til en funktion, der kun bruges ét sted, i hvilket tilfælde den ikke duplikeres. Således kan indlejring minimeres eller elimineres, hvis det optimeres til kodestørrelse, som det ofte er tilfældet i indlejrede systemer .
Inlining pålægger også en omkostning for ydeevnen på grund af kodeudvidelsen (på grund af dobbeltarbejde), der skader instruktionens cacheydelse. Dette er mest betydningsfuldt, hvis arbejdssættet i programmet (eller en varm sektion med kode) inden udvidelsen passer ind i et niveau i hukommelseshierarkiet (f.eks. L1-cache ), men efter udvidelse passer det ikke længere, hvilket resulterer i hyppige cache savner på det niveau. På grund af den markante forskel i ydeevne på forskellige niveauer i hierarkiet skader dette ydeevnen betydeligt. På det højeste niveau kan dette resultere i øgede sidefejl , katastrofal forringelse af ydeevnen på grund af thrashing , eller at programmet slet ikke kører. Denne sidste er sjælden i almindelige desktop- og serverapplikationer, hvor kodestørrelsen er lille i forhold til tilgængelig hukommelse, men kan være et problem for ressourcebegrænsede miljøer såsom indlejrede systemer. En måde at afbøde dette problem på er at opdele funktioner i en mindre hot inline sti ( hurtig sti ) og en større kold ikke-inline sti (langsom sti).
Indsættelse af sårende ydeevne er primært et problem for store funktioner, der bruges mange steder, men det break-even-punkt, ud over hvilket inlining reducerer ydeevnen, er vanskeligt at bestemme og afhænger generelt af nøjagtig belastning, så det kan være genstand for manuel optimering eller profil -styret optimering . Dette er et lignende problem med andre kodeudvidelsesoptimeringer , som f.eks. Loopudrulning , hvilket også reducerer antallet af behandlede instruktioner, men kan reducere ydeevnen på grund af dårligere cache-ydeevne.
Den nøjagtige virkning af inlining på cacheydelsen er kompliceret. For små cachestørrelser (meget mindre end arbejdssættet før udvidelse) dominerer den øgede sekvensalitet, og indlejring forbedrer cacheydelsen. For cachestørrelser tæt på arbejdssættet, hvor indlejring udvider arbejdssættet, så det ikke længere passer i cachen, dominerer dette, og cacheydelsen falder. For cachestørrelser, der er større end arbejdssættet, har indlejring ubetydelig indvirkning på cacheydelsen. Endvidere kan ændringer i cache-design, såsom videresendelse af belastning , opveje stigningen i cache-ulykker.
Compiler support
Kompilatorer bruger forskellige mekanismer til at bestemme, hvilke funktionsopkald der skal være inline disse kan omfatte manuelle tip fra programmører til specifikke funktioner sammen med overordnet kontrol via kommandolinjemuligheder . Inlining udføres automatisk af mange compilers på mange sprog baseret på vurderingen af, om inlining er gavnlig, mens det i andre tilfælde kan specificeres manuelt via compiler- direktiver , typisk ved hjælp af et kaldet nøgleord eller compiler-direktiv inline . Typisk antyder dette kun, at indlejring er ønsket, snarere end at kræve indlejring, med kraften fra tipet varierende efter sprog og kompilator.
Typisk holder kompilatorudviklere ovennævnte ydeevneproblemer i tankerne og indarbejder heuristik i deres compilere, der vælger, hvilke funktioner der skal integreres for at forbedre ydeevnen i stedet for at forværre den i de fleste tilfælde.
Implementering
Når compileren har besluttet at integrere en bestemt funktion, er det normalt simpelt at udføre selve inlining-operationen. Afhængigt af, om de compiler inlines funktioner på tværs af kode på forskellige sprog, compileren kan gøre indbygningen på enten en højt niveau mellemliggende repræsentation (ligesom abstrakte syntaks træer ) eller et lavt niveau mellemliggende repræsentation. I begge tilfælde beregner kompilatoren simpelthen argumenterne , gemmer dem i variabler svarende til funktionens argumenter og indsætter derefter funktionens brødtekst på opkaldsstedet.
Linkere kan også udføre funktionsinline. Når en linker integrerer funktioner, kan den muligvis integrere funktioner, hvis kilde ikke er tilgængelig, såsom biblioteksfunktioner (se optimering af linktid ). Et runtime-system kan også integreres. Run-time inlining kan bruge dynamisk profiloplysninger til at træffe bedre beslutninger om, hvilke funktioner der skal integreres, som i Java Hotspot-kompilatoren .
Her er et simpelt eksempel på inline-udvidelse udført "manuelt" på kildeniveau i C-programmeringssproget :
int pred(int x)
{
if (x == 0)
return 0;
else
return x - 1;
}
Før inline:
int func(int y)
{
return pred(y) + pred(0) + pred(y+1);
}
Efter inline:
int func(int y)
{
int tmp;
if (y == 0) tmp = 0; else tmp = y - 1; /* (1) */
if (0 == 0) tmp += 0; else tmp += 0 - 1; /* (2) */
if (y+1 == 0) tmp += 0; else tmp += (y + 1) - 1; /* (3) */
return tmp;
}
Bemærk, at dette kun er et eksempel. I en faktisk C-applikation ville det være at foretrække at bruge en indbygget sprogfunktion såsom parametriserede makroer eller inline-funktioner til at fortælle kompilatoren at transformere koden på denne måde. Det næste afsnit viser måder at optimere denne kode på.
Inflining ved samling makroekspansion
Assembler-makroer giver en alternativ tilgang til indlejring, hvorved en sekvens af instruktioner normalt kan genereres inline ved makroudvidelse fra en enkelt makrokildesætning (med nul eller flere parametre). En af parametrene kan være en mulighed for alternativt at generere en engangs separat subrutine indeholdende sekvensen og behandlet i stedet for et inline kald til funktionen. Eksempel:
MOVE FROM=array1,TO=array2,INLINE=NO
Heuristik
En række forskellige heuristikker er blevet undersøgt med henblik på inline. Normalt har en indlejringsalgoritme et bestemt kodebudget (en tilladt stigning i programstørrelse) og sigter mod at integrere de mest værdifulde opkaldssteder uden at overskride dette budget. I denne forstand modelleres mange inline-algoritmer normalt efter Knapsack-problemet . For at bestemme hvilke opkaldssider, der er mere værdifulde, skal en inline-algoritme estimere deres fordel - dvs. det forventede fald i udførelsestiden. Almindeligt bruger inlinere profiloplysninger om hyppigheden af udførelsen af forskellige kodestier til at estimere fordelene.
Ud over profilering af oplysninger anvender nyere just-in-time kompilatorer flere mere avancerede heuristikker, såsom:
- Spekulere i hvilke kodestier der vil resultere i den bedste reduktion i udførelsestid (ved at muliggøre yderligere kompilatoroptimeringer som følge af inline) og øge den opfattede fordel ved sådanne stier.
- Adaptiv justering af fordel-per-omkostning-tærsklen for inlining baseret på størrelsen på kompileringsenheden og mængden af kode, der allerede er angivet.
- Gruppering af underrutiner i klynger og inlineering af hele klynger i stedet for ental underrutiner. Her gætter den heuristiske klynger ved at gruppere de metoder, for hvilke kun en ordentlig delmængde af klyngen fører til en dårligere præstation end overhovedet intet.
Fordele
Selve inline-udvidelsen er en optimering, da den eliminerer overhead fra opkald, men det er meget vigtigere som en muliggør transformation . Når kompilatoren udvider et funktionslegeme i forbindelse med sit opkaldssted - ofte med argumenter, der kan være faste konstanter - kan det muligvis udføre en række forskellige transformationer, der ikke var mulige før. For eksempel kan en betinget gren vise sig at være altid sand eller altid falsk på dette særlige opkaldssted. Dette kan igen muliggøre eliminering af død kode , loop-invariant kodebevægelse eller induktionsvariabel eliminering .
I C-eksemplet i det foregående afsnit findes der masser af optimeringsmuligheder. Compileren kan følge denne rækkefølge af trin:
- De
tmp += 0udsagn i linjerne markeret (2) og (3) ikke gøre noget. Compileren kan fjerne dem. - Betingelsen
0 == 0er altid sand, så kompilatoren kan erstatte linjen markeret (2) med den deraf følgendetmp += 0(som ikke gør noget). - Compileren kan omskrive betingelsen
y+1 == 0tily == -1. - Compileren kan reducere udtrykket
(y + 1) - 1tily. - Udtrykkene
yogy+1kan ikke begge være lig nul. Dette gør det muligt for compileren at eliminere en test. - I udsagn som
if (y == 0) return yværdien afyer kendt i kroppen og kan angives.
Den nye funktion ser ud som:
int func(int y)
{
if (y == 0)
return 0;
if (y == -1)
return -2;
return 2*y - 1;
}
Begrænsninger
Komplet inline-udvidelse er ikke altid mulig på grund af rekursion : rekursivt inline-udvidelse af opkaldene afsluttes ikke. Der er forskellige løsninger, såsom at udvide en afgrænset mængde eller analysere opkaldsgrafen og bryde sløjfer ved bestemte noder (dvs. ikke udvide en eller anden kant i en rekursiv sløjfe). Et identisk problem opstår i makroekspansion, da rekursiv ekspansion ikke ophører, og løses typisk ved at forbyde rekursive makroer (som i C og C ++).
Sammenligning med makroer
Traditionelt på sprog som C blev inline-udvidelse gennemført på kildeniveau ved hjælp af parametriserede makroer . Brug af ægte inline-funktioner, som findes i C99 , giver flere fordele i forhold til denne tilgang:
- I C udfører makroopkald ikke typekontrol eller kontrollerer endda, at argumenter er velformede, hvorimod funktionsopkald normalt gør.
- I C kan en makro ikke bruge returordet med samme betydning som en funktion ville gøre (det ville få den funktion, der spurgte udvidelsen, til at ophøre snarere end makroen). Med andre ord kan en makro ikke returnere noget, der ikke er resultatet af det sidste udtryk, der blev påberåbt inde i det.
- Da C-makroer kun bruger tekstudskiftning, kan dette resultere i utilsigtede bivirkninger og ineffektivitet på grund af reevaluering af argumenter og rækkefølge .
- Compilerfejl i makroer er ofte vanskelige at forstå, fordi de henviser til den udvidede kode snarere end den kode, som programmøren skrev. Således er fejlretningsoplysninger for indbygget kode normalt mere nyttigt end for makroudvidet kode.
- Mange konstruktioner er akavet eller umulige at udtrykke ved hjælp af makroer eller bruger en væsentlig anden syntaks. Inline-funktioner bruger den samme syntaks som almindelige funktioner og kan let indlæses og ikke-inline.
Mange compilere kan også integrere nogle rekursive funktioner ; rekursive makroer er typisk ulovlige.
Bjarne Stroustrup , designeren af C ++, kan lide at understrege, at makroer bør undgås, hvor det er muligt, og går ind for omfattende brug af inline-funktioner.
Udvælgelsesmetoder
Mange kompilatorer integrerer aggressivt funktioner, hvor det er gavnligt at gøre det. Selvom det kan føre til større eksekverbare filer , er aggressiv inline alligevel blevet mere og mere ønskeligt, da hukommelseskapaciteten er steget hurtigere end CPU-hastigheden. Inlining er en kritisk optimering i funktionelle sprog og objektorienterede programmeringssprog , der stoler på at give tilstrækkelig kontekst til deres typisk små funktioner til at gøre klassiske optimeringer effektive.
Sprogstøtte
Mange sprog, herunder Java og funktionelle sprog , leverer ikke sprogkonstruktioner til inline-funktioner, men deres compilere eller tolke udfører ofte aggressiv inline-udvidelse. Andre sprog giver konstruktioner til eksplicitte hints, generelt som compiler direktiver (pragmas).
I Ada-programmeringssproget findes der en pragma for integrerede funktioner.
Funktioner i Common Lisp kan defineres som inline ved inline erklæringen som sådan:
(declaim (inline dispatch))
(defun dispatch (x)
(funcall
(get (car x) 'dispatch) x))
Den Haskell compiler GHC forsøger at inline funktioner eller værdier, der er små nok, men må indbygges kan bemærkes eksplicit ved hjælp af et sprog pragma:
key_function :: Int -> String -> (Bool, Double)
{-# INLINE key_function #-}
C og C ++
C og C ++ har et inline nøgleord, der begge fungerer som et kompileringsdirektiv - angiver, at indføring er ønsket, men ikke påkrævet - og ændrer også synligheden og linkadfærden . Synlighedsændringen er nødvendig for at gøre det muligt for funktionen at blive inline via standard C-værktøjskæden, hvor kompilering af individuelle filer (snarere oversættelsesenheder ) efterfølges af sammenkædning: for at linkeren skal være i stand til at integrere funktioner skal de specificeres i header (skal være synlig) og markeret inline (for at undgå tvetydighed fra flere definitioner).
Se også
Bemærkninger
Referencer
- Chen, WY; Chang, PP; Conte, TM; Hwu, WW (sep. 1993). "Virkningen af kodeudvidelsesoptimeringer på instruktion af cache-design" (PDF) . IEEE-transaktioner på computere . 42 (9): 1045-1057. doi : 10.1109 / 12.241594 . HDL : 2142/74513 .
- Peyton Jones, Simon ; Marlow, Simon (september 1999). Secrets of the Glasgow Haskell Compiler Inliner (Teknisk rapport). CS1 maint: modløs parameter ( link )
eksterne links
- " Eliminering af virtuelle funktionsopkald i C ++ - programmer " af Gerald Aigner og Urs Hölzle
- " Reduktion af indirekte funktionsopkald i C ++ - programmer " af Brad Calder og Dirk Grumwald
- ALTO - En Link-Time Optimizer til DEC Alpha
- " Avancerede teknikker " af John R. Levine
- " Hele programoptimering med Visual C ++ .NET " af Brandon Bray