Quando i costi delle chiamate di funzione sono ancora importanti nei compilatori moderni?

87

Sono una persona religiosa e mi sforzo di non commettere peccati. Questo è il motivo per cui tendo a scrivere piccole ( più piccole di quelle , per riformulare Robert C. Martin) per rispettare i diversi comandamenti ordinati da Pulisci codice Bibbia. Ma durante il controllo di alcune cose, sono atterrato su questo post , sotto il quale ho letto questo commento:

Remember that the cost of a method call can be significant, depending on the language. There's almost always a tradeoff between writing readable code and writing performant code.

In quali condizioni questa affermazione citata è ancora valida al giorno d'oggi, data la ricca industria di compilatori moderni e performanti?

Questa è la mia unica domanda. E non si tratta di scrivere delle funzioni lunghe o piccole. Evidenzio solo che il tuo feedback potrebbe o non contribuire a modificare il mio atteggiamento e lasciarmi incapace di resistere alla tentazione di bestemmiatori .

    
posta Billal Begueradj 09.09.2017 - 09:12
fonte

12 risposte

146

Dipende dal tuo dominio.

Se si scrive codice per microcontrollore a bassa potenza, il costo della chiamata al metodo potrebbe essere significativo. Ma se stai creando un normale sito Web o applicazione, il costo della chiamata al metodo sarà trascurabile rispetto al resto del codice. In tal caso, sarà sempre più utile concentrarsi su algoritmi e strutture dati corretti anziché su micro-ottimizzazioni come le chiamate ai metodi.

E c'è anche una domanda sul compilatore che spiega i metodi per te. La maggior parte dei compilatori è abbastanza intelligente per le funzioni inline dove è possibile.

E per ultimo, c'è una regola d'oro della performance: SEMPRE PRIMA IL PROFILO. Non scrivere codice "ottimizzato" in base a ipotesi. Se sei insolente, scrivi entrambi i casi e vedi quale è meglio.

    
risposta data 09.09.2017 - 09:20
fonte
55

L'overhead delle chiamate di funzione dipende interamente dalla lingua e dal livello di ottimizzazione che stai ottimizzando.

Su un livello ultrabasso, le chiamate di funzione e ancor più le chiamate ai metodi virtuali possono essere costose se portano a errori di pronostico o errori di cache della CPU. Se hai scritto assemblatore , saprai anche che hai bisogno di alcune istruzioni extra per salvare e ripristinare i registri attorno a un chiamata. Non è vero che un compilatore "sufficientemente intelligente" sarebbe in grado di allineare le funzioni corrette per evitare questo sovraccarico, perché i compilatori sono limitati dalla semantica del linguaggio (specialmente attorno a funzioni come il dispatch del metodo di interfaccia o le librerie caricate dinamicamente). p>

A un livello elevato, linguaggi come Perl, Python, Ruby fanno un sacco di contabilità per chiamata di funzione, rendendoli comparativamente costosi. Ciò è peggiorato dalla meta-programmazione. Una volta ho accelerato un software Python 3x semplicemente sollevando le chiamate di funzione da un ciclo molto caldo. Nel codice critico delle prestazioni, le funzioni di aiuto in linea possono avere un effetto notevole.

Ma la stragrande maggioranza del software non è così estremamente critica dal punto di vista delle prestazioni da poter notare il sovraccarico delle chiamate di funzione. In ogni caso, scrivere codice pulito e semplice paga:

  • Se il tuo codice non è critico per le prestazioni, questo semplifica la manutenzione. Anche nei software critici per le prestazioni, la maggior parte del codice non sarà un "punto caldo".

  • Se il codice è critico dal punto di vista delle prestazioni, un codice semplice facilita la comprensione del codice e consente di individuare opportunità per l'ottimizzazione. Le maggiori vincite di solito non derivano da micro-ottimizzazioni come le funzioni di inlining, ma da miglioramenti algoritmici. O formulato diversamente: non fare la stessa cosa più velocemente. Trova un modo per fare meno.

Si noti che "codice semplice" non significa "fattorizzato in mille piccole funzioni". Ogni funzione introduce anche un po 'di overhead cognitivo - è più difficile motivo su un codice più astratto. Ad un certo punto, queste minuscole funzioni potrebbero fare così poco che non usarle semplificherebbe il tuo codice.

    
risposta data 09.09.2017 - 10:45
fonte
19

Quasi tutti gli adage relativi al codice di ottimizzazione per le prestazioni sono casi speciali di legge di Amdahl . La breve, umoristica affermazione della legge di Amdahl è

If one piece of your program takes 5% of runtime, and you optimize that piece so that it now takes zero percent of runtime, the program as a whole will only be 5% faster.

(L'ottimizzazione di cose fino allo zero percento del runtime è del tutto possibile: quando ti siedi per ottimizzare un programma grande e complicato, è molto probabile che stia spendendo almeno parte del suo tempo di esecuzione su roba che non lo fa t necessario fare assolutamente .)

Questo è il motivo per cui le persone normalmente dicono di non preoccuparsi dei costi delle chiamate di funzione: non importa quanto costosi siano, normalmente il programma nel suo complesso sta spendendo solo una piccola parte del suo tempo di esecuzione sull'overhead di chiamata, quindi accelerarli non aiuta molto.

Ma, se c'è un trucco che puoi fare in modo che tutti la funzione richiami più velocemente, probabilmente questo trucco ne vale la pena. Gli sviluppatori di compilatori impiegano molto tempo ad ottimizzare la funzione "prologues" e "epilogues", poiché ciò avvantaggia tutti i programmi compilati con quel compilatore, anche se è solo un piccolo bit per ciascuno.

E, se hai motivo di credere che un programma è che spende molto del suo runtime solo facendo chiamate alla funzione, allora dovresti iniziare a pensare se alcune di quelle chiamate di funzione non sono necessarie. Ecco alcune regole pratiche per sapere quando dovresti fare questo:

  • Se il runtime per invocazione di una funzione è inferiore a un millisecondo, ma tale funzione viene chiamata centinaia di migliaia di volte, dovrebbe essere probabilmente in linea.

  • Se un profilo del programma mostra migliaia di funzioni e nessuno di esse richiede più dello 0,1% di runtime, l'overhead delle chiamate di funzione è probabilmente significativo in aggregato.

  • Se disponi di " codice lasagna ", in cui ci sono molti livelli di astrazione che difficilmente fanno lavorare al di là del dispacciamento al livello successivo e tutti questi livelli sono implementati con chiamate al metodo virtuale, quindi ci sono buone probabilità che la CPU stia perdendo un sacco di tempo sulle bancarelle di condotte indirette. Sfortunatamente, l'unica cura per questo è sbarazzarsi di alcuni strati, che è spesso molto difficile.

risposta data 09.09.2017 - 22:28
fonte
17

Sfiderò questa citazione:

There's almost always a tradeoff between writing readable code and writing performant code.

Questa è una dichiarazione davvero fuorviante e un atteggiamento potenzialmente pericoloso. Ci sono alcuni casi specifici in cui devi fare un compromesso, ma in generale i due fattori sono indipendenti.

Un esempio di compromesso necessario è quando si ha un algoritmo semplice rispetto a un più complesso ma più performante. Un'implementazione di hashtable è chiaramente più complessa di un'implementazione di elenchi collegati, ma la ricerca sarà più lenta, quindi potrebbe essere necessario scambiare la semplicità (che è un fattore di leggibilità) per le prestazioni.

Per quanto riguarda l'overhead delle chiamate alle funzioni, trasformare un algoritmo ricorsivo in un iterativo potrebbe avere un beneficio significativo a seconda dell'algoritmo e della lingua. Ma questo è di nuovo uno scenario molto specifico, e in generale il sovraccarico delle chiamate di funzione sarà trascurabile o ottimizzato.

(Alcuni linguaggi dinamici come Python hanno un significativo overhead di chiamata di metodo, ma se le prestazioni diventano un problema probabilmente non dovresti usare Python in primo luogo.)

Molti principi per codice leggibile - formattazione coerente, nomi identificativi significativi, commenti appropriati e utili e così via non hanno alcun effetto sulle prestazioni. E alcuni - come usare le enumerazioni piuttosto che le stringhe - hanno anche dei vantaggi in termini di prestazioni.

    
risposta data 10.09.2017 - 19:40
fonte
5

Nella maggior parte dei casi, la funzione di chiamata overhead non è importante.

Tuttavia il guadagno maggiore derivante dall'inlining del codice è l'ottimizzazione del nuovo codice dopo l'inline .

Ad esempio, se si chiama una funzione con argomento costante, l'ottimizzatore può ora piegare costantemente quell'argomento in cui non poteva prima di inserire la chiamata. Se l'argomento è un puntatore a funzione (o lambda), l'ottimizzatore può ora inline anche le chiamate a quel lambda.

Questa è una grande ragione per cui le funzioni virtuali e i puntatori di funzione non sono attraenti in quanto non è possibile in linea con essi a meno che il puntatore effettivo della funzione non sia stato piegato costantemente fino al sito di chiamata.

    
risposta data 11.09.2017 - 11:57
fonte
4

Supponendo che le prestazioni siano importanti per il tuo programma, e in effetti ha un sacco di chiamate, il costo può dipendere o meno a seconda del tipo di chiamata.

Se la funzione chiamata è piccola e il compilatore è in grado di eseguirla, il costo sarà essenzialmente pari a zero. I moderni compilatori / implementazioni linguistiche hanno JIT, ottimizzazioni dei tempi di collegamento e / o sistemi di moduli progettati per massimizzare la capacità di integrare funzioni quando è vantaggioso.

OTOH, c'è un costo non ovvio per il funzionamento delle chiamate: la loro semplice esistenza potrebbe inibire le ottimizzazioni del compilatore prima e dopo la chiamata.

Se il compilatore non può ragionare su cosa fa la funzione chiamata (ad es. è dispatch virtuale / dinamico o una funzione in una libreria dinamica) allora potrebbe essere necessario assumere pessimisticamente che la funzione potrebbe avere effetti collaterali - lanciare un eccezione, modificare lo stato globale o modificare qualsiasi memoria vista attraverso i puntatori. Il compilatore potrebbe dover salvare valori temporanei sul back-memory e rileggerli dopo la chiamata. Non sarà in grado di riordinare le istruzioni attorno alla chiamata, quindi potrebbe non essere in grado di vettorizzare i loop o di sollevare il calcolo ridondante dai loop.

Ad esempio, se si chiama inutilmente una funzione in ogni iterazione del ciclo:

for(int i=0; i < /* gasp! */ strlen(s); i++) x ^= s[i];

Il compilatore potrebbe sapere che è una funzione pura e spostarlo fuori dal ciclo (in un caso terribile come questo esempio si risolve anche l'algoritmo accidentale O (n ^ 2) per essere O (n)):

for(int i=0, end=strlen(s); i < end; i++) x ^= s[i];

E poi magari riscrivere il loop per elaborare 4/8/16 elementi alla volta usando le istruzioni wide / SIMD.

Ma se aggiungi una chiamata ad un codice opaco nel ciclo, anche se la chiamata non fa nulla ed è super economica, il compilatore deve assumere il peggio - che la chiamata acceda ad una variabile globale che punta allo stesso la memoria come s cambia i suoi contenuti (anche se è const nella tua funzione, può essere non- const in nessun altro), rendendo impossibile l'ottimizzazione:

for(int i=0; i < strlen(s); i++) {
    x ^= s[i];
    do_nothing();
}
    
risposta data 11.09.2017 - 00:59
fonte
3

Questo vecchio documento potrebbe rispondere alla tua domanda:

Guy Lewis Steele, Jr.. "Debunking the 'Expensive Procedure Call' Myth, or, Procedure Call Implementations Considered Harmful, or, Lambda: The Ultimate GOTO". MIT AI Lab. AI Lab Memo AIM-443. October 1977.

Estratto:

Folklore states that GOTO statements are "cheap", while procedure calls are "expensive". This myth is largely a result of poorly designed language implementations. The historical growth of this myth is considered. Both theoretical ideas and an existing implementation are discussed which debunk this myth. It is shown that the unrestricted use of procedure calls permits great stylish freedom. In particular, any flowchart can be written as a "structured" program without introducing extra variables. The difficulty with the GOTO statement and the procedure call is characterized as a conflict between abstract programming concepts and concrete language constructs.

    
risposta data 09.09.2017 - 15:09
fonte
3
  • In C ++ fai attenzione alla progettazione delle chiamate di funzioni che copiano gli argomenti, il valore predefinito è "passa per valore". La funzione di overhead delle chiamate dovuta al salvataggio dei registri e di altri elementi correlati allo stack frame può essere sopraffatta da una copia involontaria (e potenzialmente molto costosa) di un oggetto.

  • Esistono ottimizzazioni relative allo stack frame da indagare prima di rinunciare a un codice altamente fattorizzato.

  • La maggior parte delle volte in cui ho avuto a che fare con un programma lento ho scoperto che apportare modifiche algoritmiche ha prodotto una maggiore velocità rispetto alle chiamate di funzione in-lining. Ad esempio: un altro ingegnere ha restituito un parser che ha riempito una struttura map-of-maps. Come parte di ciò ha rimosso un indice memorizzato nella cache da una mappa a una associata logicamente. Si è trattato di una buona mossa di robustezza del codice, tuttavia ha reso il programma inutilizzabile a causa di un fattore di rallentamento di 100 dovuto all'esecuzione di una ricerca hash per tutti gli accessi futuri rispetto all'uso dell'indice memorizzato. Il profiling ha mostrato che la maggior parte del tempo è stata spesa nella funzione di hashing.

risposta data 10.09.2017 - 18:06
fonte
3

Come altri dicono, dovresti prima misurare le prestazioni del tuo programma e probabilmente non troverai alcuna differenza nella pratica.

Tuttavia, da un livello concettuale ho pensato di chiarire alcune cose che sono confuse nella tua domanda. Innanzitutto, chiedi:

Do function call costs still matter in modern compilers?

Notare le parole chiave "funzione" e "compilatori". La tua offerta è molto diversa:

Remember that the cost of a method call can be significant, depending on the language.

Si tratta di metodi , nel senso orientato agli oggetti.

Anche se "funzione" e "metodo" sono spesso usati in modo intercambiabile, ci sono delle differenze quando si tratta del loro costo (che stai chiedendo) e quando si tratta di compilazione (che è il contesto che hai dato).

In particolare, dobbiamo sapere su dispatch statico vs dispatch dinamico . Ignorerò le ottimizzazioni per il momento.

In una lingua come C, solitamente chiamiamo funzioni con dispatch statico . Ad esempio:

int foo(int x) {
  return x + 1;
}

int bar(int y) {
  return foo(y);
}

int main() {
  return bar(42);
}

Quando il compilatore vede la chiamata foo(y) , sa a quale funzione si riferisce il nome foo , quindi il programma di output può passare direttamente alla funzione foo , che è piuttosto economica. Questo è ciò che invio statico significa.

L'alternativa è dispatch dinamico , dove il compilatore non conosce quale funzione viene chiamata. Ad esempio, ecco un codice Haskell (poiché l'equivalente C sarebbe disordinato!):

foo x = x + 1

bar f x = f x

main = print (bar foo 42)

Qui la funzione bar chiama il suo argomento f , che potrebbe essere qualsiasi cosa. Quindi il compilatore non può semplicemente compilare bar con un'istruzione di salto veloce, perché non sa dove andare. Invece, il codice che generiamo per bar sarà dereferenziato f per scoprire a quale funzione sta puntando, quindi saltare ad esso. Questo è ciò che invio dinamico significa.

Entrambi questi esempi sono per funzioni . Hai citato i metodi , che possono essere pensati come uno stile particolare di funzioni inviate dinamicamente. Ad esempio, ecco alcuni Python:

class A:
  def __init__(self, x):
    self.x = x

  def foo(self):
    return self.x + 1

def bar(y):
  return y.foo()

z = A(42)
bar(z)

La chiamata y.foo() utilizza l'invio dinamico, dal momento che sta cercando il valore della proprietà foo nell'oggetto y e chiama ciò che trova; non sa che y avrà classe A , o che la classe A contenga un metodo foo , quindi non possiamo semplicemente passare direttamente ad essa.

OK, questa è l'idea di base. Nota che il dispatch statico è più veloce della dispatch dinamica a prescindere se compiliamo o interpretiamo; tutto il resto è uguale. Il dereferenziamento comporta un costo aggiuntivo in ogni caso.

Quindi, in che modo questo influisce sui moderni, ottimizzando i compilatori?

La prima cosa da notare è che il dispatch statico può essere ottimizzato più pesantemente: quando sappiamo a quale funzione stiamo saltando, possiamo fare cose come l'inlining. Con la spedizione dinamica, non sappiamo che stiamo saltando fino al tempo di esecuzione, quindi non possiamo fare molta ottimizzazione.

In secondo luogo, è possibile in alcune lingue inferire dove alcune disposi- zioni dinamiche finiranno di saltare e quindi ottimizzarle in un invio statico. Questo ci consente di eseguire altre ottimizzazioni come l'inlining, ecc.

Nell'esempio di Python sopra riportato tale inferenza è piuttosto senza speranza, poiché Python consente ad altri codici di sovrascrivere classi e proprietà, quindi è difficile dedurre molto di ciò che verrà conservato in tutti i casi.

Se il nostro linguaggio ci consente di imporre ulteriori restrizioni, ad esempio limitando y alla classe A utilizzando un'annotazione, potremmo utilizzare tali informazioni per dedurre la funzione di destinazione. Nelle lingue con sottoclassi (che è quasi tutte le lingue con classi!) Che in realtà non è sufficiente, dal momento che y potrebbe effettivamente avere una classe (secondaria) diversa, quindi avremmo bisogno di informazioni extra come le annotazioni final di Java per sapere esattamente quale la funzione verrà richiamata.

Haskell non è un linguaggio OO, ma possiamo dedurre il valore di f inserendo bar (che è staticamente inviato) in main , sostituendo foo per% codice%. Poiché il target di y in foo è noto staticamente, la chiamata diventa staticamente inviata e probabilmente verrà completamente integrata e ottimizzata (poiché queste funzioni sono piccole, è più probabile che il compilatore li incorpori, anche se possiamo contateci in generale).

Quindi il costo si riduce a:

  • La lingua invia la chiamata in modo statico o dinamico?
  • Se è il secondo, la lingua consente all'implementazione di dedurre il target utilizzando altre informazioni (ad esempio tipi, classi, annotazioni, inlining, ecc.)?
  • Quanto può essere ottimizzata la spedizione statica (dedotta o altrimenti)?

Se stai utilizzando un linguaggio "molto dinamico", con molta dispedizione dinamica e poche garanzie disponibili per il compilatore, ogni chiamata avrà un costo. Se stai usando un linguaggio "molto statico", un compilatore maturo produrrà un codice molto veloce. Se sei nel mezzo, allora può dipendere dal tuo stile di codifica e da quanto è intelligente l'implementazione.

    
risposta data 11.09.2017 - 12:44
fonte
2

Sì, una previsione di ramo mancante è più costosa per l'hardware moderno rispetto a decenni fa, ma i compilatori sono diventati molto più intelligenti nell'ottimizzare questo aspetto.

Ad esempio, considera Java. A prima vista, la funzione chiamata overhead dovrebbe essere particolarmente dominante in questa lingua:

  • le funzioni minuscole sono molto diffuse a causa della convenzione JavaBean
  • Le funzioni
  • sono predefinite in virtuale e solitamente sono
  • l'unità di compilazione è la classe; il runtime supporta il caricamento di nuove classi in qualsiasi momento, comprese le sottoclassi che sostituiscono metodi precedentemente monomorfici

Inorriditi da queste pratiche, il programmatore C medio avrebbe predetto che Java doveva essere almeno di un ordine di grandezza più lento di C. E 20 anni fa avrebbe avuto ragione. I benchmark moderni, tuttavia, collocano il codice idiomatico di Java entro una piccola percentuale del codice C equivalente. Com'è possibile?

Uno dei motivi è che le chiamate in linea dei Moduli JVM moderni sono ovvie. Lo fa usando inline speculative:

  1. Il codice appena caricato viene eseguito senza ottimizzazione. Durante questa fase, per ogni sito di chiamata, la JVM tiene traccia di quali metodi sono stati effettivamente richiamati.
  2. Una volta che il codice è stato identificato come hotspot delle prestazioni, il runtime utilizza queste statistiche per identificare il percorso di esecuzione più probabile e lo incorpora, precedendolo con un ramo condizionale nel caso in cui l'ottimizzazione speculativa non si applichi.

Cioè il codice:

int x = point.getX();

viene riscritto in

if (point.class != Point) GOTO interpreter;
x = point.x;

E ovviamente il runtime è abbastanza intelligente per spostare questo tipo di controllo fino a quando il punto non viene assegnato, o lo elide se il tipo è noto al codice chiamante.

In sintesi, se anche Java gestisce l'inlining del metodo automatico, non vi è alcun motivo intrinseco per cui un compilatore non possa supportare l'inlining automatico e ogni ragione per farlo, perché l'inlining è estremamente vantaggioso per i processori moderni. Non riesco quindi a immaginare un compilatore mainstream moderno che ignori le più elementari strategie di ottimizzazione, e presumerei che un compilatore sia in grado di farlo a meno che non sia provato diversamente.

    
risposta data 10.09.2017 - 20:10
fonte
2

Remember that the cost of a method call can be significant, depending on the language. There's almost always a tradeoff between writing readable code and writing performant code.

Questo è, sfortunatamente, altamente dipendente da:

  • la toolchain del compilatore, incluso il JIT se presente,
  • il dominio.

Innanzitutto, la prima legge dell'ottimizzazione delle prestazioni è prima profilo . Esistono molti domini in cui le prestazioni della parte software sono irrilevanti per le prestazioni dell'intero stack: chiamate al database, operazioni di rete, operazioni del sistema operativo, ...

Ciò significa che le prestazioni del software sono del tutto irrilevanti, anche se non migliorano la latenza, l'ottimizzazione del software può comportare risparmi energetici e risparmi hardware (o risparmi di batteria per le app mobili), che possono essere importanti.

Tuttavia, quelli in genere NON possono essere osservati, e spesso i miglioramenti algoritmici volte superano le micro-ottimizzazioni con un ampio margine.

Quindi, prima di ottimizzare, devi capire per cosa stai ottimizzando ... e se ne vale la pena.

Ora, per quanto riguarda le prestazioni del software puro, varia notevolmente tra i toolchain.

Ci sono due costi per una chiamata di funzione:

  • il costo del tempo di esecuzione,
  • il costo del tempo di compilazione.

Il costo del tempo di esecuzione è piuttosto ovvio; per eseguire una chiamata di funzione è necessaria una certa quantità di lavoro. Ad esempio, utilizzando C su x86, una chiamata di funzione richiederà (1) versare i registri nello stack, (2) spingere gli argomenti ai registri, eseguire la chiamata e in seguito (3) ripristinare i registri dallo stack. Vedi questo riepilogo delle convenzioni di chiamata per vedere il lavoro coinvolto .

Questo registro spargimento / ripristino richiede una quantità non trascurabile di volte (dozzine di cicli della CPU).

In genere si prevede che questo costo sarà banale rispetto al costo effettivo dell'esecuzione della funzione, tuttavia alcuni schemi sono controproducenti qui: getter, funzioni protette da una semplice condizione, ecc ...

Oltre agli interpreti , un programmatore spera quindi che il suo compilatore o JIT ottimizzerà le chiamate di funzioni che non sono necessarie; anche se questa speranza a volte può non portare frutto. Perché gli ottimizzatori non sono magici.

Un ottimizzatore può rilevare che una chiamata di funzione è banale, e in linea la chiamata: essenzialmente, copia / incolla il corpo della funzione nel sito di chiamata. Questa non è sempre una buona ottimizzazione (potrebbe indurre a gonfiare), ma in generale è utile perché inlining espone il contesto e il contesto consente ulteriori ottimizzazioni.

Un esempio tipico è:

void func(condition: boolean) {
    if (condition) {
        doLotsOfWork();
    }
}

void call() { func(false); }

Se func è in linea, l'ottimizzatore si renderà conto che il ramo non viene mai preso e ottimizza call in void call() {} .

In questo senso, le chiamate di funzione nascondendo le informazioni dall'ottimizzatore (se non ancora in linea) possono inibire determinate ottimizzazioni. Le chiamate alle funzioni virtuali sono particolarmente colpevoli di questo, perché la devirtualizzazione (dimostrando quale funzione alla fine viene chiamata in fase di esecuzione) non è sempre facile.

In conclusione, il mio consiglio è di scrivere chiaramente in primo luogo, evitando la premessa pessimizzazione algoritmica (complessità cubica o morsi peggiori rapidamente), e quindi ottimizzare solo ciò che deve essere ottimizzato.

    
risposta data 11.09.2017 - 15:08
fonte
1

"Remember that the cost of a method call can be significant, depending on the language. There's almost always a tradeoff between writing readable code and writing performant code."

Under what conditions is this quoted statement still valid nowadays given the rich industry of performant modern compilers?

Devo solo dire di no. Credo che la citazione sia imprudente da buttare lì fuori.

Naturalmente non sto dicendo la verità completa, ma non mi interessa essere così sincero. È come in quel film di Matrix, ho dimenticato se era 1 o 2 o 3 - Penso che fosse quello con la sexy attrice italiana con i meloni grandi (non mi piaceva nessuno tranne il primo), quando oracle lady ha detto a Keanu Reeves, "Ti ho appena detto quello che avevi bisogno di sentire", o qualcosa del genere, è quello che voglio fare ora.

I programmatori non hanno bisogno di sentire questo. Se hanno esperienza con i profiler nella loro mano e la citazione è in qualche modo applicabile ai loro compilatori, loro lo sapranno già e impareranno questo nel modo giusto a patto che capiscano il loro output di profilazione e perché determinate chiamate foglia sono hotspot, attraverso la misurazione. Se non hanno esperienza e non hanno mai profilato il loro codice, questa è l'ultima cosa che hanno bisogno di sentire, che dovrebbero iniziare a compromettere superstiziosamente il modo in cui scrivono il codice fino al punto di definire tutto prima di identificare anche gli hotspot nella speranza che ciò accada diventare più performante.

In ogni caso, per una risposta più accurata, dipende. Alcune delle condizioni delle barche sono già elencate tra le risposte migliori. Le condizioni possibili solo scegliendo una lingua sono già enormi, come il C ++ che dovrebbe entrare nella distribuzione dinamica nelle chiamate virtuali e quando può essere ottimizzato via e sotto quali compilatori e persino linker, e che già garantisce una risposta dettagliata e tanto meno provare per affrontare le condizioni in ogni lingua possibile e compilatore là fuori. Ma aggiungerò sopra, "chi se ne frega?" perché anche lavorando in aree critiche per le prestazioni come raytracing, l'ultima cosa che avrò mai da fare prima di iniziare sono i metodi mano-inlining prima che io abbia qualsiasi misura.

Credo che alcune persone siano troppo zelanti nel suggerire di non fare mai micro-ottimizzazioni prima di misurare. Se l'ottimizzazione per la localizzazione di riferimento conta come una micro-ottimizzazione, allora spesso comincio ad applicare tali ottimizzazioni proprio all'inizio con una mentalità progettuale orientata ai dati in aree che so per certo saranno cruciali per le prestazioni (codice di raytracing, ad esempio), perché altrimenti so che dovrò riscrivere le grandi sezioni subito dopo aver lavorato in questi domini per anni. L'ottimizzazione della rappresentazione dei dati per gli hit della cache spesso può avere lo stesso tipo di miglioramento delle prestazioni dei miglioramenti algoritmici, a meno che non stiamo parlando come quadratic time to linear.

Ma non ho mai, mai visto una buona ragione per iniziare l'inline prima delle misurazioni, soprattutto perché i profiler sono decenti a rivelare ciò che potrebbe trarre beneficio dall'inlining, ma non a rivelare ciò che potrebbe trarre vantaggio dall'essere non inline (e non inlining può effettivamente fare codice più veloce se la chiamata di funzione non rivestita è un caso raro, migliorando la località di riferimento per l'icache per il codice a caldo e talvolta consentendo anche agli ottimizzatori di eseguire un lavoro migliore per il percorso del caso comune di esecuzione).

    
risposta data 01.12.2017 - 03:21
fonte

Leggi altre domande sui tag