Parola chiave “in linea” vs concetto “inlining”

Sto facendo questa domanda di base per rendere le registrazioni dirette. Ho fatto riferimento a questa domanda e alla sua risposta attualmente accettata , che non è convincente. Tuttavia, la seconda risposta più votata offre una visione migliore, ma neanche perfetta.

Durante la lettura di seguito, distinguere tra la parola chiave inline e il concetto di “inlining”.

Ecco la mia opinione:

Il concetto di inlining

Questo è fatto per salvare il sovraccarico di chiamata di una funzione. È più simile alla sostituzione del codice in stile macro. Niente da contestare.

La parola chiave inline

Percezione A

La parola chiave inline è una richiesta al compilatore solitamente utilizzata per funzioni più piccole, in modo che il compilatore possa ottimizzarlo e effettuare chiamate più veloci. Il compilatore è libero di ignorarlo.

Lo discuto, per i seguenti motivi:

  1. Le funzioni più grandi e ricorsive non sono in linea e il compilatore ignora la parola chiave inline .
  2. Le funzioni più piccole vengono automaticamente evidenziate dall’ottimizzatore indipendentemente dalla parola chiave inline menzionata o meno.

È abbastanza chiaro che l’utente non ha alcun controllo sulla funzione che si integra con l’uso della parola chiave in inline .

Percezione B

inline non ha nulla a che fare con il concetto di inlining. Mettere in inline davanti alla funzione big / ricorsiva non aiuta e le funzioni più piccole non ne hanno bisogno, per essere inline.

L’ unico uso deterministico di inline è il mantenimento della regola della definizione unica .

cioè se una funzione è dichiarata con inline solo sotto le cose è obbligatorio:

  1. Anche se il suo corpo si trova in più unità di traduzione (ad esempio include quell’intestazione in più file .cpp ), il compilatore genererà solo 1 definizione ed eviterà più errori di linker di simboli. (Nota: se i corpi di quella funzione sono diversi, allora è un comportamento indefinito).
  2. Il corpo della funzione inline deve essere visibile / accessibile in tutte le unità di traduzione che lo utilizzano. In altre parole, la dichiarazione di una funzione inline in .h e la definizione in un qualsiasi file .cpp comporterà un “errore del linker di simboli non definito” per altri file .cpp

Verdetto

La percezione “A” è completamente sbagliata e la percezione “B” è completamente giusta .

Ci sono alcune citazioni in questo standard, tuttavia mi aspetto una risposta che spieghi logicamente se questo verdetto è vero o falso.

Non ero sicuro della tua richiesta:

Le funzioni più piccole sono automaticamente “in linea” dall’ottimizzatore indipendentemente dal fatto che la funzione inline sia menzionata o meno … È abbastanza chiaro che l’utente non ha alcun controllo sulla funzione “inlining” con l’uso della parola chiave inline .

Ho sentito che i compilatori sono liberi di ignorare la tua richiesta in inline , ma non pensavo che lo ignorassero completamente.

Ho cercato nel repository Github per Clang e LLVM per scoprirlo. (Grazie, software open source!) Ho scoperto che la parola chiave inline rende Clang / LLVM più probabile per l’allineamento di una funzione.

La ricerca

La ricerca della parola inline nel repository Clang porta allo specificatore di token kw_inline . Sembra che Clang utilizzi un intelligente sistema basato su macro per build il lexer e altre funzioni relative alle parole chiave, quindi c’è un riferimento diretto come if (tokenString == "inline") return kw_inline per trovare if (tokenString == "inline") return kw_inline . Ma qui in ParseDecl.cpp , vediamo che kw_inline genera una chiamata a DeclSpec::setFunctionSpecInline() .

 case tok::kw_inline: isInvalid = DS.setFunctionSpecInline(Loc, PrevSpec, DiagID); break; 

All’interno di questa funzione , impostiamo un po ‘ed emettiamo un avvertimento se è un duplicato in inline :

 if (FS_inline_specified) { DiagID = diag::warn_duplicate_declspec; PrevSpec = "inline"; return true; } FS_inline_specified = true; FS_inlineLoc = Loc; return false; 

Cercando FS_inline_specified altrove, vediamo che è un singolo bit in un bitfield, ed è usato in una funzione getter , isInlineSpecified() :

 bool isInlineSpecified() const { return FS_inline_specified | FS_forceinline_specified; } 

Cercando i siti di chiamata di isInlineSpecified() , troviamo il codegen , dove convertiamo l’albero di parsing C ++ nella rappresentazione intermedia di LLVM:

 if (!CGM.getCodeGenOpts().NoInline) { for (auto RI : FD->redecls()) if (RI->isInlineSpecified()) { Fn->addFnAttr(llvm::Attribute::InlineHint); break; } } else if (!FD->hasAttr()) Fn->addFnAttr(llvm::Attribute::NoInline); 

Clang to LLVM

Abbiamo finito con la fase di analisi di C ++. Ora il nostro inline viene convertito in un attributo dell’object Function LLVM neutrale alla lingua. Passiamo da Clang al repository LLVM .

Ricerca di llvm::Attribute::InlineHint restituisce il metodo Inliner::getInlineThreshold(CallSite CS) (con un blocco braceless dall’aspetto spaventoso) :

 // Listen to the inlinehint attribute when it would increase the threshold // and the caller does not need to minimize its size. Function *Callee = CS.getCalledFunction(); bool InlineHint = Callee && !Callee->isDeclaration() && Callee->getAttributes().hasAttribute(AttributeSet::FunctionIndex, Attribute::InlineHint); if (InlineHint && HintThreshold > thres && !Caller->getAttributes().hasAttribute(AttributeSet::FunctionIndex, Attribute::MinSize)) thres = HintThreshold; 

Quindi abbiamo già una soglia di base per l’inlinea dal livello di ottimizzazione e altri fattori, ma se è inferiore al HintThreshold globale, la HintThreshold . (HintThreshold è impostabile dalla riga di comando.)

getInlineThreshold() sembra avere un solo sito di chiamata , un membro di SimpleInliner :

 InlineCost getInlineCost(CallSite CS) override { return ICA->getInlineCost(CS, getInlineThreshold(CS)); } 

Chiama un metodo virtuale, chiamato anche getInlineCost , sul suo puntatore membro su un’istanza di InlineCostAnalysis .

Cercando ::getInlineCost() per trovare le versioni che sono membri della class, ne troviamo uno membro di AlwaysInline – che è una funzione di compilazione non standard ma ampiamente supportata – e un’altra che fa parte di InlineCostAnalysis . Usa il suo parametro Threshold qui :

 CallAnalyzer CA(Callee->getDataLayout(), *TTI, AT, *Callee, Threshold); bool ShouldInline = CA.analyzeCall(CS); 

CallAnalyzer::analyzeCall() è di oltre 200 linee e fa il vero lavoro grintoso di decidere se la funzione è inlineabile . Pesa molti fattori, ma man mano che leggiamo il metodo vediamo che tutti i suoi calcoli manipolano la Threshold o il Cost . E alla fine:

 return Cost < Threshold; 

Ma il valore restituito denominato ShouldInline è davvero un termine improprio. In effetti lo scopo principale di analyzeCall() è di impostare le variabili membro Cost e Threshold sull'object CallAnalyzer . Il valore restituito indica solo il caso in cui qualche altro fattore ha scavalcato l'analisi costo-vs-soglia, come vediamo qui :

 // Check if there was a reason to force inlining or no inlining. if (!ShouldInline && CA.getCost() < CA.getThreshold()) return InlineCost::getNever(); if (ShouldInline && CA.getCost() >= CA.getThreshold()) return InlineCost::getAlways(); 

Altrimenti, restituiamo un object che memorizza il Cost e la Threshold .

 return llvm::InlineCost::get(CA.getCost(), CA.getThreshold()); 

Quindi nella maggior parte dei casi non stiamo restituendo una decisione sì o no. La ricerca continua! Dove viene utilizzato questo valore di ritorno di getInlineCost() ?

La vera decisione

Si trova in bool Inliner::shouldInline(CallSite CS) . Un'altra grande funzione. getInlineCost() chiama getInlineCost() .

Si scopre che getInlineCost analizza il costo intrinseco dell'integrazione della funzione - la sua firma di argomento, la lunghezza del codice, la ricorsione, la ramificazione, il collegamento, ecc. - e alcune informazioni aggregate su ogni posto in cui viene utilizzata la funzione. D'altra parte, shouldInline() combina queste informazioni con più dati su un luogo specifico in cui viene utilizzata la funzione.

In tutto il metodo ci sono chiamate a InlineCost::costDelta() - che utilizzerà il valore di Threshold di analyzeCall() come calcolato da analyzeCall() . Alla fine, restituiamo un bool . La decisione è presa. In Inliner::runOnSCC() :

 if (!shouldInline(CS)) { emitOptimizationRemarkMissed(CallerCtx, DEBUG_TYPE, *Caller, DLoc, Twine(Callee->getName() + " will not be inlined into " + Caller->getName())); continue; } // Attempt to inline the function. if (!InlineCallIfPossible(CS, InlineInfo, InlinedArrayAllocas, InlineHistoryID, InsertLifetime, DL)) { emitOptimizationRemarkMissed(CallerCtx, DEBUG_TYPE, *Caller, DLoc, Twine(Callee->getName() + " will not be inlined into " + Caller->getName())); continue; } ++NumInlined; 

InlineCallIfPossible() esegue l'inlining sulla base della shouldInline() .

Quindi la Threshold stata influenzata dalla parola chiave inline e viene utilizzata alla fine per decidere se effettuare l'allineamento.

Pertanto, la Percezione B è in parte errata perché almeno un importante compilatore modifica il suo comportamento di ottimizzazione in base alla parola chiave in inline .

Tuttavia, possiamo anche vedere che in inline è solo un suggerimento, e altri fattori potrebbero prevalere su di esso.

Sono corretti entrambi.

L’uso di inline potrebbe, o potrebbe non, influenzare la decisione del compilatore di inline qualsiasi chiamata particolare alla funzione. Quindi A è corretto – agisce come una richiesta non vincolante che chiama la funzione inline, che il compilatore è libero di ignorare.

L’effetto semantico di inline è di allentare le restrizioni della regola One Definition per consentire definizioni identiche in più unità di traduzione, come descritto in B. Per molti compilatori, questo è necessario per consentire l’inlining delle chiamate di funzione – la definizione deve essere disponibile a quel punto, e ai compilatori è richiesto solo di elaborare un’unità di traduzione alla volta.