Funzioni Inline in Linguaggio C

Le funzioni inline introdotte in C99 consentono di ridurre l'overhead delle chiamate, favorendo prestazioni migliori rispetto all'approccio tradizionale basato su funzioni normali o macro parametrizzate.

Tuttavia, la parola chiave inline è soltanto un suggerimento al compilatore, che può decidere se inserire effettivamente il corpo della funzione nel punto in cui viene richiamata.

In questo articolo vedremo come dichiarare funzioni inline, le relative implicazioni di linkage e le restrizioni imposte dallo standard.

Funzioni inline

Le dichiarazioni di funzione in C99 hanno un'opzione aggiuntiva che non esiste in C89: possono contenere la parola chiave inline.

Per capire l'effetto di inline, dobbiamo immaginare le istruzioni a livello macchina generate dal compilatore C per gestire la chiamata e il ritorno da una funzione.

A livello macchina, possono essere necessarie diverse istruzioni per preparare la chiamata; la chiamata stessa comporta il salto alla prima istruzione della funzione e potrebbero esserci istruzioni aggiuntive eseguite dalla funzione all'avvio dell'esecuzione.

Se la funzione ha degli argomenti, questi devono essere copiati (poiché in C vengono passati per valore).

Il ritorno da una funzione richiede un impegno simile, sia da parte della funzione chiamata sia da parte di quella chiamante.

L'insieme di operazioni necessarie per chiamare e ritornare da una funzione è spesso definito overhead di chiamata, perché è lavoro in più rispetto a ciò che realmente la funzione dovrebbe svolgere.

Definizione

Overhead di Chiamata

L'overhead di chiamata è il lavoro aggiuntivo che il programma deve svolgere per chiamare una funzione e ritornare da essa. Questo lavoro include la preparazione dei parametri, il salto alla funzione e il ritorno alla funzione chiamante.

Sebbene l'overhead di una singola chiamata possa rallentare il programma di una quantità minima, in alcuni casi può incidere in modo significativo (ad esempio, quando una funzione viene chiamata milioni o miliardi di volte, oppure su processori molto lenti, o in sistemi embedded con vincoli temporali stringenti).

Nello standard C89, l'unico modo per evitare l'overhead di una chiamata di funzione era usare una macro parametrizzata. Questo perché le macro vengono espanse direttamente nel punto in cui vengono chiamate, senza alcun overhead di chiamata. Si tratta a tutti gli effetti di una sostituzione testuale.

Tuttavia, le macro parametrizzate presentano diversi svantaggi:

  • Non controllano i tipi degli argomenti;
  • Possono essere difficili da scrivere e da leggere;
  • Non si può sfruttare la ricorsione;
  • Non possono chiamare altre macro.

C99 offre una soluzione migliore al problema: creare una funzione inline. La parola chiave inline suggerisce un'implementazione in cui il compilatore sostituisce la chiamata di funzione con le istruzioni macchina che ne rappresentano il corpo. In questo modo si elimina l'overhead della chiamata, anche se il codice compilato può aumentare di dimensioni.

Dichiarare una funzione come inline non costringe realmente il compilatore ad applicare il processo alla funzione, ma è un semplice suggerimento affinché il compilatore cerchi di rendere le chiamate a quella funzione il più rapide possibile, eventualmente espandendo inline la funzione dove viene chiamata.

Il compilatore è libero di ignorare questo suggerimento. In tal senso, inline è simile alle parole chiave register e restrict, che il compilatore può utilizzare per ottimizzare le prestazioni di un programma, ma può anche scegliere di ignorarle.

Ricapitolando:

Definizione

Funzione Inline

Una funzione inline è una funzione dichiarata con la parola chiave inline. Questo suggerisce al compilatore di sostituire le chiamate alla funzione con il corpo della funzione stessa, eliminando l'overhead di chiamata.

Definizioni Inline

Una funzione inline ha la parola chiave inline come parte dei suoi specificatori di dichiarazione:

inline double average(double a, double b)
{
    return (a + b) / 2;
}

Qui la situazione diventa più complessa. La funzione average ha external linkage, quindi altri file sorgente potrebbero contenere funzioni chiamate average. Tuttavia, la definizione di average non viene considerata una definizione esterna dal compilatore (bensì una inline definition), quindi tentare di chiamare average da un altro file potrebbe causare un errore.

Esistono due modi per evitare questo problema:

  1. Aggiungere la parola chiave static alla definizione della funzione:

    static inline double average(double a, double b)
    {
        return (a + b) / 2;
    }
    

    Adesso average ha internal linkage, e non può essere chiamata da altri file. Altri file possono contenere proprie definizioni di average, che possono differire da questa.

  2. Fornire una definizione esterna per average, in modo che possa essere richiamata da altri file. Un modo per farlo è scrivere la funzione average una seconda volta (senza usare inline) e metterla in un file sorgente diverso. Tuttavia, avere due versioni della stessa funzione può essere rischioso, perché non è garantito che rimangano coerenti se il programma viene modificato.

Un approccio migliore è il seguente:

  • Mettiamo la definizione inline di average in un file header (ad esempio average.h):

    #ifndef AVERAGE_H
    #define AVERAGE_H
    
    inline double average(double a, double b)
    {
        return (a + b) / 2;
    }
    
    #endif
    
  • Creiamo quindi un file sorgente abbinato, average.c:

    #include "average.h"
    
    extern double average(double a, double b);
    

Qualsiasi file che necessiti di chiamare la funzione average può semplicemente includere average.h, che contiene la definizione inline di average. Il file average.c contiene un prototipo per average che usa la parola chiave extern, il che fa sì che la definizione della funzione, inclusa da average.h, venga trattata come definizione esterna in average.c.

La regola generale in C99 è che se tutte le dichiarazioni a livello di file di una funzione in un dato file includono inline ma non extern, la definizione della funzione in quel file è inline.

Se la funzione viene usata altrove nel programma (compreso il file che contiene la definizione inline), una definizione esterna della funzione dovrà essere fornita da un altro file.

Quando la funzione viene chiamata, il compilatore potrà scegliere di effettuare una chiamata ordinaria (usando la definizione esterna) o un'espansione inline (usando la definizione inline).

Non c'è modo di sapere quale scelta farà il compilatore, per cui è essenziale che le due definizioni siano coerenti. La tecnica appena illustrata (usare average.h e average.c) garantisce che tali definizioni siano le stesse.

Restrizioni sulle Funzioni Inline

Poiché le funzioni inline vengono implementate in un modo piuttosto diverso dalle funzioni ordinarie, sono soggette a regole e limitazioni speciali. Le variabili con durata di archiviazione statica rappresentano un problema particolare per le funzioni inline con external linkage. Di conseguenza, C99 impone le seguenti restrizioni su una funzione inline con external linkage (ma non su una con internal linkage):

  • La funzione non può definire una variabile static modificabile.
  • La funzione non può contenere riferimenti a variabili con internal linkage.

Tuttavia, una tale funzione può definire una variabile che sia contemporaneamente static e const; ogni definizione inline della funzione può quindi creare la propria copia di quella variabile.

Uso delle Funzioni Inline con GCC

Alcuni compilatori, incluso GCC, supportavano le funzioni inline prima dello standard C99. Di conseguenza, le regole sull'uso delle funzioni inline possono variare a seconda della versione. In particolare, lo schema descritto in precedenza (usare i file average.h e average.c) potrebbe non funzionare con alcuni compilatori.

Le funzioni dichiarate sia static sia inline dovrebbero funzionare correttamente, indipendentemente dalla versione di GCC. Questa strategia è legale in C99, quindi è la più sicura. Una funzione static inline può essere usata in un singolo file o inclusa in un file header e inserita in qualsiasi file sorgente che ne abbia bisogno.

Esiste un altro modo per condividere una funzione inline tra più file, compatibile con le versioni più vecchie di GCC ma in conflitto con C99. Questa tecnica prevede di inserire la definizione della funzione in un file header, specificando che la funzione è sia extern sia inline, per poi inserire in un altro file sorgente una seconda copia della definizione (senza le parole extern e inline). In questo modo, se il compilatore sceglie di applicare il processo di inline alla funzione per qualunque motivo, avrà comunque una definizione disponibile.

Un'ultima nota su GCC: le funzioni vengono rese inline soltanto quando l'ottimizzazione è richiesta tramite l'opzione -O nella linea di comando.

In Sintesi

In questa lezione abbiamo appreso nuovi concetti riguardanti le funzioni:

  • Overhead di funzione: chiamare e restituire da una funzione comporta costi aggiuntivi (overhead), che le funzioni inline mirano a ridurre.
  • Parola chiave inline: indica che il compilatore può sostituire la chiamata con il corpo della funzione, eliminando il salto di esecuzione.
  • External vs. internal linkage: se la funzione inline ha external linkage, occorre fornire anche una definizione “esterna” per consentire la chiamata da altri file.
  • Restrizioni sulle funzioni inline: alcune variabili statiche o con internal linkage non sono compatibili con funzioni inline che abbiano external linkage.
  • Supporto GCC: diverse versioni di GCC possono gestire le inline diversamente rispetto allo standard C99; la strategia più sicura resta usare static inline o seguire lo schema “header + definizione esterna”.