In che misura le chiamate alle funzioni influiscono sulle prestazioni?

9

L'estrazione di funzionalità in metodi o funzioni è un must per la modularità del codice, la leggibilità e l'interoperabilità, specialmente in OOP.

Ma questo significa che verranno fatte più chiamate.

In che modo la suddivisione del nostro codice in metodi o funzioni influisce effettivamente sulle prestazioni nelle moderne * lingue?

* I più popolari: C, Java, C ++, C #, Python, JavaScript, Ruby ...

    
posta dabadaba 10.05.2016 - 13:59
fonte

6 risposte

20

Forse. Il compilatore potrebbe decidere "hey, questa funzione viene chiamata solo poche volte e io dovrei ottimizzare per la velocità, quindi inserirò semplicemente questa funzione". In sostanza, il compilatore sostituirà la chiamata alla funzione con il corpo della funzione. Ad esempio, il codice sorgente sarebbe simile a questo.

void DoSomething()
{
   a = a + 1;
   DoSomethingElse(a);
}

void DoSomethingElse(int a)
{
   b = a + 3;
}

Il compilatore decide di aggiungere% co_de in linea e il codice diventa

void DoSomething()
{
   a = a + 1;
   b = a + 3;
}

Quando le funzioni non sono in linea, sì c'è un colpo di prestazioni per effettuare una chiamata di funzione. Tuttavia, è un successo così minuscolo che solo il codice ad alte prestazioni si preoccuperà delle chiamate di funzione. E su quei tipi di progetti, il codice viene tipicamente scritto in assembly.

Le chiamate di funzione (a seconda della piattaforma) in genere richiedono un paio di istruzioni di 10, incluso il salvataggio / ripristino dello stack. Alcune chiamate di funzione consistono in un'istruzione di salto e ritorno.

Ma ci sono altre cose che potrebbero influire sul rendimento delle chiamate di funzione. La funzione chiamata potrebbe non essere caricata nella cache del processore, causando un errore di cache e forzando il controller di memoria ad afferrare la funzione dalla RAM principale. Ciò può causare un grande successo per le prestazioni.

In breve: le chiamate di funzione possono o non possono influire sulle prestazioni. L'unico modo per dire è profilare il tuo codice. Non cercare di indovinare dove sono gli spot del codice lento, perché il compilatore e l'hardware hanno trucchi incredibili. Profili il codice per ottenere la posizione dei punti lenti.

    
risposta data 10.05.2016 - 14:43
fonte
5

Si tratta di un'implementazione del compilatore o del runtime (e delle sue opzioni) e non si può dire con certezza.

All'interno di C e C ++, alcuni compilatori invieranno chiamate in linea basate su impostazioni di ottimizzazione - questo può essere visto banalmente esaminando l'assembly generato quando si guardano strumenti come link

Altre lingue, come Java, hanno questo come parte del runtime. Questo fa parte del JIT ed è stato approfondito in questa domanda SO . In particolare, guarda le opzioni JVM per HotSpot

-XX:InlineSmallCode=n Inline a previously compiled method only if its generated native code size is less than this. The default value varies with the platform on which the JVM is running.
-XX:MaxInlineSize=35 Maximum bytecode size of a method to be inlined.
-XX:FreqInlineSize=n Maximum bytecode size of a frequently executed method to be inlined. The default value varies with the platform on which the JVM is running.

Quindi sì, il compilatore JIT HotSpot integrerà i metodi che soddisfano determinati criteri.

L'impatto di questo, è difficile da determinare poiché ogni JVM (o compilatore) potrebbe fare le cose in modo diverso e provare a rispondere con un ampio tratto di una lingua è quasi sicuro. L'impatto può essere determinato correttamente solo analizzando il codice nell'ambiente di esecuzione appropriato ed esaminando l'output compilato.

Questo può essere visto come un approccio errato con CPython non inlining, ma Jython (Python in esecuzione nella JVM) con alcune chiamate inline. Allo stesso modo, MRI Ruby non inlining mentre JRuby lo farebbe, e ruby2c che è un transpiler per ruby in C ... che potrebbe quindi essere inlining o meno a seconda delle opzioni del compilatore C con cui è stato compilato.

Le lingue non sono in linea. Implementazioni può .

    
risposta data 10.05.2016 - 17:24
fonte
4

Stai cercando le prestazioni nel posto sbagliato. Il problema con le chiamate di funzione non è che costano molto. C'è un altro problema. Le chiamate di funzione potrebbero essere assolutamente gratuite e avresti ancora questo altro problema.

È che una funzione è come una carta di credito. Dal momento che puoi usarlo facilmente, tendi ad usarlo più di quanto forse dovresti. Supponiamo che tu lo chiami il 20% in più del necessario. Quindi, il tipico software di grandi dimensioni contiene diversi livelli, ciascuna funzione di chiamata nel livello sottostante, quindi il fattore di 1,2 può essere aggravato dal numero di livelli. (Ad esempio, se ci sono cinque livelli e ogni livello ha un fattore di rallentamento di 1,2, il fattore di rallentamento composto è 1,2 ^ 5 o 2,5). Questo è solo un modo per pensarci.

Questo non significa che devi evitare le chiamate di funzione. Significa che, quando il codice è attivo e funzionante, dovresti sapere come trovare ed eliminare gli sprechi. Vi sono molti consigli eccellenti su come farlo nei siti di stackexchange. Questo dà uno dei miei contributi.

AGGIUNTO: piccolo esempio. Una volta ho lavorato in un team su un software di fabbrica che monitorava una serie di ordini di lavoro o "lavori". C'era una funzione JobDone(idJob) che poteva dire se un lavoro era stato fatto. Un lavoro è stato fatto quando sono state eseguite tutte le sottoprocessi e ognuna di queste è stata eseguita quando tutte le sue operazioni secondarie sono state eseguite. Tutte queste cose sono state registrate in un database relazionale. Una singola chiamata a un'altra funzione potrebbe estrarre tutte quelle informazioni, quindi JobDone ha chiamato quell'altra funzione, ha visto se il lavoro è stato eseguito e ha buttato via il resto. Quindi le persone potrebbero facilmente scrivere codice come questo:

while(!JobDone(idJob)){
    ...
}

o

foreach(idJob in jobs){
    if (JobDone(idJob)){
        ...
    }
}

Vedi il punto? La funzione era così "potente" e facile da chiamare che è stata chiamata troppo. Quindi il problema delle prestazioni non erano le istruzioni che entravano e uscivano dalla funzione. Era necessario che ci fosse un modo più diretto per capire se i lavori erano stati fatti. Ancora una volta, questo codice avrebbe potuto essere incorporato in migliaia di righe di codice altrimenti innocente. Cercare di sistemarlo in anticipo è ciò che tutti cercano di fare, ma è come cercare di lanciare freccette in una stanza buia. Quello che ti serve invece è di farlo funzionare, e quindi lasciare che il "codice lento" ti dica di cosa si tratta, semplicemente prendendo tempo. Per questo uso pausa casuale .

    
risposta data 10.05.2016 - 21:28
fonte
1

Penso che dipenda davvero dalla lingua e dalla funzione. Sebbene i compilatori c e c ++ possano incorporare molte funzioni, questo non è il caso per Python o Java.

Anche se non conosco i dettagli specifici di java (eccetto che ogni metodo è virtuale ma ti consiglio di controllare meglio la documentazione), in Python sono sicuro che non ci sono inlining, nessuna ottimizzazione della ricorsione di coda e chiamate di funzione sono abbastanza costoso.

Le funzioni Python sono fondamentalmente oggetti eseguibili (e infatti è anche possibile definire il metodo call () per trasformare una funzione di istanza di oggetto in una funzione). Ciò significa che c'è un bel po 'di overhead nel chiamarli ...

MA

quando definisci le variabili all'interno delle funzioni, l'interprete usa il LOADFAST invece della normale istruzione LOAD nel bytecode, rendendo il tuo codice più veloce ...

Un'altra cosa è che quando definisci un oggetto callable, sono possibili modelli come la memoizzazione e possono effettivamente accelerare molto il tuo calcolo (al costo di usare più memoria). Fondamentalmente è sempre un compromesso. Il costo delle chiamate di funzione dipende anche dai parametri, perché determinano la quantità di cose che devi copiare sullo stack (quindi in c / c ++ è prassi comune passare grandi parametri come le strutture tramite puntatori / riferimenti invece che per valore).

Penso che la tua domanda sia in pratica troppo ampia per poter rispondere completamente su stackexchange.

Quello che ti suggerisco di fare è iniziare con una lingua e studiare la documentazione avanzata per capire come le chiamate di funzione sono implementate da quel linguaggio specifico.

Sarai sorpreso da quante cose imparerai in questo processo.

Se hai un problema specifico, fai misurazioni / profilazione e decidi il tempo è meglio creare una funzione o copiare / incollare il codice equivalente.

se fai una domanda più specifica, sarebbe più facile ottenere una risposta più specifica, credo.

    
risposta data 10.05.2016 - 16:24
fonte
1

Dipende da quanto tempo ci vuole per eseguire il sovraccarico con la chiamata di funzione (impostazione dello stack frame, passaggio di variabili, pulizia successiva ...) rispetto al tempo trascorso all'interno della funzione facendo un lavoro utile. (Impostazioni lingua, compilatore e compilatore dipendenti)

Ad ogni modo, per rendersi conto di ciò, un tempo significativo dell'esecuzione deve essere speso usando questa funzione. Se la funzione viene chiamata raramente, l'overhead non importa comunque. In tal caso, l'importanza potrebbe essere quella di avere una / e funzione / e supplementare / i chiamata / e al fine di ottenere un codice più facilmente leggibile. Se possibile, potrebbero essere utilizzati strumenti di profilazione. Spesso i compilatori sono in grado di ottimizzare le chiamate di funzione quindi non è un problema (ma non sempre.)

Per i linguaggi compilati, è spesso possibile ottenere il compilatore per produrre un file assembly, oppure è possibile disassemblare il file oggetto contenente la funzione in questione e quindi ispezionarlo. In genere, è necessario solo se si conosce che la funzione viene chiamata abbastanza spesso in un sistema in cui la potenza di elaborazione della CPU è una risorsa scarsa.

Una volta ho fatto l '"errore" in cui avevo una catena di funzioni che eseguiva l'elaborazione del segnale campione per campione (per semplicità iniziare con). Quindi per ogni campione c'erano più chiamate di funzione. Ovviamente ho notato scarse prestazioni e ho modificato il codice per lavorare con array di campioni. Dopo di ciò, l'utilizzo della CPU del programma era quasi inesistente (rispetto alla CPU legata in precedenza) facendo esattamente la stessa elaborazione del segnale ...

La mia conclusione è che normalmente, (su un PC) in genere è possibile utilizzare le chiamate di funzione come preferisci, preferibilmente per la leggibilità. Tuttavia, quando si implementano determinati algoritmi di elaborazione intensiva, occorre prestare attenzione al numero di chiamate di funzione che vengono eseguite per parte di dati elaborati.

    
risposta data 10.05.2016 - 23:14
fonte
1

Ho misurato il sovraccarico delle chiamate di funzione C ++ dirette e virtuali su Xenon PowerPC qualche tempo fa .

Le funzioni in questione avevano un singolo parametro e un singolo ritorno, quindi il passaggio dei parametri si verificava sui registri.

Per farla breve, il sovraccarico di una chiamata di funzione diretta (non virtuale) era di circa 5,5 nanosecondi o 18 cicli di clock, rispetto a una chiamata di funzione in linea. Il sovraccarico di una chiamata di funzione virtuale era di 13,2 nanosecondi o 42 cicli di clock, rispetto a quello in linea.

Questi tempi sono probabilmente diversi su diverse famiglie di processori. Il mio codice di prova è qui ; puoi eseguire lo stesso esperimento sul tuo hardware. Utilizza un timer di alta precisione come rdtsc per l'implementazione di CFastTimer; il tempo di sistema () non è abbastanza preciso.

    
risposta data 18.05.2016 - 20:45
fonte

Leggi altre domande sui tag