Perché i compilatori non incorporano tutto? [chiuso]


13

A volte i compilatori chiamano le funzioni in linea. Ciò significa che spostano il codice della funzione chiamata nella funzione chiamante. Questo rende le cose leggermente più veloci perché non c'è bisogno di spingere e pop cose dentro e fuori dallo stack di chiamate.

Quindi la mia domanda è: perché i compilatori non incorporano tutto? Presumo che renderebbe l'eseguibile notevolmente più veloce.

L'unica ragione a cui riesco a pensare è un eseguibile significativamente più grande, ma conta davvero in questi giorni con centinaia di GB di memoria? Le prestazioni migliorate non valgono la pena?

C'è qualche altro motivo per cui i compilatori non incorporano solo tutte le chiamate di funzione?


18
IDK su di te, ma non ho centinaia di GB di memoria in giro.
Appunto

2
Isn't the improved performance worth it?Per un metodo che eseguirà un ciclo 100 volte e scricchiolerà alcuni numeri seri, il sovraccarico di spostare 2 o 3 argomenti nei registri della CPU non è niente.
Doval,

5
Sei eccessivamente generico, "compilatori" significa "tutti i compilatori" e "tutto" significa davvero "tutto"? Quindi la risposta è semplice, ci sono situazioni in cui semplicemente non puoi inline. Mi viene in mente la ricorsione.
Otávio Décio,

17
La localizzazione della cache è molto più importante dell'overhead di una piccola funzione.
SK-logic,

3
Al giorno d'oggi, il miglioramento delle prestazioni conta davvero con centinaia di GFLOPS di potere di elaborazione?
mouviciel,

Risposte:


22

La prima nota che uno dei principali effetti di inline è che consente ulteriori ottimizzazioni nel sito di chiamata.

Per la tua domanda: ci sono cose che sono difficili o addirittura impossibili da includere:

  • librerie collegate dinamicamente

  • funzioni determinate dinamicamente (invio dinamico, chiamato tramite puntatori a funzione)

  • funzioni ricorsive (can recursion tail)

  • funzioni per le quali non si dispone del codice (ma l'ottimizzazione del tempo di collegamento lo consente per alcuni di essi)

Quindi l'allineamento non ha solo effetti benefici:

  • eseguibile più grande significa più spazio su disco e maggiore tempo di caricamento

  • eseguibile più grande significa aumento della pressione della cache (si noti che l'aggiunta di funzioni abbastanza piccole come semplici getter può ridurre la dimensione dell'eseguibile e la pressione della cache)

E infine, per le funzioni che richiedono un tempo non banale per eseguire, il guadagno non vale la pena.


3
alcune chiamate ricorsive possono essere integrate (chiamate di coda), ma tutte possono essere trasformate in iterazione se si aggiunge facoltativamente uno stack
maniaco del cricchetto

@ratchetfreak, puoi anche trasformare una chiamata ricorsiva non di coda in una di coda. Ma questo è per me nel regno del "difficile" (soprattutto quando si hanno funzioni ricorsive o si deve determinare dinamicamente dove saltare per simulare il ritorno), ma non è impossibile (si mette semplicemente in atto un framework di continuazione e considerato che presente diventa più facile).
AProgrammer,

11

Una grande limitazione è il polimorfismo di runtime. Se si verifica un dispacciamento dinamico durante la scrittura foo.bar(), è impossibile incorporare la chiamata del metodo. Questo spiega perché i compilatori non incorporano tutto.

Nemmeno le chiamate ricorsive possono essere facilmente integrate.

L'allineamento del modulo incrociato è anche difficile da eseguire per motivi tecnici (la ricompilazione incrementale sarebbe impossibile per es.)

Tuttavia, i compilatori incorporano molte cose.


3
Inline tramite una spedizione virtuale è molto difficile, ma non impossibile. Alcuni compilatori C ++ sono in grado di farlo in determinate circostanze.
bstamour,

2
... così come alcuni compilatori JIT (devirtualizzazione).
Frank,

@bstamour Qualsiasi compilatore semi-decente di qualsiasi linguaggio con ottimizzazioni appropriate su invierà staticamente, cioè devirtualise, una chiamata a un metodo dichiarato virtuale su un oggetto il cui tipo dinamico è conoscibile al momento della compilazione. Questo può facilitare l'allineamento se la fase di devirtualizzazione si verifica prima della (o un'altra) fase di allineamento. Ma questo è banale. C'era qualcos'altro che intendevi? Non vedo come si possa ottenere qualsiasi "Inline attraverso una spedizione virtuale". Per linea, bisogna conoscere il tipo statico - cioè devirtualise - così l'esistenza di mezzi inlining non v'è alcuna spedizione virtuale
underscore_d

9

In primo luogo, non è sempre possibile inline, ad esempio le funzioni ricorsive potrebbero non essere sempre inlinabili (ma un programma contenente una definizione ricorsiva di factsolo con una stampa di fact(8)potrebbe essere inline).

Quindi, l'allineamento non è sempre vantaggioso. Se il compilatore si allinea così tanto che il codice del risultato è abbastanza grande da avere le sue parti calde non adatte per esempio nella cache dell'istruzione L1, potrebbe essere molto più lento della versione non incorporata (che si adatterebbe facilmente alla cache L1) ... Inoltre, i processori recenti sono molto veloci nell'esecuzione di CALLun'istruzione macchina (almeno in una posizione nota, cioè una chiamata diretta, non una chiamata tramite puntatore).

Alla fine, l'allineamento completo richiede un'analisi completa del programma. Questo potrebbe non essere possibile (o è troppo costoso). Con C o C ++ compilato da GCC (e anche con Clang / LLVM ) è necessario abilitare l' ottimizzazione del tempo di collegamento (compilando e collegando ad es. g++ -flto -O2) E ciò richiede parecchio tempo di compilazione.


1
Per la cronaca, LLVM / Clang (e molti altri compilatori) supporta anche l'ottimizzazione del tempo di collegamento .
Si

Lo so; LTO esisteva nel secolo precedente (IIRC, almeno in alcuni compilatori proprietari di MIPS).
Basile Starynkevitch,

7

Per quanto sorprendente possa sembrare, integrare tutto non riduce necessariamente i tempi di esecuzione. La maggiore dimensione del codice può rendere difficile per la CPU conservare tutto il codice nella sua cache in una volta. Una cache cache sul codice diventa più probabile e una cache cache è costosa. Ciò è molto peggiorato se le funzioni potenzialmente incorporate sono grandi.

Di tanto in tanto ho notato notevoli miglioramenti delle prestazioni prendendo grandi blocchi di codice contrassegnati come "inline" dai file di intestazione, inserendoli nel codice sorgente, quindi il codice è solo in un posto piuttosto che in ogni sito di chiamata. Quindi la cache della CPU viene utilizzata meglio e ottieni anche un tempo di compilazione migliore ...


questo sembra semplicemente ripetere punti fatti e spiegati in una risposta precedente che era stata pubblicata un'ora fa
moscerino

1
Quali cache? L1? L2? L3? Qual è più importante?
Peter Mortensen,

1

Integrare tutto non significherebbe solo un aumento del consumo di memoria del disco, ma anche un aumento del consumo di memoria interna che non è così abbondante. Ricorda che il codice si basa anche sulla memoria nel segmento di codice; se una funzione viene chiamata da 10000 posizioni (ad esempio quelle da librerie standard in un progetto abbastanza grande), il codice per quella funzione occupa 10000 volte più memoria interna.

Un altro motivo potrebbe essere i compilatori JIT; se tutto è in linea, non ci sono hot spot da compilare dinamicamente.


1

Uno, ci sono semplici esempi in cui l'inserimento di tutto funzionerà molto male. Considera questo semplice codice C:

void f1 (void) { printf ("Hello, world\n"); }
void f2 (void) { f1 (); f1 (); f1 (); f1 (); }
void f3 (void) { f2 (); f2 (); f2 (); f2 (); }
...
void f99 (void) { f98 (); f98 (); f98 (); f98 (); }

Indovina cosa ti farà tutto.

Successivamente, supponi che l'allineamento renderà le cose più veloci. Questo è il caso a volte, ma non sempre. Uno dei motivi è che il codice che si adatta alla cache delle istruzioni viene eseguito molto più velocemente. Se chiamo una funzione da 10 posizioni, eseguirò sempre il codice che si trova nella cache delle istruzioni. Se è in linea, le copie sono ovunque e girano molto più lentamente.

Ci sono altri problemi: Inlining produce enormi funzioni. Le funzioni enormi sono molto più difficili da ottimizzare. Ho ottenuto notevoli vantaggi nel codice critico per le prestazioni nascondendo le funzioni in un file separato per impedire al compilatore di incorporarle. Di conseguenza, il codice generato per queste funzioni era molto meglio quando erano nascoste.

BTW. Non ho "centinaia di GB di memoria". Il mio computer di lavoro non ha nemmeno "centinaia di GB di spazio sul disco rigido". E se la mia applicazione fosse "centinaia di GB di memoria", occorrerebbero 20 minuti solo per caricare l'applicazione in memoria.

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.