Quanto influiscono le chiamate di funzione sulle prestazioni?


13

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

Ciò significa che verranno effettuate più funzioni.

In che modo la suddivisione del nostro codice in metodi o funzioni influisce effettivamente sulle prestazioni in linguaggi moderni * ?

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



1
Credo che ogni implementazione linguistica degna di nota sia in linea da diversi decenni. IOW, il sovraccarico è esattamente 0.
Jörg W Mittag

1
"Verranno effettuate più chiamate di funzione" spesso non è vero poiché molte di queste chiamate avranno l'overhead ottimizzato dai vari compilatori / interpreti che elaborano il codice e inseriscono le cose. Se la tua lingua non ha questo tipo di ottimizzazioni, potrei non considerarla moderna.
Ixrec,

2
In che modo influirà sulle prestazioni? Lo renderà più veloce, o più lento, o non lo cambierà, a seconda di quale lingua specifica usi e quale sia la struttura del codice effettivo e possibilmente su quale versione del compilatore stai usando e forse anche quale piattaforma stai ' stai correndo. Ogni risposta che otterrai sarà una variazione di questa incertezza, con più parole e più prove a sostegno.
GrandOpener,

1
L'impatto, se presente, è così piccolo che tu, una persona, non lo noterai mai . Ci sono altre cose molto più importanti di cui preoccuparsi. Ad esempio se le schede devono essere di 5 o 7 spazi.
MetaFight,

Risposte:


21

Può essere. Il compilatore potrebbe decidere "ehi, questa funzione viene chiamata solo poche volte e dovrei ottimizzare la velocità, quindi inserirò questa funzione". In sostanza, il compilatore sostituirà la chiamata di 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 inline DoSomethingElsee il codice diventa

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

Quando le funzioni non sono incorporate, sì, si verifica un hit delle prestazioni per effettuare una chiamata di funzione. Tuttavia, è un successo così minuscolo che solo il codice di prestazioni estremamente elevate si preoccuperà delle chiamate di funzione. E su questi tipi di progetti, il codice è in genere scritto in assembly.

Le chiamate di funzione (a seconda della piattaforma) in genere comportano alcune decine di istruzioni, 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 sulle prestazioni delle chiamate di funzione. La funzione chiamata potrebbe non essere caricata nella cache del processore, causando un mancato funzionamento della cache e costringendo il controller di memoria a catturare la funzione dalla RAM principale. Ciò può causare un grande successo per le prestazioni.

In breve: le chiamate di funzione possono o meno influire sulle prestazioni. L'unico modo per dirlo è profilare il tuo codice. Non provare a indovinare dove si trovano gli spot di codice lenti, perché il compilatore e l'hardware hanno alcuni incredibili trucchi nelle maniche. Profilare il codice per ottenere la posizione dei punti lenti.


1
Ho visto con compilatori moderni (gcc, clang) in situazioni in cui mi importava davvero che creassero un codice piuttosto negativo per i loop all'interno di una grande funzione . L'estrazione del loop in una funzione statica non ha aiutato a causa dell'inline. L'estrazione del loop in una funzione esterna ha creato in alcuni casi significativi miglioramenti della velocità (misurabili nei parametri di riferimento).
gnasher729,

1
Vorrei tornare indietro e dire che OP dovrebbe stare attento all'ottimizzazione precoce
Patrick

1
@Patrick Bingo. Se hai intenzione di ottimizzare, usa un profiler per vedere dove sono le sezioni lente. Non indovinare. Di solito puoi avere un'idea di dove potrebbero essere le sezioni lente, ma confermalo con un profiler.
CHendrix,

@ gnasher729 Per risolvere quel particolare problema, occorrerà più di un profiler - si dovrà imparare a leggere anche il codice macchina smontato. Mentre c'è un'ottimizzazione prematura, non esiste un apprendimento precoce (almeno nello sviluppo del software).
rwong,

Si potrebbe avere questo problema se si sta chiamando una funzione di un milione di volte, ma si hanno maggiori probabilità di avere altri problemi che stanno avendo un impatto significativamente più grande.
Michael Shaw,

5

Questa è una questione di 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 incorporeranno le chiamate in base alle impostazioni di ottimizzazione - questo può essere visto in modo banale esaminando l'assemblaggio generato quando si esaminano strumenti come https://gcc.godbolt.org/

Altre lingue, come Java, hanno questo come parte del runtime. Questo fa parte della squadra comune ed elaborato in questa domanda SO . In dettaglio le opzioni JVM per HotSpot

-XX:InlineSmallCode=n Inline un metodo precedentemente compilato solo se la dimensione del suo codice nativo generato è inferiore a questo. Il valore predefinito varia in base alla piattaforma su cui è in esecuzione JVM.
-XX:MaxInlineSize=35 Dimensione massima del bytecode di un metodo da incorporare.
-XX:FreqInlineSize=n Dimensione massima del bytecode di un metodo eseguito frequentemente da incorporare. Il valore predefinito varia in base alla piattaforma su cui è in esecuzione JVM.

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

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

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

Le lingue non sono in linea. Le implementazioni possono .


5

Cerchi performance 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 si avrebbe ancora questo altro problema.

È che una funzione è come una carta di credito. Dal momento che puoi usarlo facilmente, tendi a usarlo più di quanto dovresti. Supponiamo che tu lo chiami il 20% in più del necessario. Quindi, il tipico software di grandi dimensioni contiene diversi livelli, ciascuno dei quali chiama le funzioni nel livello sottostante, quindi il fattore 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 di pensarci.

Questo non significa che dovresti evitare le chiamate di funzione. Ciò significa che, quando il codice è attivo e funzionante, dovresti sapere come trovare ed eliminare i rifiuti. Ci sono molti consigli eccellenti su come farlo su siti di stackexchange. Questo dà uno dei miei contributi.

AGGIUNTO: piccolo esempio. Una volta ho lavorato in una squadra su software di fabbrica che ha seguito 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 tutte le sue sotto-attività sono state completate, e ognuna di esse è stata eseguita quando tutte le sue sotto-operazioni sono state completate. Tutte queste cose sono state monitorate in un database relazionale. Una singola chiamata a un'altra funzione potrebbe estrarre tutte quelle informazioni, JobDonechiamate così l' altra funzione, vedere se il lavoro è stato fatto e gettare via il resto. Quindi le persone potrebbero facilmente scrivere codice in questo modo:

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

o

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

Vedi il punto? La funzione era così "potente" e facile da chiamare che veniva chiamata troppo. Quindi il problema delle prestazioni non erano le istruzioni per entrare e uscire dalla funzione. Era che doveva esserci un modo più diretto per dire se i lavori fossero stati fatti. Ancora una volta, questo codice avrebbe potuto essere incorporato in migliaia di righe di codice altrimenti innocente. Cercare di risolverlo in anticipo è ciò che tutti cercano di fare, ma è come provare a lanciare freccette in una stanza buia. Quello di cui hai bisogno invece è farlo funzionare, e poi lasciare che il "codice lento" ti dica di cosa si tratta, semplicemente prendendo tempo. Per questo uso una pausa casuale .


1

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

Anche se non conosco i dettagli specifici di Java (tranne per il fatto che ogni metodo è virtuale, ma ti consiglio di controllare meglio la documentazione), in Python sono sicuro che non ci siano allineamenti, nessuna ottimizzazione di ricorsione della coda e chiamate di funzioni sono piuttosto costose.

Le funzioni di Python sono fondamentalmente oggetti eseguibili (e infatti puoi anche definire il metodo call () per rendere un'istanza di oggetto una funzione). Ciò significa che c'è un sacco di spese generali nel chiamarli ...

MA

quando si definiscono variabili all'interno delle funzioni, l'interprete utilizza LOADFAST invece delle normali istruzioni LOAD nel bytecode, rendendo più veloce il codice ...

Un'altra cosa è che quando si definisce un oggetto richiamabile, sono possibili modelli come la memoizzazione che possono velocizzare molto il calcolo (a costo di utilizzare più memoria). Fondamentalmente è sempre un compromesso. Il costo delle chiamate di funzione dipende anche dai parametri, perché determinano quanta roba devi effettivamente copiare nello stack (quindi in c / c ++ è pratica comune passare grandi parametri come strutture tramite puntatori / riferimenti anziché 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 quella lingua specifica.

Rimarrai sorpreso da quante cose imparerai in questo processo.

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

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


Citando te: "Penso che la tua domanda sia in pratica troppo ampia per poter rispondere completamente su stackexchange." Come posso restringerlo allora? Mi piacerebbe vedere alcuni dati reali che rappresentano l'impatto della chiamata di funzione nelle prestazioni. Non mi interessa quale lingua, sono solo curioso di vedere una spiegazione più dettagliata, se possibile con il backup dei dati, come ho detto.
Dabadaba,

Il punto è che dipende dalla lingua. In C e C ++, se la funzione è inline, l'impatto è 0. Se non è inline, dipende dai suoi parametri, se è nella cache o no, ecc ...
ingframin

1

Ho misurato l'overhead 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 è verificato sui registri.

Per farla breve, l'overhead 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 quelli in linea.

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.