Hvorfor er ikke passere konstruere ved henvisning en felles optimalisering?

stemmer
23

Frem til i dag, hadde jeg alltid tenkt at anstendig kompilatorer automatisk konvertere struct pass-by-verdien til pass-by-referanse hvis struct er stor nok til at sistnevnte ville være raskere. Så langt jeg vet, dette virker som en no-brainer optimalisering. Men for å tilfredsstille min nysgjerrighet på om dette faktisk skjer, laget jeg en enkel test i både C ++ og D og så på produksjonen av både GCC og Digital Mars D. Begge insisterte på passerer 32-byte structs av verdi når alle funksjon i spørsmålet gjorde var å legge opp medlemmene og returnere verdier, uten endring av struct gått i. C ++ versjonen er nedenfor.

#include iostream.h

struct S {
    int i, j, k, l, m, n, o, p;
};

int foo(S s) {
    return s.i + s.j + s.k + s.l + s.m + s.n + s.o + s.p;
}

int main() {
    S s;
    int bar = foo(s);
    cout << bar;
}

Mitt spørsmål er, hvorfor pokker ville ikke noe sånt som dette bli optimalisert av kompilatoren til pass-by-referansen i stedet for faktisk å skyve alle disse inter på stakken?

Merk: Iler brytere brukt: GCC -O2 (. -O3 inlined foo ()), DMD -O -inline -release.

Edit: Selvfølgelig, i det generelle tilfelle semantikken forbikjørings-verdi g forbikjørings-referansen ikke vil være den samme, som for eksempel hvis kopi konstruktører er involvert eller den opprinnelige struct er modifisert i anropt. Men i en rekke reelle scenarier, vil semantikk være identiske i form av observerbar atferd. Dette er tilfeller jeg spør om.

Publisert på 16/02/2009 klokken 03:12
kilden bruker
På andre språk...                            


12 svar

stemmer
23

Ikke glem at i C / C ++ kompilatoren må være i stand til å kompilere en samtale til en funksjon kun basert på funksjonen erklæringen.

Gitt at innringere kanskje bruker bare denne informasjonen, er det ikke mulig for en kompilator for å kompilere funksjon for å dra nytte av optimalisering du snakker om. Den som ringer kan ikke vite funksjonen vil ikke endre noe, og slik at det ikke kan passere ref. Siden noen innringere kan passere verdi på grunn av mangel på informasjon, har som oppgave å være sammensatt antar pass-by-verdi, og alle må passere verdi.

Merk at selv om du merket parameteren som ' const', kompilatoren fortsatt ikke kan utføre optimalisering, fordi funksjonen kan ligge og kastet bort constness (dette er tillatt og godt definert så lenge objektet blir vedtatt i er faktisk ikke konst).

Jeg tror at for statiske funksjoner (eller de i en anonym namespace), kan kompilatoren muligens gjøre optimalisering du snakker om, siden funksjonen ikke har ekstern kobling. Så lenge den adressen til funksjonen ikke er gått over til andre rutiner eller lagres i en peker, bør det ikke være kallbart fra andre kode. I dette tilfellet kompilatoren kunne ha full kjennskap til alle innringere, så jeg antar at det kunne gjøre optimalisering.

Jeg er ikke sikker på om noen gjør (faktisk, jeg vil bli overrasket hvis noen gjør det, siden det sannsynligvis ikke kunne brukes veldig ofte).

Selvfølgelig, som programmerer (ved bruk av C ++) kan du tvinge kompilatoren for å utføre denne optimaliseringen ved å bruke const&parametere når det er mulig. Jeg vet at du spør hvorfor kompilatoren ikke kan gjøre det automatisk, men jeg antar at dette er den neste beste tingen.

Svarte 16/02/2009 kl. 03:29
kilden bruker

stemmer
10

Problemet er du spør kompilatoren for å ta en beslutning om prosjektets hensikt brukerkode. Kanskje jeg vil ha min super store struct å bli vedtatt av verdi, slik at jeg kan gjøre noe i kopien konstruktøren. Tro meg, noen der ute har noe de gyldig må bli kalt inn en kopi konstruktør for nettopp et slikt scenario. Bytte til en av dommeren vil omgå kopi konstruktøren.

Å ha dette være en kompilator generert avgjørelse ville være en dårlig idé. Årsaken er er at det gjør det umulig å resonnere om flyten av koden din. Du kan ikke se på en samtale, og vet hva det vil gjøre. Du må a) kjenne koden og b) gjett kompilatoren optimalisering.

Svarte 16/02/2009 kl. 03:21
kilden bruker

stemmer
10

Ett svar er at kompilatoren vil trenge for å oppdage at det kalles metoden ikke endre innholdet i struct på noen måte. Hvis den gjorde, så effekten av bestått ved henvisning vil avvike fra det som går forbi verdi.

Svarte 16/02/2009 kl. 03:19
kilden bruker

stemmer
4

Det er sant at kompilatorer i enkelte språk kan gjøre dette hvis de har tilgang til funksjonen blir kalt, og hvis de kan anta at den kalles funksjonen ikke vil være i endring. Dette er noen ganger referert til som global optimalisering og det virker sannsynlig at noen C eller C ++ kompilatorer ville faktisk optimalisere tilfeller som dette - mer sannsynlig ved innebygging koden for en slik triviell funksjon.

Svarte 16/02/2009 kl. 03:22
kilden bruker

stemmer
3

Jeg tror dette er definitivt en optimalisering du kan implementere (under noen forutsetninger, se siste avsnitt), men det er ikke klart for meg at det ville være lønnsomt. I stedet for å skyve argumenter på stakken (eller passerer dem gjennom registre, avhengig av ringer konvensjonen), vil du presse en peker som du ville lese verdier. Denne ekstra omvei ville koste sykluser. Det vil også kreve gått argument for å være i minnet (slik at du kan peke på det) i stedet for i registre. Det ville bare være en fordel hvis de postene som sendes hatt mange felt og funksjonen mottar posten bare lest noen få av dem. De ekstra sykluser bortkastede av indirekte måtte gjøre opp for sykler ikke bortkastet ved å skyve unødvendige felt.

Du kan bli overrasket over at den omvendte optimalisering, argument markedsføring , faktisk gjennomføres i LLVM. Dette omdanner en referanse argumentet til en verdi argument (eller et aggregat til skalarene) for interne funksjoner med et lite antall felt som bare leses fra. Dette er spesielt nyttig for språk som passerer nesten alt som referanse. Hvis du følger dette med døde argument eliminering , vil du heller ikke trenger å passere felt som ikke er berørt.

Det bærer nevne at optimaliseringer som endrer måten en funksjon som kalles fungerer bare når funksjonen blir optimalisert er intern til modulen blir kompilert (du får dette ved å erklære en funksjon statici C og med maler i C ++). Optimizer skal fikse ikke bare funksjon, men også alle meldere. Dette gjør slike optimaliseringer ganske begrenset i omfang med mindre du gjør dem på linken tid. I tillegg vil optimalisering aldri bli kalt når en kopi konstruktør er involvert (som andre plakater har nevnt), fordi det potensielt kan endre semantikk av programmet, som en god optimizer aldri skal gjøre.

Svarte 16/02/2009 kl. 04:17
kilden bruker

stemmer
2

Pass-by-referansen er bare syntetisk sukker for pass-by-adresse / pekeren. Så funksjonen må implisitt dereference en peker til å lese parameterverdien. Dereferencing pekeren kan være dyrere (hvis det i en sløyfe) deretter struct kopi for kopi-av-verdi.

Enda viktigere, som andre har nevnt, pass-by-referansen har forskjellige semantikk enn pass-by-verdi. constreferanser trenger ikke bety den refererte verdien endres ikke. andre funksjonskall kan endre refererte verdi.

Svarte 16/02/2009 kl. 04:41
kilden bruker

stemmer
2

Bytte fra av verdi til med henvisning vil endre signaturen til funksjonen. Hvis funksjonen er ikke statisk dette ville føre knytte feil for andre kompilering innretninger som ikke er klar over optimalisering du gjorde.
Faktisk den eneste måten å gjøre en slik optimalisering er av en slags post-linken global optimalisering fase. Disse er notorisk vanskelig å gjøre, men noen kompilatorer gjør dem til en viss grad.

Svarte 16/02/2009 kl. 03:33
kilden bruker

stemmer
2

Det er mange grunner til å bestå av verdi, og som har kompilatoren optimalisere ut din intensjon kan bryte koden din.

Eksempel, hvis den anropte funksjons modifiserer strukturen på noen måte. Hvis du hadde tenkt at resultatene skal sendes tilbake til den som ringer så vil du enten passere en peker / referanse eller returnere det selv.

Hva du spør kompilatoren å gjøre er å endre atferd av koden din, som ville bli betraktet som en kompilator feil.

Hvis du ønsker å gjøre optimalisering og passerer ved henvisning så for all del endre noens eksisterende funksjon / metodedefinisjoner å akseptere referanser; det er ikke alt som er vanskelig å gjøre. Du kan bli overrasket over brudd du føre uten å vite det.

Svarte 16/02/2009 kl. 03:26
kilden bruker

stemmer
1

Effektivt å sende en structhenvisning selv når funksjonen erklæringen viser pass-by-verdi er en felles optimalisering: det er bare at det vanligvis skjer indirekte via inlining, så det er ikke åpenbart fra den genererte koden.

Men for at dette skal skje, må kompilatoren å vite at callee doens't endre passerte objekt mens det kompilere den som ringer . Ellers vil det være begrenset av plattform / språk ABI som dikterer nøyaktig hvordan verdier sendes til funksjoner.

Det kan skje selv uten fletting!

Likevel vil visse kompilatorer gjøre å gjennomføre denne optimaliseringen selv i fravær av fletting, selv om omstendighetene er forholdsvis begrenset, i det minste på plattformer ved hjelp av SysV ABI (Linux, OSX, etc) på grunn av begrensninger av stabelen layout. Vurder følgende enkle eksempel, basert direkte på koden din:

__attribute__((noinline))
int foo(S s) {
    return s.i + s.j + s.k + s.l + s.m + s.n + s.o + s.p;
}

int bar(S s) {
    return foo(s);
}

Her, på språknivå barsamtaler foomed pass-by-verdi semantikk som kreves av C ++. Hvis vi studerer forsamlingen som genereres av gcc , men det ser ut som dette:

foo(S):
        mov     eax, DWORD PTR [rsp+12]
        add     eax, DWORD PTR [rsp+8]
        add     eax, DWORD PTR [rsp+16]
        add     eax, DWORD PTR [rsp+20]
        add     eax, DWORD PTR [rsp+24]
        add     eax, DWORD PTR [rsp+28]
        add     eax, DWORD PTR [rsp+32]
        add     eax, DWORD PTR [rsp+36]
        ret
bar(S):
        jmp     foo(S)

Merk at barbare direkte samtaler foo, uten å gjøre en kopi: barvil bruke samme kopi av ssom ble sendt til bar(på stakken). Spesielt det gjør ikke noen kopier som er implisert av språk semantikk (ignorerer som om ). Så gcc har utført nøyaktig optimalisering du ba om. Klang ikke gjør det selv: det gjør en kopi på stakken som det går til foo().

Dessverre, de tilfeller der dette kan fungere er ganske begrenset: SysV krever at disse store strukturene er overlevert stabelen i en bestemt posisjon, slik som for gjenbruk er bare mulig hvis callee forventer objektet i nøyaktig samme sted.

Det er mulig i foo/bareksemplet siden bar tar det Ssom den første parameter på samme måte som foo, og bargjør en hale anrop til foosom unngår behovet for den implisitte returadressen trykk som ellers ville ødelegge evnen til å gjenbruke stabelen argument.

For eksempel, hvis vi bare legge en + 1til kallet til foo:

int bar(S s) {
    return foo(s) + 1;
}

Trikset er ødelagt, siden nå stillingen som bar::ser annerledes enn plasseringen foovil forvente sitt sargument, og vi trenger en kopi:

bar(S):
        push    QWORD PTR [rsp+32]
        push    QWORD PTR [rsp+32]
        push    QWORD PTR [rsp+32]
        push    QWORD PTR [rsp+32]
        call    foo(S)
        add     rsp, 32
        add     eax, 1
        ret

Dette betyr ikke at den som ringer bar()må være helt trivielt skjønt. For eksempel kan det endre sin kopi av s, før den sendes sammen:

int bar(S s) {
    s.i += 1;
    return foo(s);
}

... og optimalisering vil bli bevart:

bar(S):
        add     DWORD PTR [rsp+8], 1
        jmp     foo(S)

I prinsippet er dette muligheten for denne typen optimalisering mye av vaktene i Win64 ringer konvensjonen som bruker en skjult peker å passere store strukturer. Dette gir en mye mer fleksibilitet i gjenbruk av eksisterende strukturer på stabelen eller andre steder for å implementere forbikjørings-referanse under dekslene.

inlining

Alt det til side, men den viktigste måten dette optimalisering skjer er via inlining.

For eksempel ved -O2kompilering alle klang, gcc og MSVC ikke gjør noen kopi av S objekt 1 . Både klang og gcc egentlig ikke opprette objektet i det hele tatt, men bare beregnet resultatet mer eller mindre direkte uten å henvise ubrukte felt. MSVC gjør bevilge stack plass for en kopi, men aldri bruker det: den fyller ut bare ett eksemplar av Sbare og leser fra det, akkurat som pass-by-referanse (MSVC genererer mye verre kode enn de to andre kompilatorer for denne saken).

Legg merke til at selv om foodet inlined inn mainkompilatorer også generere en egen frittstående kopi av foo()funksjon siden den har ekstern lenke og så kan brukes av dette objektet filen. I dette er kompilatoren begrenset av Application Binary Interface : den SysV ABI (for Linux) eller Win64 ABI (for Windows) definerer nøyaktig hvordan verdier må sendes, avhengig av type og størrelse av verdien. Store konstruksjoner er vedtatt av skjult peker, og kompilatoren må respektere at når kompilering foo. Det har også å respektere at kompilere noen som ringer til foonår foo ikke kan ses: siden det har ingen anelse om hva foovil gjøre.

Så det er svært lite vindu for kompilatoren å lage en effektiv optimalisering som forvandler pass-by-verdien til pass-by-referanse fordi:

1) Hvis det kan se både den som ringer og callee ( mainog fooi ditt eksempel), er det sannsynlig at callee vil bli inlined inn den som ringer hvis det er liten nok, og som funksjonen blir store og ikke-inlinable, effekten av faste kostnader ting som ringer konvensjonen overhead blitt relativt mindre.

2) Dersom kompilatoren ikke kan se både anroperen og anropte på samme tid to , vanligvis har det å kompilere hver henhold til plattformen ABI. Det er ingen muligheter for optimalisering av samtalen på samtalen området siden kompilatoren ikke vet hva callee vil gjøre, og det er ingen muligheter for optimalisering innenfor callee fordi kompilatoren har å gjøre konservative antagelser om hva den som ringer gjorde.


1 Mitt eksempel er litt mer komplisert at den opprinnelige en å unngå kompilatoren bare optimalisere alt bort helt (i særdeleshet, får du tilgang initialisert minne, slik at programmet ikke selv har definert atferd): Jeg fylle noen av feltene smed argcsom er en verdi kompilatoren kan ikke forutsi.

2. En kompilator kan se både "samtidig" betyr vanligvis at de er enten i den samme oversettelsesenheten eller at link-tid-optimalisering blir brukt.

Svarte 20/07/2018 kl. 00:20
kilden bruker

stemmer
1

På mange plattformer, store strukturer er faktisk vedtatt av referanse, men heller den som ringer vil forventes å passere en referanse til en kopi at funksjonen kan manipulere som den liker en eller kalt funksjonen vil bli forventet å lage en kopi av den konstruksjon, til hvilken den mottar en referanse, og deretter utføre noen manipulasjoner på kopien.

Mens det er mange situasjoner der kopioperasjoner kan faktisk bli utelatt, vil det ofte være vanskelig for en kompilator for å bevise at slike operasjoner kan bli eliminert. For eksempel, gitt:

struct FOO { ... };

void func1(struct FOO *foo1);
void func2(struct FOO foo2);

void test(void)
{
  struct FOO foo;
  func1(&foo);
  func2(foo);
}

er det ingen måte en kompilator kunne vite om fookan bli endret i løpet av utførelsen av func2( func1kunne ha lagret en kopi av foo1eller en peker avledet fra den i en fil-omfang gjenstand som deretter brukes av func2). Slike modifikasjoner, men bør ikke påvirke kopi av foo(dvs. foo2) mottatt func2. Dersom fooble vedtatt av referanse og func2ikke lage en kopi, handlinger som påvirker fooville feil påvirke foo2.

Merk at selv void func3(const struct FOO);ikke er menings: callee er lov å kaste bort const, og det normale asm kall konvensjonen fortsatt tillate callee å endre minne holder for-verdien kopi.

Dessverre er det relativt få tilfeller der undersøke den som ringer eller kalt funksjon isolert ville være tilstrekkelig til å bevise at en kopi operasjon kan sikkert sløyfes, og det er mange tilfeller der selv undersøke begge ville være utilstrekkelig. Dermed erstatter pass-by-verdi med pass-by-referanse er en vanskelig optimalisering som Utbetalingen er ofte tilstrekkelig til å rettferdiggjøre problemet.


Fotnote 1: For eksempel Windows x64 passerer gjenstander større enn 8 byte av ikke-const referansen (callee "eier" den spisse til minne). Dette betyr ikke bidra til å unngå kopiering i det hele tatt; motivasjonen er å gjøre alle funksjonsargument passe i 8 bytes slik at de danner en matrise på stakken (etter smitte registerargument å skygge plass), noe som gjør variadic funksjoner lett å implementere.

I motsetning gjør x86-64 System V hva spørsmålet beskriver for objekter som er større enn 16 byte: kopiere dem til stabelen. (Mindre gjenstander er pakket inn i opp til to registre.)

Svarte 19/07/2018 kl. 17:58
kilden bruker

stemmer
1

kompilatoren må være sikker på at struct som er gått (som er navngitt i ringer koden) i er ikke endret

double x; // using non structs, oh-well

void Foo(double d)
{
      x += d; // ok
      x += d; // Oops
}

void main()
{
     x = 1;
     Foo(x);
}
Svarte 16/02/2009 kl. 05:31
kilden bruker

stemmer
1

Vel, er det trivielle svaret at plasseringen av struct i minnet er forskjellig, og dermed dataene du passerer er annerledes. Jo mer komplisert svar, tror jeg, er threading.

Kompilatoren vil trenge for å oppdage en) som foo endrer ikke struct; b) at foo ikke gjør noen beregning av den fysiske plassering av struct elementene; Og c) at den som ringer, eller en annen tråd framsatt av anroperen, ikke endrer struct før foo blir kjørt ferdig.

I ditt eksempel, er det tenkelig at kompilatoren kan gjøre disse tingene - men minnet lagret er inkonsekvent og sannsynligvis ikke verdt å ta gjetning. Hva skjer hvis du kjører det samme programmet med en struct som har to millioner elementer?

Svarte 16/02/2009 kl. 04:06
kilden bruker

Cookies help us deliver our services. By using our services, you agree to our use of cookies. Learn more