Quali sono le alternative all'utilizzo di uno stack per rappresentare la semantica della chiamata di funzione?


19

Sappiamo tutti e amiamo che le chiamate di funzione sono generalmente implementate usando lo stack; ci sono frame, indirizzi di ritorno, parametri, l'intero lotto.

Tuttavia, lo stack è un dettaglio di implementazione: le convenzioni di chiamata possono fare cose diverse (ad esempio x86 fastcall utilizza (alcuni) registri, MIPS e follower utilizzano finestre di registro e così via) e le ottimizzazioni possono fare anche altre cose (allineamento, omissione del puntatore di frame, ottimizzazione del tail-call ..).

Certo, la presenza di comode istruzioni di stack su molte macchine (macchine virtuali come JVM e CLR, ma anche macchine reali come x86 con il loro PUSH / POP ecc.) Rendono conveniente usarlo per le chiamate di funzione, ma in alcuni casi è possibile programmare in un modo in cui non è necessario uno stack di chiamate (sto pensando allo stile di passaggio di continuazione qui o agli attori in un sistema di passaggio di messaggi)

Quindi, stavo cominciando a chiedermi: è possibile implementare la semantica della chiamata di funzione senza uno stack, o meglio, usando una diversa struttura di dati (una coda, forse, o una mappa associativa?)
Naturalmente, capisco che uno stack è molto conveniente (c'è un motivo per cui è onnipresente) ma di recente mi sono imbattuto in un'implementazione che mi ha fatto meravigliare ..

Qualcuno di voi sa se è mai stato fatto in una lingua / macchina / macchina virtuale e, in tal caso, quali sono le differenze e le carenze che colpiscono?

EDIT: I miei sentimenti viscerali sono che diversi approcci di sub-computazione possono usare diverse strutture di dati. Ad esempio, il calcolo lambda non è basato sullo stack (l'idea di un'applicazione funzionale è catturata dalle riduzioni) ma stavo guardando un linguaggio / macchina / esempio reali. Ecco perché sto chiedendo ...


Clean utilizza un grafico e una macchina di riscrittura dei grafici, che a sua volta viene implementata utilizzando una macchina a tre stack, ma con contenuti diversi rispetto al solito.

Per le macchine virtuali, è possibile utilizzare un elenco collegato. Ogni nodo dell'elenco è un frame. Poiché lo stack hardware viene utilizzato dalla VM, ciò consente ai frame di esistere nell'heap senza il sovraccarico di realloc().
Shawnhcorey,

Risposte:


19

A seconda della lingua, potrebbe non essere necessario utilizzare uno stack di chiamate. Gli stack di chiamate sono necessari solo nelle lingue che consentono la ricorsione o la ricorsione reciproca. Se la lingua non consente la ricorsione, una sola invocazione di qualsiasi procedura può essere attiva in qualsiasi momento e le variabili locali per tale procedura possono essere allocate staticamente. Tali lingue devono prevedere modifiche al contesto, per la gestione degli interrupt, ma ciò non richiede ancora uno stack.

Fare riferimento a FORTRAN IV (e precedenti) e alle versioni precedenti di COBOL per esempi di lingue che non richiedono stack di chiamate.

Consultare Control Data 6600 (e precedenti macchine Control Data) per un esempio di supercomputer iniziale di grande successo che non ha fornito supporto hardware diretto per stack di chiamate. Fare riferimento al PDP-8 per un esempio di minicomputer iniziale di grande successo che non supportava gli stack di chiamate.

Per quanto ne so, le macchine stack Burroughs B5000 sono state le prime macchine con stack di chiamate hardware. Le macchine B5000 sono state progettate da zero per eseguire ALGOL, che ha richiesto la ricorsione. Avevano anche una delle prime architetture basate su descrittori, che ha gettato le basi per architetture di capacità.

Per quanto ne so, è stato il PDP-6 (che è diventato il DEC-10) a rendere popolare l'hardware dello stack di chiamate, quando la comunità di hacker del MIT ne ha preso in consegna uno e ha scoperto che l'operazione PUSHJ (Push Return Address and Jump) ha permesso di ridurre la routine di stampa decimale da 50 istruzioni a 10.

La funzione di base più semplice chiama la semantica in un linguaggio che consente la ricorsione richiede capacità che si abbinino perfettamente con uno stack. Se è tutto ciò di cui hai bisogno, uno stack di base è una buona e semplice corrispondenza. Se hai bisogno di qualcosa di più, la tua struttura dati deve fare di più.

Il miglior esempio di aver bisogno di più di quello che ho incontrato è la "continuazione", la capacità di sospendere un calcolo nel mezzo, salvarlo come una bolla di stato congelata e sparare di nuovo più tardi, probabilmente molte volte. Le continuazioni sono diventate popolari nel dialetto Scheme di LISP, come un modo per implementare, tra le altre cose, le uscite di errore. Le continuazioni richiedono la possibilità di creare un'istantanea dell'ambiente di esecuzione corrente e riprodurlo in un secondo momento, e uno stack è in qualche modo scomodo per questo.

La "Struttura e interpretazione dei programmi per computer" di Abelson & Sussman fornisce alcuni dettagli sulle continuazioni.


2
È stata una bellissima intuizione storica, grazie! Quando ho posto la mia domanda, avevo davvero in mente le continuazioni, in particolare lo stile di continuazione-passaggio (CPS). In tal caso, uno stack non è solo scomodo, ma probabilmente non è necessario: non è necessario ricordare dove tornare, si fornisce un punto in cui continuare l'esecuzione. Mi chiedevo se altri approcci senza stack fossero comuni, e tu ne hai dato alcuni molto buoni di cui non ero a conoscenza.
Lorenzo Dematté,

Leggermente correlato: hai indicato correttamente "se la lingua non consente la ricorsione". Che dire del linguaggio con ricorsione, in particolare delle funzioni che non sono ricorsive della coda? Hanno bisogno di uno stack "di progettazione"?
Lorenzo Dematté,

"Le pile di chiamate sono necessarie solo nelle lingue che consentono la ricorsione o la ricorsione reciproca" - no. Se una funzione può essere chiamata da più di un luogo (ad es. Entrambi fooe barpuò chiamare baz), allora la funzione deve sapere a cosa tornare. Se annidate queste informazioni "a chi tornare", finirete con uno stack. Non importa come lo chiami o se è supportato dall'hardware della CPU o qualcosa che emuli nel software (o anche se è un elenco collegato di voci allocate staticamente), è ancora uno stack.
Brendan,

@Brendan non necessariamente (almeno, questo è l'intero scopo della mia domanda). "Dove tornare" o meglio "dove andare dopo" deve essere uno stack, ovvero una struttura LIFO? Potrebbe essere un albero, una mappa, una coda o qualcos'altro?
Lorenzo Dematté,

Ad esempio, la mia sensazione è che CPS abbia solo bisogno di un albero, ma non ne sono sicuro, né so dove guardare. Ecco perché lo sto chiedendo ...
Lorenzo Dematté,

6

Non è possibile implementare la semantica della chiamata di funzione senza utilizzare una sorta di stack. È possibile giocare solo a giochi di parole (ad es. Usare un nome diverso, come "Buffo return buffer").

È possibile utilizzare qualcosa che non implementa la semantica di chiamata di funzione (ad es. Stile di passaggio di continuazione, attori) e quindi costruire sopra di essa una semantica di chiamata di funzione; ma ciò significa aggiungere una sorta di struttura di dati per tracciare dove viene passato il controllo quando la funzione ritorna, e quella struttura di dati sarebbe un tipo di stack (o uno stack con un nome / una descrizione diversi).

Immagina di avere molte funzioni che possono chiamarsi a vicenda. In fase di esecuzione, ogni funzione deve sapere dove tornare quando esce dalla funzione. Se firstchiama secondallora hai:

second returns to somewhere in first

Quindi, se hai delle secondchiamate third:

third returns to somewhere in second
second returns to somewhere in first

Quindi, se hai delle thirdchiamate fourth:

fourth returns to somewhere in third
third returns to somewhere in second
second returns to somewhere in first

Come viene chiamata ogni funzione, più informazioni "dove restituire" devono essere archiviate da qualche parte.

Se una funzione restituisce, vengono utilizzate le informazioni "dove restituire" e non sono più necessarie. Ad esempio, se fourthritorna da qualche parte, thirdla quantità di informazioni "dove tornare" diventerebbe:

third returns to somewhere in second
second returns to somewhere in first

Fondamentalmente; "semantica di chiamata di funzione" implica che:

  • è necessario disporre delle informazioni "dove restituire"
  • la quantità di informazioni aumenta quando vengono chiamate le funzioni e si riduce quando le funzioni ritornano
  • il primo pezzo di informazioni "dove restituire" memorizzato sarà l'ultimo pezzo di informazioni "dove restituire" scartato

Descrive un buffer FILO / LIFO o uno stack.

Se si tenta di utilizzare un tipo di albero, tutti i nodi dell'albero non avranno mai più di un figlio. Nota: un nodo con più figli può verificarsi solo se una funzione chiama 2 o più funzioni contemporaneamente , il che richiede una sorta di concorrenza (ad esempio thread, fork (), ecc.) E non sarebbe "semantica di chiamata di funzione". Se ogni nodo dell'albero non avrà mai più di un figlio; quindi quell'albero sarebbe usato solo come buffer FILO / LIFO o stack; e poiché è usato solo come buffer FILO / LIFO o stack è corretto affermare che "albero" è uno stack (e l'unica differenza sono i giochi di parole e / o i dettagli di implementazione).

Lo stesso vale per qualsiasi altra struttura di dati che potrebbe essere plausibilmente utilizzata per implementare la "semantica della chiamata di funzione" - sarà usata come una pila (e l'unica differenza sono i giochi di parole e / oi dettagli di implementazione); a meno che non interrompa la "semantica della funzione chiamata". Nota: fornirei esempi per altre strutture di dati se potessi, ma non riesco a pensare a nessuna altra struttura leggermente plausibile.

Naturalmente come viene implementato uno stack è un dettaglio di implementazione. Potrebbe essere un'area di memoria (in cui si tiene traccia di uno "stack top corrente"), potrebbe essere una sorta di elenco collegato (in cui si tiene traccia di "voce corrente nell'elenco"), oppure potrebbe essere implementato in alcuni altro modo. Inoltre, non importa se l'hardware ha il supporto integrato o meno.

Nota: se una sola invocazione di una procedura può essere attiva in qualsiasi momento; quindi è possibile allocare staticamente lo spazio per le informazioni "dove tornare". Questo è ancora uno stack (ad esempio un elenco collegato di voci allocate staticamente utilizzate in modo FILO / LIFO).

Si noti inoltre che ci sono alcune cose che non seguono la "semantica della chiamata di funzione". Queste cose includono "semantica potenzialmente molto diversa" (ad es. Continuazione che passa, modello dell'attore); e include anche estensioni comuni a "semantica di chiamate di funzioni" come concorrenza (thread, fibre, qualunque cosa), setjmp/ longjmp, gestione delle eccezioni, ecc.


Per definizione, uno stack è una raccolta LIFO: Last in, first out. Una coda è una raccolta FIFO.
John R. Strohm,

Quindi uno stack è l'unica struttura di dati accettabile? Se è così, perché?
Lorenzo Dematté,

@ JohnR.Strohm: risolto :-)
Brendan il

1
Per le lingue senza ricorsione (diretta o mutale), è possibile allocare staticamente per ogni metodo una variabile che identificherà il luogo da cui quel metodo è stato chiamato l'ultima volta. Se il linker è a conoscenza di queste cose, può allocare tali variabili in un modo che non sarà peggiore di quello che farebbe uno stack se ogni percorso di esecuzione staticamente possibile fosse effettivamente preso .
supercat,

4

Il linguaggio concatenativo del giocattolo XY utilizza una coda di chiamata e uno stack di dati per l'esecuzione.

Ogni fase di calcolo implica semplicemente la dequizione della parola successiva da eseguire e, nel caso dei builtin, alimentare la sua funzione interna dello stack di dati e della coda di chiamata come argomenti, o con userdefs, spingendo le parole che la compongono in primo piano.

Quindi, se abbiamo una funzione per raddoppiare l'elemento superiore:

; double dup + ;
// defines 'double' to be composed of 'dup' followed by '+'
// dup duplicates the top element of the data stack
// + pops the top two elements and push their sum

Quindi le funzioni di composizione +e duphanno le seguenti firme di tipo stack / coda:

// X is arbitraty stack, Y is arbitrary queue, ^ is concatenation
+      [X^a^b Y] -> [X^(a + b) Y]
dup    [X^a Y] -> [X^a^a Y]

E paradossalmente, doublesarà simile a questo:

double [X Y] -> [X dup^+^Y]

Quindi, in un certo senso, XY è impilabile.


Wow grazie! Lo esaminerò ... non sono sicuro che si applichi davvero alle chiamate di funzione, ma vale comunque la pena dare un'occhiata
Lorenzo Dematté,

1
@ Karl Damgaard Asmussen "spingendo le parole che la compongono in prima fila" "spingendo in avanti" Non è uno stack?

@ guesttttttt222222222 non proprio. Un callstack memorizza i puntatori di ritorno e quando la funzione ritorna, il callstack viene visualizzato. La coda di esecuzione memorizza solo i puntatori alle funzioni e quando esegue la funzione successiva, viene espansa nella sua definizione e spostata in primo piano. In XY la coda di esecuzione è in realtà un deque, poiché ci sono operazioni che funzionano anche sul retro della coda di esecuzione.
Karl Damgaard Asmussen,
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.