Le pile ci consentono di aggirare elegantemente i limiti imposti dal numero finito di registri.
Immagina di avere esattamente 26 "registri az" globali (o anche di avere solo i registri delle dimensioni di 7 byte del chip 8080) E ogni funzione che scrivi in questa app condivide questo elenco piatto.
Un inizio ingenuo sarebbe quello di allocare i primi pochi registri alla prima funzione, e sapendo che ci sono voluti solo 3, inizia con "d" per la seconda funzione ... Ti esaurisci rapidamente.
Invece, se hai un nastro metaforico, come il turing machine, potresti avere ciascuna funzione che avvia una "chiamata un'altra funzione" salvando tutte le variabili che sta usando e inoltra () il nastro, e quindi la funzione di chiamata può confondersi con altrettante si registra come vuole. Al termine della chiamata, restituisce il controllo alla funzione genitore, che sa dove catturare l'output della chiamata, se necessario, e quindi riproduce il nastro all'indietro per ripristinarne lo stato.
Il frame delle chiamate di base è proprio questo, e viene creato e rilasciato da sequenze di codice macchina standardizzate che il compilatore inserisce attorno alle transizioni da una funzione all'altra. (È da tanto tempo che devo ricordare i miei frame stack C, ma puoi leggere in vari modi i doveri di chi lascia cadere ciò che su X86_calling_conventions .)
(La ricorsione è fantastica, ma se avessi mai dovuto destreggiarti tra i registri senza uno stack, apprezzeresti davvero le pile.)
Suppongo che lo spazio su disco rigido e la RAM necessari per memorizzare il programma e supportarne la compilazione (rispettivamente) siano il motivo per cui utilizziamo gli stack di chiamate. È corretto?
Mentre oggi possiamo integrarci di più ("maggiore velocità" è sempre buona; "meno kb di assembly" significa molto poco in un mondo di flussi video) Il limite principale è nella capacità del compilatore di appiattirsi attraverso determinati tipi di schemi di codice.
Ad esempio, oggetti polimorfici - se non conosci l'unico e solo tipo di oggetto che ti verrà consegnato, non puoi appiattirti; devi guardare la vtable delle caratteristiche dell'oggetto e chiamare attraverso quel puntatore ... banale da fare in fase di esecuzione, impossibile da incorporare in fase di compilazione.
Una moderna toolchain può felicemente incorporare una funzione polimorficamente definita quando ha appiattito abbastanza il / i chiamante / i per sapere esattamente quale sapore di obj è:
class Base {
public: void act() = 0;
};
class Child1: public Base {
public: void act() {};
};
void ActOn(Base* something) {
something->act();
}
void InlineMe() {
Child1 thingamabob;
ActOn(&thingamabob);
}
in quanto sopra, il compilatore può scegliere di continuare a allineare staticamente, da InlineMe a tutto ciò che è dentro act (), né la necessità di toccare alcun vtables in fase di esecuzione.
Ma qualsiasi incertezza su quale sapore dell'oggetto lo lascerà come una chiamata a una funzione discreta, anche se alcune altre invocazioni della stessa funzione sono sottolineate.