Waarom zijn verwijzingen naar inline-functies toegestaan?

Ik heb twee vragen:

1) Waarom zijn verwijzingen naar inline-functies toegestaan ​​in C++?
Ik heb gelezen dat de code van inline-functies gewoon wordt gekopieerd naar de functieaanroepinstructie en dat er geen geheugentoewijzing tijdens het compileren is in inline-functies. Dus waarom kan er een aanwijzer bestaan ​​naar een inline-functie, aangezien er geen vast geheugenadres is voor inline-functies?

2) Bekijk de onderstaande code:

inline void func()    
{
    int n=0;
    cout<<(&n);
} 

Moet het niet elke keer dat func()wordt aangeroepen verschillende waarden van het adres van func()afdrukken?
[Omdat ik denk dat elke keer dat inline functiecode wordt gekopieerd, de lokale variabelen opnieuw moeten worden toegewezen (terwijl in het geval van normale functies opnieuw initialisatie plaatsvindt)]

Ik ben een beginner en ik heb deze vraag gesteld omwille van mijn conceptversterking. Corrigeer me als ik ergens fout ben.


Antwoord 1, autoriteit 100%

1) Waarom zijn verwijzingen naar inline-functies toegestaan ​​in c++?

Omdat inline-functies functies zijn zoals alle andere, en ernaar verwijzen is een van de dingen die u met functies kunt doen. Inline-functies zijn in dit opzicht gewoon niet speciaal.

Ik heb gelezen dat de code van inline-functies wordt gekopieerd naar de functieaanroepinstructie en dat er geen geheugentoewijzingen voor compileren zijn in inline-functies.

Jij (en misschien het materiaal dat je hebt gelezen) hebt twee verwante en gelijknamige concepten door elkaar gehaald.

Een inline-functie wordt gedefinieerd in alle vertaaleenheden die deze gebruiken, terwijl een niet-inline-functie slechts in één vertaaleenheid wordt gedefinieerd zoals vereist door de regel met één definitie. Dat is wat een inline-declaratie van een functie betekent; het versoepelt de regel met één definitie, maar geeft ook de aanvullende eis dat het moet worden gedefinieerd in alle vertaaleenheden die het gebruiken (wat niet mogelijk zou zijn geweest als de odr niet was versoepeld).

Inline-uitbreiding (of inlining) is een optimalisatie, waarbij een functieaanroep wordt vermeden door de aangeroepen functie naar het frame van de aanroeper te kopiëren. Een functieaanroep kan inline worden uitgebreid, of de functie nu inline is gedeclareerd of niet. En een functie die inline is gedeclareerd, is niet noodzakelijkerwijs inline uitgebreid.

Een functie kan echter niet inline worden uitgebreid in een vertaaleenheid waar deze niet is gedefinieerd (tenzij de optimalisatie van de koppelingstijd de uitbreiding uitvoert). Daarom maakt de vereiste om in alle TU’s te zijn gedefinieerd die de inline-declaratie toestaat, ook de inline-uitbreiding van de functie mogelijk door de functie te definiëren in alle TU’s die deze aanroepen. Maar de optimalisatie is niet gegarandeerd.

2) Moet het niet elke keer dat func() wordt aangeroepen verschillende adreswaarden van n afdrukken?

Inline-uitbreiding zorgt er wel voor dat de lokale variabelen in het frame van de beller worden geplaatst, ja. Maar hun locatie zal, ongeacht de uitbreiding, verschillen als de oproepen afkomstig zijn van afzonderlijke frames.

Er wordt meestal een normale, niet-uitgebreide versie gegenereerd van elke functie die inline is uitgebreid. Als het adres van een functie wordt genomen, verwijst het naar die niet-uitgebreide functie. Als de compiler kan bewijzen dat alle aanroepen van een functie inline zijn, kan de compiler ervoor kiezen om de niet-uitgebreide versie helemaal niet te leveren. Dit vereist dat de functie een interne koppeling heeft, en het nemen van het adres van de functie maakt zo’n bewijs meestal erg moeilijk of onmogelijk.


Antwoord 2, autoriteit 43%

Het inlinetrefwoordwas oorspronkelijk een hint naar de compiler dat u, de programmeur, denkt dat deze functie een kandidaat is voor inlining – de compiler is niet verplicht om dit te respecteren.

In modern gebruik heeft het weinig tot niets te maken met inlining – moderne compilers gebruiken vrijelijk (of niet) functies “achter u”, deze maken deel uit van de optimalisatietechnieken.

Codetransformaties (inclusief inlining) worden gedaan onder de “as-if ” regelin C++, wat in feite betekent dat de compiler de code kan transformeren zoals hij wil, zolang de uitvoering “alsof” is, werd de originele code uitgevoerd zoals geschreven. Deze regel zorgt voor optimalisaties in C++.

Dat gezegd hebbende, als een adres eenmaal van een functie is genomen, moet het bestaan ​​(d.w.z. het adres moet geldig zijn). Dit kan betekenen dat het niet langer inline is, maar het kan nog steeds zijn (de optimizer past de juiste analyse toe).

Dus waarom kan er een aanwijzer bestaan ​​naar een inline-functie, aangezien er geen vast geheugenadres is voor inline-functies?

Nee, het is slechts een hint en heeft grotendeels betrekking op koppeling en niet op daadwerkelijke inlining. Dit voedt, wat misschien wel het belangrijkste huidige gebruik is, het definiëren van functies in header-bestanden.

Moet het niet verschillende adreswaarden van func()afdrukken elke keer dat func()wordt aangeroepen?

Het zou kunnen, de func()is een lokale variabele, gebaseerd op de stapellocatie wanneer de functie wordt uitgevoerd. Dat gezegd hebbende, de functie inline, het heeft betrekking op koppeling, de linker zal de functies samenvoegen over de vertaaleenheden.


Zoals vermeld in de opmerkingen;

… dat als het voorbeeld wordt gewijzigd in static int n, elke aanroep van de functie een constante waarde moet afdrukken (in een enkel programma natuurlijk) … en dat is waar, ongeacht of de code inline is of niet.

Dit is opnieuw het effect van de koppelingsvereiste op de lokale variabele func().


Antwoord 3, autoriteit 22%

Je leest oud materiaal. De belangrijkste reden om tegenwoordig inlinete gebruiken, is om functie-instanties in header-bestanden toe te staan. Gebruik van inlinetrefwoord met een functie geeft aan de linker aan dat alle instanties van de functie in vertaaleenheden kunnen worden gecombineerd; het hebben van een niet-inline-functie in een header die is opgenomen uit meerdere eenheden veroorzaakt ongedefinieerd gedrag als gevolg van een overtreding van de One Definition Rule.

C++17 voegt ook inline variabelen, die dezelfde eigenschap hebben dat de variabele in een kop kan worden gedefinieerd, en alle definities worden gecombineerd door de linker in plaats van een ODR-schending te veroorzaken.

De dingen waar je het over hebt met “code wordt gekopieerd naar de aanroepende functie” wordt inlininggenoemd en is onafhankelijk van het inlinetrefwoord. De compiler zal beslissen of hij dit al dan niet doet, op basis van optimalisatie-instellingen, voor zowel niet-inline-functies als inline-functies.


Antwoord 4, autoriteit 8%

Inline-functies zijn niet altijd inline. Het geeft alleen aan dat de programmeur deze functie inline zou willen hebben. De compiler mag elke functie inline gebruiken, ongeacht of inline trefwoord is gebruikt of niet.

Als het adres van de functie wordt gebruikt, is de functie hoogstwaarschijnlijk niet inline in het uiteindelijke uitvoerbare bestand, althans in GCC:

Als een functie zowel inline als statisch is, als alle aanroepen van de functie in de aanroeper zijn geïntegreerd en het adres van de functie nooit wordt gebruikt, wordt er nooit naar de eigen assemblercode van de functie verwezen.

GCC-documentatie


Antwoord 5, autoriteit 6%

Afgezien van het reeds genoemde punt dat een inline-functie eigenlijk niet inline hoeft te zijn (en veel functies zonder inlinezijninline gezet door moderne compilers ), is het ook heel goed denkbaar om een aanroep via een functieaanwijzerin te voeren. Voorbeeld:

#include <iostream>
int foo(int (*fun)(int), int x) {
  return fun(x);
}
int succ(int n) {
  return n+1;
}
int main() {
  int c=0;
  for (int i=0; i<10000; ++i) {
    c += foo(succ, i);
  }
  std::cout << c << std::endl;
}

Hier kan foo(succ, i)als geheelworden ingevoegd in slechts i+1. En dat lijkt inderdaad te gebeuren: g++ -O3 -Sproduceert code voor de functies fooen succ

_Z3fooPFiiEi:
.LFB998:
    .cfi_startproc
    movq    %rdi, %rax
    movl    %esi, %edi
    jmp *%rax
    .cfi_endproc
.LFE998:
    .size   _Z3fooPFiiEi, .-_Z3fooPFiiEi
    .p2align 4,,15
    .globl  _Z4succi
    .type   _Z4succi, @function
_Z4succi:
.LFB999:
    .cfi_startproc
    leal    1(%rdi), %eax
    ret
    .cfi_endproc

Maar dan genereert het code voor maindie nooit verwijst naareen van deze, maar bevat alleen een nieuwe gespecialiseerde _GLOBAL__sub_I__Z3fooPFiiEi:

.LFE999:
    .size   _Z4succi, .-_Z4succi
    .section    .text.startup,"ax",@progbits
    .p2align 4,,15
    .globl  main
    .type   main, @function
main:
.LFB1000:
    .cfi_startproc
    movdqa  .LC1(%rip), %xmm4
    xorl    %eax, %eax
    pxor    %xmm1, %xmm1
    movdqa  .LC0(%rip), %xmm0
    movdqa  .LC2(%rip), %xmm3
    jmp .L5
    .p2align 4,,10
    .p2align 3
.L8:
    movdqa  %xmm2, %xmm0
.L5:
    movdqa  %xmm0, %xmm2
    addl    $1, %eax
    paddd   %xmm3, %xmm0
    cmpl    $2500, %eax
    paddd   %xmm0, %xmm1
    paddd   %xmm4, %xmm2
    jne .L8
    movdqa  %xmm1, %xmm5
    subq    $24, %rsp
    .cfi_def_cfa_offset 32
    movl    $_ZSt4cout, %edi
    psrldq  $8, %xmm5
    paddd   %xmm5, %xmm1
    movdqa  %xmm1, %xmm6
    psrldq  $4, %xmm6
    paddd   %xmm6, %xmm1
    movdqa  %xmm1, %xmm7
    movd    %xmm7, 12(%rsp)
    movl    12(%rsp), %esi
    call    _ZNSolsEi
    movq    %rax, %rdi
    call    _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
    xorl    %eax, %eax
    addq    $24, %rsp
    .cfi_def_cfa_offset 8
    ret
    .cfi_endproc
.LFE1000:
    .size   main, .-main
    .p2align 4,,15
    .type   _GLOBAL__sub_I__Z3fooPFiiEi, @function
_GLOBAL__sub_I__Z3fooPFiiEi:
.LFB1007:
    .cfi_startproc
    subq    $8, %rsp
    .cfi_def_cfa_offset 16
    movl    $_ZStL8__ioinit, %edi
    call    _ZNSt8ios_base4InitC1Ev
    movl    $__dso_handle, %edx
    movl    $_ZStL8__ioinit, %esi
    movl    $_ZNSt8ios_base4InitD1Ev, %edi
    addq    $8, %rsp
    .cfi_def_cfa_offset 8
    jmp __cxa_atexit
    .cfi_endproc
.LFE1007:
    .size   _GLOBAL__sub_I__Z3fooPFiiEi, .-_GLOBAL__sub_I__Z3fooPFiiEi
    .section    .init_array,"aw"
    .align 8
    .quad   _GLOBAL__sub_I__Z3fooPFiiEi
    .local  _ZStL8__ioinit
    .comm   _ZStL8__ioinit,1,1

Dus in dit geval bevat het eigenlijke programma niet eens een functieaanwijzer die verwijst naar succ– de compiler heeft ontdekt dat deze aanwijzer toch altijd naar dezelfde functie zou verwijzen, en was daarom in staat om elimineer het hele ding zonder het gedrag te veranderen. Dit kan de prestaties aanzienlijk verbeteren, wanneer u vaak kleine functies aanroept via functiewijzers. Dat is een vrij wijdverbreide techniek in functionele talen; compilers voor talen als O’Caml en Haskell maken veel gebruik van dit soort optimalisatie.


Disclaimer: mijn montagevaardigheden zijn bijna onbestaande. Misschien praat ik hier onzin.

Other episodes