Capisco cos'è un puntatore dello stack - ma a cosa serve?


11

Il puntatore dello stack punta in cima allo stack, che memorizza i dati su ciò che chiamiamo "LIFO". Per rubare l'analogia di qualcun altro, è come una pila di piatti in cui metti e prendi i piatti in alto. Il puntatore dello stack, OTOH, punta al "piatto" superiore dello stack. Almeno, questo è vero per x86.

Ma perché il computer / programma "cura" ciò a cui punta il puntatore dello stack? In altre parole, quale scopo ha il puntatore dello stack e sapere dove punta servire?

Una spiegazione comprensibile per i programmatori C. sarebbe apprezzata.


Perché non puoi vedere la parte superiore della pila in ariete come puoi vedere la cima di una pila di piatti.
tkausl,


8
Tu non prendi un piatto dal fondo di una pila. Ne aggiungi uno in alto e qualcun altro lo prende dall'alto . Stai pensando a una coda qui.
Kilian Foth,

@Snowman La tua modifica sembra cambiare il significato della domanda. moonman239, puoi verificare se il cambio di Snowman è accurato, in particolare l'aggiunta di "A cosa serve effettivamente questo stack, invece di spiegarne la struttura?"
8bittree,

1
@ 8bittree Vedere la descrizione della modifica: ho copiato la domanda come indicato nella riga dell'oggetto nel corpo della domanda. Certo, sono sempre aperto alla possibilità di aver modificato qualcosa e l'autore originale è sempre libero di tornare indietro o modificare in altro modo il post.

Risposte:


23

A quale scopo serve effettivamente questo stack, anziché spiegarne la struttura?

Hai molte risposte che descrivono accuratamente la struttura dei dati memorizzati nello stack, che noto è l'opposto della domanda che hai posto.

Lo scopo che serve lo stack è: lo stack fa parte della reificazione della continuazione in un linguaggio senza coroutine .

Disimballiamo quello.

La continuazione è semplicemente la risposta alla domanda "che cosa accadrà dopo nel mio programma?" Ad ogni punto di ogni programma succederà qualcosa dopo. Verranno calcolati due operandi, quindi il programma continua calcolando la loro somma, quindi il programma continua assegnando la somma a una variabile, quindi ... e così via.

Reificazione è solo una parola di alto livello per realizzare concretamente un concetto astratto. "Cosa succede dopo?" è un concetto astratto; il modo in cui la pila è disposta fa parte di come quel concetto astratto si trasforma in una vera macchina che calcola davvero le cose.

Le coroutine sono funzioni che possono ricordare dove si trovavano, cedere il controllo a un'altra coroutine per un po ', quindi riprendere da dove si erano interrotte in seguito, ma non necessariamente immediatamente dopo le rese coroutine appena chiamate. Pensa a "return return" o "wait" in C #, che deve ricordare dove si trovavano quando viene richiesto l'articolo successivo o quando l'operazione asincrona viene completata. Le lingue con coroutine o caratteristiche linguistiche simili richiedono strutture di dati più avanzate rispetto a uno stack per implementare la continuazione.

In che modo uno stack implementa la continuazione? Altre risposte dicono come. Lo stack memorizza (1) i valori di variabili e temporali le cui vite sono note per non essere superiori all'attivazione del metodo corrente e (2) l'indirizzo del codice di continuazione associato all'attivazione del metodo più recente. Nelle lingue con gestione delle eccezioni, lo stack può anche memorizzare informazioni sulla "continuazione dell'errore", vale a dire cosa farà il programma dopo quando si verifica una situazione eccezionale.

Vorrei cogliere l'occasione per notare che lo stack non ti dice "da dove vengo?" - sebbene sia spesso utilizzato nel debug. Lo stack ti dice dove stai andando e quali saranno i valori delle variabili di attivazione quando ci arriverai . Il fatto che in una lingua senza coroutine, dove stai andando dopo, è quasi sempre da dove vieni rende questo tipo di debug più semplice. Ma non è necessario che un compilatore memorizzi le informazioni da dove proviene il controllo se può cavarsela senza farlo. Le ottimizzazioni delle chiamate in coda, ad esempio, distruggono le informazioni sulla provenienza del controllo del programma.

Perché utilizziamo lo stack per implementare la continuazione in linguaggi senza coroutine? Perché la caratteristica dell'attivazione sincrona dei metodi è che il modello di "sospendere il metodo corrente, attivare un altro metodo, riprendere il metodo corrente conoscendo il risultato del metodo attivato" quando composto con se stesso forma logicamente una pila di attivazioni. Realizzare una struttura di dati che implementa questo comportamento simile a uno stack è molto economico e facile. Perché è così economico e facile? Poiché i set di chip sono stati progettati per molti decenni appositamente per rendere questo tipo di programmazione facile per gli autori di compilatori.


Si noti che la citazione a cui si fa riferimento è stata erroneamente aggiunta in una modifica da un altro utente e da allora è stata corretta, rendendo questa risposta non del tutto rivolta alla domanda.
8bittree

2
Sono abbastanza certo che una spiegazione dovrebbe aumentare la chiarezza. Non sono del tutto convinto che "la pila è parte della reificazione di seguito in una lingua senza coroutine" viene anche vicino a quello :-)

4

L'utilizzo di base dello stack è di memorizzare l'indirizzo di ritorno per le funzioni:

void a(){
    sub();
}
void b(){
    sub();
}
void sub() {
    //should i got back to a() or to b()?
}

e dal punto di vista C questo è tutto. Dal punto di vista del compilatore:

  • tutti gli argomenti delle funzioni vengono passati dai registri della CPU - se non ci sono abbastanza registri, gli argomenti verranno messi in pila
  • dopo che la funzione termina (la maggior parte) i registri devono avere gli stessi valori di prima di immetterli, quindi i registri usati vengono sottoposti a backup in pila

E dal punto di vista del sistema operativo: il programma può essere interrotto in qualsiasi momento, quindi dopo aver completato l'attività di sistema, è necessario ripristinare lo stato della CPU, quindi è possibile memorizzare tutto nello stack

Tutto questo funziona dal momento che non ci importa di quanti oggetti sono già in pila o quanti altri verranno aggiunti in futuro, dobbiamo solo sapere quanto abbiamo spostato il puntatore della pila e ripristinarlo dopo aver finito.


1
Penso che sia più preciso affermare che gli argomenti vengono inseriti nello stack, anche se spesso come registri di ottimizzazione vengono utilizzati invece su processori che hanno abbastanza registri gratuiti per l'attività. Questo è un problema, ma penso che corrisponda meglio a come le lingue si sono evolute storicamente. I primi compilatori C / C ++ non usavano affatto i registri per questo.
Gort il robot il

4

LIFO vs FIFO

LIFO sta per Last In, First Out. Come in, l'ultimo oggetto messo nella pila è il primo oggetto estratto dalla pila.

Quello che hai descritto con l'analogia dei tuoi piatti (nella prima revisione ), è una coda o FIFO, First In, First Out.

La differenza principale tra i due è che il LIFO / stack spinge (inserisce) e si apre (rimuove) dalla stessa estremità, e una FIFO / coda lo fa dalle estremità opposte.

// Both:

Push(a)
-> [a]
Push(b)
-> [a, b]
Push(c)
-> [a, b, c]

// Stack            // Queue
Pop()               Pop()
-> [a, b]           -> [b, c]

Il puntatore dello stack

Diamo un'occhiata a ciò che sta accadendo sotto il cofano dello stack. Ecco un po 'di memoria, ogni casella è un indirizzo:

...[ ][ ][ ][ ]...                       char* sp;
    ^- Stack Pointer (SP)

E c'è un puntatore dello stack che punta nella parte inferiore dello stack attualmente vuoto (se lo stack cresce o diminuisce non è particolarmente rilevante qui, quindi lo ignoreremo, ma ovviamente nel mondo reale, che determina quale operazione aggiunge e quali sottrae dal SP).

Quindi spingiamo di a, b, and cnuovo. Grafica a sinistra, operazione "alto livello" al centro, pseudo codice C-ish a destra:

...[a][ ][ ][ ]...        Push('a')      *sp = 'a';
    ^- SP
...[a][ ][ ][ ]...                       ++sp;
       ^- SP

...[a][b][ ][ ]...        Push('b')      *sp = 'b';
       ^- SP
...[a][b][ ][ ]...                       ++sp;
          ^- SP

...[a][b][c][ ]...        Push('c')      *sp = 'c';
          ^- SP
...[a][b][c][ ]...                       ++sp;
             ^- SP

Come puoi vedere, ogni volta che inseriamo pushl'argomento nella posizione attualmente puntata dal puntatore dello stack e regola il puntatore dello stack in modo che punti nella posizione successiva.

Ora pop:

...[a][b][c][ ]...        Pop()          --sp;
          ^- SP
...[a][b][c][ ]...                       return *sp; // returns 'c'
          ^- SP
...[a][b][c][ ]...        Pop()          --sp;
       ^- SP
...[a][b][c][ ]...                       return *sp; // returns 'b'
       ^- SP

Popè l'opposto di push, regola il puntatore dello stack in modo che punti alla posizione precedente e rimuove l'elemento che era lì (di solito per restituirlo a chiunque abbia chiamato pop).

Probabilmente l'hai notato be csono ancora nella memoria. Voglio solo assicurarti che quelli non sono errori di battitura. Torneremo su quello a breve.

La vita senza un puntatore pila

Vediamo cosa succede se non abbiamo un puntatore allo stack. A partire dalla spinta di nuovo:

...[ ][ ][ ][ ]...
...[ ][ ][ ][ ]...        Push(a)        ? = 'a';

Ehm, hmm ... se non abbiamo un puntatore dello stack, allora non possiamo spostare qualcosa all'indirizzo a cui punta. Forse possiamo usare un puntatore che punta alla base anziché all'inizio.

...[ ][ ][ ][ ]...                       char* bp; // "base pointer"
    ^- bp                                bp = malloc(...);

...[a][ ][ ][ ]...        Push(a)        *bp = 'a';
    ^- bp
// No stack pointer, so no need to update it.
...[b][ ][ ][ ]...        Push(b)        *bp = 'b';
    ^- bp

Uh Oh. Poiché non possiamo modificare il valore fisso della base dello stack, abbiamo semplicemente sovrascritto aspingendolo bnella stessa posizione.

Bene, perché non teniamo traccia di quante volte abbiamo spinto. E dovremo anche tenere traccia dei tempi in cui siamo spuntati.

...[ ][ ][ ][ ]...                       char* bp; // "base pointer"
    ^- bp                                bp = malloc(...);
                                         int count = 0;

...[a][ ][ ][ ]...        Push(a)        bp[count] = 'a';
    ^- bp
...[a][ ][ ][ ]...                       ++count;
    ^- bp
...[a][b][ ][ ]...        Push(a)        bp[count] = 'b';
    ^- bp
...[a][b][ ][ ]...                       ++count;
    ^- bp
...[a][b][ ][ ]...        Pop()          --count;
    ^- bp
...[a][b][ ][ ]...                       return bp[count]; //returns b
    ^- bp

Bene funziona, ma in realtà è abbastanza simile a prima, tranne che *pointerè più economico di pointer[offset](senza aritmetica extra), per non parlare del fatto che è meno da digitare. Mi sembra una perdita.

Proviamo di nuovo. Invece di usare lo stile di stringa Pascal per trovare la fine di una raccolta basata su array (tenendo traccia di quanti elementi ci sono nella raccolta), proviamo lo stile di stringa C (scansiona dall'inizio alla fine):

...[ ][ ][ ][ ]...                       char* bp; // "base pointer"
    ^- bp                                bp = malloc(...);

...[ ][ ][ ][ ]...        Push(a)        char* top = bp;
    ^- bp, top
                                         while(*top != 0) { ++top; }
...[ ][ ][ ][a]...                       *top = 'a';
    ^- bp    ^- top

...[ ][ ][ ][ ]...        Pop()          char* top = bp;
    ^- bp, top
                                         while(*top != 0) { ++top; }
...[ ][ ][ ][a]...                       --top;
    ^- bp       ^- top                   return *top; // returns '('

Potresti aver già indovinato il problema qui. Non è garantito che la memoria non inizializzata sia 0. Quindi, quando cerchiamo la posizione più alta a, finiamo per saltare un mucchio di posizioni di memoria inutilizzate che contengono spazzatura casuale. Allo stesso modo, quando eseguiamo la scansione verso l'alto, finiamo per saltare ben oltre ciò che aabbiamo appena spinto fino a quando non troviamo finalmente un'altra posizione di memoria che sembra essere 0, e torniamo indietro e restituiamo la spazzatura casuale appena prima.

È abbastanza facile da risolvere, dobbiamo solo aggiungere operazioni Pushe Popassicurarci che la parte superiore dello stack sia sempre aggiornata per essere contrassegnata con un 0, e dobbiamo inizializzare lo stack con un tale terminatore. Ovviamente ciò significa anche che non possiamo avere un 0(o qualunque valore scegliamo come terminatore) come valore effettivo nello stack.

Inoltre, abbiamo anche cambiato le operazioni O (1) in operazioni O (n).

TL; DR

Il puntatore dello stack tiene traccia della parte superiore dello stack, in cui si verifica tutta l'azione. Ci sono modi per sbarazzarsi di esso ( bp[count]e topsono essenzialmente ancora il puntatore dello stack), ma entrambi finiscono per essere più complicati e più lenti rispetto al semplice avere il puntatore dello stack. E non sapere dove si trova la parte superiore della pila significa che non puoi usare la pila.

Nota: il puntatore dello stack che punta alla "parte inferiore" dello stack di runtime in x86 potrebbe essere un malinteso relativo all'intero stack di runtime che è capovolto. In altre parole, la base dello stack viene posizionata in un indirizzo di memoria elevato e la punta dello stack si riduce in indirizzi di memoria inferiori. Il puntatore dello stack fa punto alla punta dello stack in cui si verifica l'opportunità, solo che punta è in un indirizzo di memoria inferiore alla base della pila.


2

Il puntatore dello stack viene utilizzato (con il puntatore del frame) per lo stack delle chiamate (seguire il collegamento a wikipedia, dove è presente una buona immagine).

Lo stack di chiamate contiene frame di chiamata, che contengono indirizzo di ritorno, variabili locali e altri dati locali (in particolare, contenuto di registri versato ; formali).

Leggi anche le chiamate di coda (alcune chiamate ricorsive di coda non necessitano di alcun frame di chiamata), la gestione delle eccezioni (come setjmp e longjmp , possono comportare il pop-up di più frame di stack contemporaneamente), segnali e interruzioni e continuazioni . Vedere anche convenzioni di chiamata e interfacce binarie di applicazioni (ABI), in particolare l' ABI x86-64 (che definisce che alcuni registri vengono passati da argomenti formali).

Inoltre, codifica alcune semplici funzioni in C, quindi usa gcc -Wall -O -S -fverbose-asm per compilarlo e guarda nel .s file assemblatore generato .

Appel ha scritto un vecchio documento del 1986 affermando che la garbage collection può essere più veloce dell'allocazione dello stack (usando lo stile di continuazione-passaggio nel compilatore), ma questo è probabilmente falso sui processori x86 di oggi (in particolare a causa degli effetti cache).

Si noti che le convenzioni di chiamata, le ABI e il layout dello stack sono diversi su 3268 i686 e 64 bit x86-64. Inoltre, le convenzioni di chiamata (e chi è responsabile dell'allocazione o del pop-up del frame di chiamata) possono essere diverse con lingue diverse (ad es. C, Pascal, Ocaml, SBCL Common Lisp hanno convenzioni di chiamata diverse ....)

A proposito, recenti estensioni x86 come AVX stanno imponendo vincoli di allineamento sempre più grandi sul puntatore dello stack (IIRC, un frame di chiamata su x86-64 vuole essere allineato a 16 byte, cioè due parole o puntatori).


1
Potresti menzionare che l'allineamento a 16 byte su x86-64 significa il doppio della dimensione / allineamento di un puntatore, che in realtà è più interessante del conteggio dei byte.
Deduplicatore il

1

In parole povere, il programma si preoccupa perché sta usando quei dati e deve tenere traccia di dove trovarli.

Se si dichiarano variabili locali in una funzione, lo stack è dove sono memorizzati. Inoltre, se chiami un'altra funzione, lo stack è dove memorizza l'indirizzo di ritorno in modo che possa tornare alla funzione in cui ti trovavi quando quella che hai chiamato è finita e riprendere da dove era stata interrotta.

Senza SP, la programmazione strutturata come la conosciamo sarebbe sostanzialmente impossibile. (Potresti aggirare il fatto di non averlo, ma richiederebbe praticamente l'implementazione della tua versione di esso, quindi non fa molta differenza.)


1
La tua affermazione che la programmazione strutturata senza uno stack sarebbe impossibile è falsa. I programmi compilati in stile di passaggio continuo non consumano stack, ma sono programmi perfettamente sensati.
Eric Lippert,

@EricLippert: per valori di "perfettamente sensato" sufficientemente insensati da includere in piedi sulla testa e capovolgersi, forse. ;-)
Mason Wheeler il

1
Con il passaggio di continuazione , è possibile che non sia necessario uno stack di chiamate. In effetti, ogni chiamata è una chiamata di coda e goto piuttosto che ritorno. "Poiché CPS e TCO eliminano il concetto di ritorno di una funzione implicita, il loro uso combinato può eliminare la necessità di uno stack di runtime."

@MichaelT: ho detto "essenzialmente" impossibile per un motivo. CPS può teoricamente raggiungere questo obiettivo, ma in pratica diventa ridicolmente molto difficile scrivere molto rapidamente il codice del mondo reale di qualsiasi complessità in CPS, come Eric ha sottolineato in una serie di post sul blog sull'argomento .
Mason Wheeler,

1
@MasonWheeler Eric sta parlando dei programmi compilati in CPS. Ad esempio, citando il blog di Jon Harrop : In fact, some compilers don’t even use stack frames [...], and other compilers like SML/NJ convert every call into continuation style and put stack frames on the heap, splitting every segment of code between a pair of function calls in the source into its own separate function in the compiled form.È diverso da "implementare la tua versione di [lo stack]".
Doval,

1

Per lo stack del processore in un processore x86, l'analogia di una pila di piatti è davvero imprecisa.
Per vari motivi (principalmente storici), lo stack del processore cresce dalla parte superiore della memoria verso la parte inferiore della memoria, quindi un'analogia migliore sarebbe la catena di maglie della catena che pendono dal soffitto. Quando si spinge qualcosa nello stack, un collegamento a catena viene aggiunto al collegamento più basso.

Il puntatore dello stack si riferisce al collegamento più basso della catena e viene utilizzato dal processore per "vedere" dove si trova quel collegamento più basso, in modo che i collegamenti possano essere aggiunti o rimossi senza dover percorrere l'intera catena dal soffitto verso il basso.

In un certo senso, all'interno di un processore x86, lo stack è sottosopra ma viene utilizzato il normale davanzale di terminazione dello stack, in modo che il collegamento più basso venga indicato come la parte superiore dello stack.


I collegamenti a catena che ho menzionato sopra sono in realtà celle di memoria in un computer e si abituano a memorizzare variabili locali e alcuni risultati intermedi di calcoli. I programmi per computer si occupano di dove si trova la parte superiore dello stack (ovvero dove si trova il link più basso), poiché la grande maggioranza delle variabili a cui una funzione deve accedere esiste vicino a dove si riferisce il puntatore dello stack ed è auspicabile un accesso rapido ad essi.


1
The stack pointer refers to the lowest link of the chain and is used by the processor to "see" where that lowest link is, so that links can be added or removed without having to travel the entire chain from the ceiling down.Non sono sicuro che questa sia una buona analogia. In realtà i collegamenti non vengono mai aggiunti o rimossi. Il puntatore dello stack è più simile a un pezzetto di nastro utilizzato per contrassegnare uno dei collegamenti. Se si perde quel nastro, non si avrà un modo per sapere che era il più in basso di collegamento si è utilizzato a tutti ; viaggiare lungo la catena dal soffitto non ti aiuterebbe.
Doval,

Quindi il puntatore dello stack fornisce un punto di riferimento che il programma / computer può usare per trovare le variabili locali di una funzione?
moonman239

In tal caso, come fa il computer a trovare le variabili locali? Cerca semplicemente tutti gli indirizzi di memoria dal basso verso l'alto?
moonman239

@ moonman239: No, durante la compilazione, il compilatore tiene traccia di dove è memorizzata ogni variabile rispetto al puntatore dello stack. Il processore comprende tale indirizzamento relativo per fornire accesso diretto alle variabili.
Bart van Ingen Schenau,

1
@BartvanIngenSchenau Ah, OK. Un po 'come quando sei nel bel mezzo del nulla e hai bisogno di aiuto, quindi dai al 911 un'idea di dove sei rispetto a un punto di riferimento. Il puntatore dello stack, in questo caso, di solito è il "punto di riferimento" più vicino e quindi, forse, il miglior punto di riferimento.
moonman239,

1

Questa risposta si riferisce specificamente il puntatore dello stack del thread corrente (di esecuzione) .

Nei linguaggi di programmazione procedurale, un thread in genere ha accesso a uno stack 1 per i seguenti scopi:

  • Flusso di controllo, ovvero "stack di chiamate".
    • Quando una funzione chiama un'altra funzione, lo stack di chiamate ricorda dove tornare.
    • Uno stack di chiamate è necessario perché è così che vogliamo che una "chiamata di funzione" si comporti - "riprendere da dove avevamo interrotto" .
    • Esistono altri stili di programmazione che non hanno chiamate di funzione nel mezzo dell'esecuzione (ad esempio, è consentito specificare la funzione successiva solo quando viene raggiunta la fine della funzione corrente) o non hanno chiamate di funzioni (usando solo goto e salti condizionali ). Questi stili di programmazione potrebbero non aver bisogno di uno stack di chiamate.
  • Parametri di chiamata funzione.
    • Quando una funzione chiama un'altra funzione, i parametri possono essere inseriti nello stack.
    • È necessario che il chiamante e il chiamante seguano la stessa convenzione di chi è responsabile della cancellazione dei parametri dallo stack, al termine della chiamata.
  • Variabili locali che vivono all'interno di una chiamata di funzione.
    • Si noti che una variabile locale appartenente a un chiamante può essere resa accessibile a un chiamante passando un puntatore a quella variabile locale al chiamante.

Nota 1 : dedicato all'uso del thread, sebbene il suo contenuto sia interamente leggibile - e cancellabile - da altri thread.

Nella programmazione assembly, C e C ++, tutti e tre gli scopi possono essere raggiunti dallo stesso stack. In alcune altre lingue, alcuni scopi possono essere raggiunti da stack separati o memoria allocata dinamicamente.


1

Ecco una versione deliberatamente semplificata di ciò per cui viene utilizzato lo stack.

Immagina la pila come una pila di schede. Il puntatore della pila punta sulla prima carta.

Quando si chiama una funzione:

  • Scrivi l'indirizzo del codice immediatamente dopo la riga che ha chiamato la funzione su una carta e lo metti in pila. (Vale a dire aumentare il puntatore dello stack di uno e scrivere l'indirizzo nel punto in cui punta)
  • Quindi annoti i valori contenuti nei registri su alcune carte e li metti in pila. (ad esempio, si aumenta il puntatore dello stack in base al numero di registri e si copia il contenuto del registro nella posizione in cui punta)
  • Quindi metti una carta segnapunti sulla pila. (ovvero si salva il puntatore dello stack corrente).
  • Quindi scrivi il valore di ogni parametro con cui viene chiamata la funzione, uno su una carta, e lo metti in pila. (ad esempio, si aumenta il puntatore dello stack in base al numero di parametri e si scrivono i parametri nel punto in cui punta il puntatore dello stack.)
  • Quindi aggiungi una scheda per ogni variabile locale, potenzialmente scrivendo il valore iniziale su di essa. (ovvero incrementa il puntatore dello stack in base al numero di variabili locali.)

A questo punto, il codice nella funzione viene eseguito. Il codice è compilato per sapere dove ogni carta è relativa alla cima. Quindi sa che la variabile xè la terza carta dall'alto (cioè il puntatore dello stack - 3) e che il parametro yè la sesta carta dall'alto (cioè il puntatore dello stack - 6.)

Questo metodo significa che l'indirizzo di ogni variabile o parametro locale non deve essere inserito nel codice. Al contrario, tutti questi elementi di dati vengono indirizzati in relazione al puntatore dello stack.

Quando la funzione ritorna, l'operazione inversa è semplicemente:

  • Cerca la carta marcatore e lancia tutte le carte sopra di essa. (ad es. impostare il puntatore dello stack sull'indirizzo salvato.)
  • Ripristina i registri dalle carte salvate in precedenza e gettale via. (ovvero sottrarre un valore fisso dal puntatore dello stack)
  • Inizia a eseguire il codice dall'indirizzo sulla scheda in alto e poi buttalo via. (es. sottrai 1 dal puntatore dello stack.)

Lo stack è ora tornato allo stato precedente alla chiamata della funzione.

Quando consideri questo, nota due cose: l'allocazione e la deallocazione dei locali è un'operazione estremamente veloce in quanto sta semplicemente aggiungendo un numero o sottraendo un numero dal puntatore dello stack. Nota anche come funziona naturalmente con la ricorsione.

Ciò è semplificato a fini esplicativi. In pratica, parametri e locali possono essere inseriti nei registri come ottimizzazione e il puntatore dello stack verrà generalmente incrementato e decrementato dalla dimensione della parola della macchina, non da una. (Per nominare un paio di cose.)


1

I moderni linguaggi di programmazione, come ben sapete, supportano il concetto di chiamate di subroutine (spesso chiamate "chiamate di funzione"). Ciò significa che:

  1. Nel mezzo di un codice, puoi chiamare qualche altra funzione nel tuo programma;
  2. Quella funzione non sa esplicitamente da dove è stata chiamata;
  3. Tuttavia, quando il suo lavoro è finito ed è return, il controllo ritorna nel punto esatto in cui è stata avviata la chiamata, con tutti i valori delle variabili locali in vigore come quando è stata avviata la chiamata.

In che modo il computer ne tiene traccia? Mantiene un registro continuo di quali funzioni sono in attesa di quali chiamate restituire. Questo record è una pila, e dal momento che è un uno così importante, normalmente chiamiamo la pila.

E poiché questo modello di chiamata / ritorno è così importante, le CPU sono state a lungo progettate per fornire un supporto hardware speciale. Il puntatore dello stack è una funzionalità hardware nelle CPU, un registro dedicato esclusivamente a tenere traccia della parte superiore dello stack e utilizzato dalle istruzioni della CPU per ramificarsi in una subroutine e tornare da essa.

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.