Qual è lo scopo di uno stack? Perchè ne abbiamo bisogno?


320

Quindi sto imparando MSIL in questo momento per imparare a eseguire il debug delle mie applicazioni C # .NET.

Mi sono sempre chiesto: qual è lo scopo della pila?

Solo per mettere la mia domanda nel contesto:
perché c'è un trasferimento dalla memoria allo stack o al "caricamento"? D'altra parte, perché c'è un trasferimento dallo stack alla memoria o "memorizzazione"? Perché non solo averli tutti messi nella memoria?

  • È perché è più veloce?
  • È perché è basato sulla RAM?
  • Per efficienza?

Sto cercando di capire questo per aiutarmi a capire i codici CIL molto più in profondità.


28
Lo stack è una parte della memoria, proprio come l'heap è un'altra parte della memoria.
CodesInChaos,

@CodeInChaos stai parlando di tipi di valore vs tipi di riferimento? o è lo stesso in termini di codici IL? ... So che lo stack è solo più veloce ed efficiente dell'heap (ma questo è nel mondo dei tipi valore / riferimento .. che non so se qui è lo stesso?)
Jan Carlo Viray,

15
@CodeInChaos - Penso che lo stack a cui fa riferimento Jan sia lo stack machine su cui è scritto IL, al contrario della regione di memoria che accetta i frame di stack durante le chiamate di funzione. Sono due diversi stack e, dopo JIT, lo stack IL non esiste (su x86, comunque)
Damien_The_Unbeliever

4
In che modo la conoscenza di MSIL ti aiuterà a eseguire il debug delle applicazioni .NET?
Piotr Perak,

1
Sulle macchine moderne, il comportamento nella cache del codice è un creatore di prestazioni. La memoria è ovunque. Lo stack è, di solito, proprio qui. Supponendo che lo stack sia una cosa reale e non solo un concetto utilizzato nell'esprimere l'operazione di un codice. Nell'implementazione di una piattaforma con MSIL, non è necessario che il concetto di stack arrivi all'hardware spingendo effettivamente i bit.
Ripristina Monica il

Risposte:


441

AGGIORNAMENTO: questa domanda mi è piaciuta così tanto che l'ho resa oggetto del mio blog il 18 novembre 2011 . Grazie per l'ottima domanda!

Mi sono sempre chiesto: qual è lo scopo della pila?

Suppongo che intendi lo stack di valutazione del linguaggio MSIL e non lo stack effettivo per thread in fase di esecuzione.

Perché c'è un trasferimento dalla memoria allo stack o al "caricamento"? D'altra parte, perché c'è un trasferimento dallo stack alla memoria o "memorizzazione"? Perché non solo averli tutti messi nella memoria?

MSIL è un linguaggio "macchina virtuale". I compilatori come il compilatore C # generano CIL , quindi in fase di esecuzione un altro compilatore chiamato compilatore JIT (Just In Time) trasforma l'IL in un codice macchina effettivo che può essere eseguito.

Quindi, prima rispondiamo alla domanda "perché MSIL non ha affatto?" Perché non basta che il compilatore C # scriva il codice macchina?

Perché è più economico farlo in questo modo. Supponiamo di non averlo fatto in quel modo; supponiamo che ogni lingua debba avere il proprio generatore di codice macchina. Hai venti lingue diverse: C #, JScript .NET , Visual Basic, IronPython , F # ... E supponi di avere dieci processori diversi. Quanti generatori di codice devi scrivere? 20 x 10 = 200 generatori di codice. È molto lavoro. Supponiamo ora di voler aggiungere un nuovo processore. Devi scrivere il generatore di codice per questo venti volte, uno per ogni lingua.

Inoltre, è un lavoro difficile e pericoloso. Scrivere generatori di codice efficienti per i chip di cui non sei esperto è un duro lavoro! I progettisti di compilatori sono esperti nell'analisi semantica della loro lingua, non nell'assegnazione efficiente dei registri di nuovi set di chip.

Ora supponiamo di farlo nel modo CIL. Quanti generatori CIL devi scrivere? Uno per lingua. Quanti compilatori JIT devi scrivere? Uno per processore. Totale: 20 + 10 = 30 generatori di codice. Inoltre, il generatore da linguaggio a CIL è facile da scrivere perché CIL è un linguaggio semplice e anche il generatore da codice CIL a macchina è facile da scrivere perché CIL è un linguaggio semplice. Ci liberiamo di tutte le complessità di C # e VB e quant'altro e "abbassa" tutto in un linguaggio semplice per cui è facile scrivere un jitter.

Avere una lingua intermedia riduce notevolmente i costi di produzione di un nuovo compilatore di lingue . Riduce inoltre drasticamente i costi di supporto di un nuovo chip. Vuoi supportare un nuovo chip, trovi alcuni esperti su quel chip e fai scrivere loro un jitter CIL e il gioco è fatto; quindi supporti tutte quelle lingue sul tuo chip.

OK, quindi abbiamo stabilito perché abbiamo MSIL; perché avere una lingua intermedia riduce i costi. Perché allora la lingua è un "stack machine"?

Perché le macchine stack sono concettualmente molto semplici da gestire per gli autori di compilatori di lingue. Le pile sono un meccanismo semplice e facilmente comprensibile per la descrizione dei calcoli. Le macchine stack sono anche concettualmente molto facili da gestire per gli autori di compilatori JIT. L'uso di uno stack è un'astrazione semplificante e, di nuovo, riduce i nostri costi .

Ti chiedi "perché avere uno stack?" Perché non fare tutto direttamente dalla memoria? Bene, pensiamo a quello. Supponiamo di voler generare il codice CIL per:

int x = A() + B() + C() + 10;

Supponiamo di avere la convenzione che "add", "call", "store" e così via tolgono sempre i loro argomenti dallo stack e mettono i loro risultati (se ce n'è uno) nello stack. Per generare il codice CIL per questo C # diciamo semplicemente qualcosa del tipo:

load the address of x // The stack now contains address of x
call A()              // The stack contains address of x and result of A()
call B()              // Address of x, result of A(), result of B()
add                   // Address of x, result of A() + B()
call C()              // Address of x, result of A() + B(), result of C()
add                   // Address of x, result of A() + B() + C()
load 10               // Address of x, result of A() + B() + C(), 10
add                   // Address of x, result of A() + B() + C() + 10
store in address      // The result is now stored in x, and the stack is empty.

Ora supponiamo di averlo fatto senza una pila. Faremo a modo tuo, dove ogni codice operativo prende gli indirizzi dei suoi operandi e l'indirizzo in cui memorizza il risultato :

Allocate temporary store T1 for result of A()
Call A() with the address of T1
Allocate temporary store T2 for result of B()
Call B() with the address of T2
Allocate temporary store T3 for the result of the first addition
Add contents of T1 to T2, then store the result into the address of T3
Allocate temporary store T4 for the result of C()
Call C() with the address of T4
Allocate temporary store T5 for result of the second addition
...

Vedi come va? Il nostro codice sta diventando enorme perché dobbiamo allocare esplicitamente tutto l'archiviazione temporanea che normalmente per convenzione andrebbe semplicemente nello stack . Peggio ancora, i nostri codici operativi stessi stanno diventando enormi perché ora tutti devono prendere come argomento l'indirizzo in cui scriveranno il loro risultato e l'indirizzo di ciascun operando. Un'istruzione "add" che sa che sta per togliere due cose dallo stack e mettere una cosa su può essere un singolo byte. Un'istruzione add che accetta due indirizzi di operando e un indirizzo di risultato sarà enorme.

Utilizziamo codici operativi basati su stack perché gli stack risolvono il problema comune . Vale a dire: voglio allocare un po 'di spazio di archiviazione temporaneo, usarlo molto presto e poi liberarmene rapidamente quando ho finito . Partendo dal presupposto che abbiamo uno stack a nostra disposizione, possiamo rendere gli opcode molto piccoli e il codice molto conciso.

AGGIORNAMENTO: Alcuni pensieri aggiuntivi

Per inciso, questa idea di ridurre drasticamente i costi (1) specificando una macchina virtuale, (2) scrivendo compilatori che indirizzano il linguaggio VM e (3) scrivendo implementazioni della VM su una varietà di hardware, non è affatto una nuova idea . Non ha avuto origine con MSIL, LLVM, bytecode Java o altre infrastrutture moderne. La prima implementazione di questa strategia di cui sono a conoscenza è la macchina pcode del 1966.

La prima volta che ho sentito parlare personalmente di questo concetto è stato quando ho appreso come gli implementatori Infocom sono riusciti a far funzionare Zork su così tante macchine diverse così bene. Hanno specificato una macchina virtuale chiamata Z-machine e quindi hanno creato emulatori Z-machine per tutto l'hardware su cui volevano far funzionare i loro giochi. Ciò ha aggiunto l'enorme vantaggio di poter implementare la gestione della memoria virtuale su sistemi primitivi a 8 bit; un gioco potrebbe essere più grande di quello che si adatterà alla memoria perché potrebbero semplicemente inserire il codice dal disco quando ne avevano bisogno e scartarlo quando dovevano caricare nuovo codice.


63
WOW. Questo è ESATTAMENTE quello che stavo cercando. Il modo migliore per ottenere una risposta è ottenerne uno dallo sviluppatore principale stesso. Grazie per il tempo, e sono sicuro che questo aiuterà tutti coloro che si chiedono le complessità del compilatore e di MSIL. Grazie Eric.
Jan Carlo Viray,

18
Questa è stata un'ottima risposta. Mi ricorda perché ho letto il tuo blog anche se sono un ragazzo Java. ;-)
giovedì

34
@JanCarloViray: sei il benvenuto! Noto che sono uno sviluppatore principale, non lo sviluppatore principale. Ci sono diverse persone in questa squadra con quel titolo di lavoro e non sono nemmeno il più anziano di loro.
Eric Lippert,

17
@Eric: Se / quando smetti di amare la programmazione, dovresti prendere in considerazione l'idea di insegnare ai programmatori. Oltre al divertimento, potresti fare un omicidio senza le pressioni degli affari. L'atmosfera fantastica è ciò che hai in quella zona (e meravigliosa pazienza, potrei aggiungere). Lo dico come ex docente universitario.
Alan,

19
Circa 4 paragrafi in cui stavo dicendo a me stesso "Sembra Eric", il 5 o il 6 mi ero laureato in "Sì, sicuramente Eric" :) Un'altra risposta davvero completa ed epica.
Binary Worrier,

86

Tieni presente che quando stai parlando di MSIL, stai parlando di istruzioni per una macchina virtuale . La macchina virtuale utilizzata in .NET è una macchina virtuale basata su stack. A differenza di una VM basata su registro, la VM Dalvik utilizzata nei sistemi operativi Android ne è un esempio.

Lo stack nella VM è virtuale, spetta all'interprete o al compilatore just-in-time tradurre le istruzioni della VM in codice effettivo che viene eseguito sul processore. Che nel caso di .NET è quasi sempre un jitter, il set di istruzioni MSIL è stato progettato per essere jitter fin dall'inizio. A differenza del bytecode Java, ad esempio, ha istruzioni distinte per le operazioni su tipi di dati specifici. Il che lo rende ottimizzato per essere interpretato. In realtà esiste un interprete MSIL, che viene utilizzato in .NET Micro Framework. Che gira su processori con risorse molto limitate, non può permettersi la RAM necessaria per archiviare il codice macchina.

Il modello di codice macchina effettivo è misto, con sia uno stack che registri. Uno dei grandi lavori dell'ottimizzatore di codice JIT è trovare modi per archiviare le variabili che sono conservate nello stack nei registri, migliorando così notevolmente la velocità di esecuzione. Un jitter Dalvik ha il problema opposto.

Lo stack di macchine è altrimenti una struttura di archiviazione di base che esiste da molto tempo nei progetti di processori. Ha un'ottima località di riferimento, una caratteristica molto importante per le moderne CPU che mastica i dati molto più velocemente di quanto la RAM possa fornirli e supporta la ricorsione. La progettazione del linguaggio è fortemente influenzata dall'avere uno stack, visibile a supporto delle variabili locali e l'ambito limitato al corpo del metodo. Un problema significativo con lo stack è quello per cui questo sito è chiamato.


2
+1 per una spiegazione molto dettagliata e +100 (se potessi) per un ulteriore confronto DETTAGLIATO con altri sistemi e linguaggio :)
Jan Carlo Viray,

4
Perché Dalvik è una macchina Register? Sicne si rivolge principalmente ai processori ARM. Ora, x86 ha la stessa quantità di registri ma essendo un CISC, solo 4 di questi sono realmente utilizzabili per la memorizzazione dei locali perché il resto è implicitamente usato nelle istruzioni comuni. Le architetture ARM, d'altra parte, hanno molti più registri che possono essere usati per archiviare i locali, quindi facilitano un modello di esecuzione basato sui registri.
Johannes Rudolph,

1
@JohannesRudolph Non è più vero da quasi due decenni. Solo perché la maggior parte dei compilatori C ++ ha ancora come target il set di istruzioni x86 degli anni '90 non significa che x86 stesso sia defficiente. Haswell ha 168 registri interi per uso generico e 168 registri AVX GP, ad esempio - molto più di qualsiasi CPU ARM che conosca. È possibile utilizzare tutti quelli dell'assembly x86 (moderno) nel modo desiderato. Colpa degli autori del compilatore, non dell'architettura / CPU. In effetti, è uno dei motivi per cui la compilazione intermedia è così attraente: un codice binario, il migliore per una determinata CPU; niente confusione con l'architettura degli anni '90.
Luaan,

2
@JohannesRudolph Il compilatore .NET JIT attualmente utilizza registri piuttosto pesantemente; lo stack è principalmente un'astrazione della macchina virtuale IL, il codice che viene effettivamente eseguito sulla CPU è molto diverso. Le chiamate al metodo possono essere pass-by register, i locali possono essere portati ai registri ... Il vantaggio principale dello stack nel codice macchina è l'isolamento che dà alle chiamate di subroutine: se si inserisce un locale in un registro, una chiamata di funzione può effettuare perdi quel valore e non puoi davvero dirlo.
Luaan,

1
@RahulAgarwal Il codice macchina generato può o meno utilizzare lo stack per un dato valore locale o intermedio. In IL, ogni argomento e locale è nello stack - ma nel codice macchina, questo non è vero (è permesso, ma non richiesto). Alcune cose sono utili in pila e vengono messe in pila. Alcune cose sono utili nell'heap e sono messe nell'heap. Alcune cose non sono affatto necessarie o richiedono solo alcuni momenti in un registro. Le chiamate possono essere eliminate del tutto (in linea), oppure i loro argomenti possono essere passati nei registri. La squadra ha molta libertà.
Luaan,

20

C'è un articolo di Wikipedia molto interessante / dettagliato su questo, Vantaggi dei set di istruzioni della macchina stack . Avrei bisogno di citarlo interamente, quindi è più semplice semplicemente mettere un link. Citerò semplicemente i sottotitoli

  • Codice oggetto molto compatto
  • Compilatori semplici / interpreti semplici
  • Stato minimo del processore

-1 @xanatos Potresti provare a sintetizzare i titoli che hai preso?
Tim Lloyd,

@chibacity Se avessi voluto sintetizzarli, avrei risposto. Stavo cercando di recuperare un ottimo collegamento.
xanatos,

@xanatos Capisco i tuoi obiettivi, ma condividere un link a un articolo così grande su Wikipedia non è un'ottima risposta. Non è difficile da trovare solo googling. D'altra parte, Hans ha una bella risposta.
Tim Lloyd,

@chibacity L'OP era probabilmente pigro nel non cercare prima. Il rispondente qui ha fornito un buon collegamento (senza descriverlo). Due mali fanno un bene :-) E voterò Hans.
xanatos,

a responder e @xanatos +1 per un ottimo collegamento. Stavo aspettando che qualcuno riassumesse completamente e avessi una risposta del knowledge pack .. se Hans non avesse dato una risposta, avrei creato la tua come risposta accettata ... è solo un link, quindi non lo era giusto per Hans che ha fatto un grande sforzo per la sua risposta .. :)
Jan Carlo Viray,

8

Per aggiungere un po 'di più alla domanda dello stack. Il concetto di stack deriva dalla progettazione della CPU in cui il codice macchina nell'unità logica aritmetica (ALU) opera su operandi che si trovano nello stack. Ad esempio, un'operazione di moltiplicazione può prendere i due operandi principali dallo stack, moltiplicarli e riposizionare il risultato nello stack. Il linguaggio macchina in genere ha due funzioni di base per aggiungere e rimuovere gli operandi dallo stack; PUSH e POP. In molti dsp della CPU (processore di segnale digitale) e controller di macchine (come quello che controlla una lavatrice) lo stack si trova sul chip stesso. Ciò consente un accesso più rapido all'ALU e consolida le funzionalità richieste in un singolo chip.


5

Se il concetto di stack / heap non viene seguito e i dati vengono caricati in una posizione di memoria casuale O i dati vengono archiviati da posizioni di memoria casuali ... saranno molto non strutturati e non gestiti.

Questi concetti vengono utilizzati per archiviare i dati in una struttura predefinita per migliorare le prestazioni, l'utilizzo della memoria ... e quindi chiamati strutture di dati.



0

Ho cercato "interruzione" e nessuno l'ha incluso come vantaggio. Per ogni dispositivo che interrompe un microcontrollore o un altro processore, di solito ci sono registri che vengono inseriti in uno stack, viene chiamata una routine di servizio di interruzione e, una volta terminato, i registri vengono rimossi dallo stack e rimessi dove sono erano. Quindi il puntatore dell'istruzione viene ripristinato e l'attività normale riprende da dove era stata interrotta, quasi come se l'interruzione non fosse mai avvenuta. Con lo stack, puoi effettivamente avere diversi dispositivi (teoricamente) che si interrompono a vicenda e tutto funziona, a causa dello stack.

Esiste anche una famiglia di linguaggi basati su stack chiamati linguaggi concatenativi . Sono tutti (credo) linguaggi funzionali, perché lo stack è un parametro implicito passato, e anche lo stack modificato è un ritorno implicito da ciascuna funzione. Sia Forth che Factor (che è eccellente) sono esempi, insieme ad altri. Factor è stato usato in modo simile a Lua, per i giochi di script, ed è stato scritto da Slava Pestov, un genio che attualmente lavora presso Apple. Il suo Google TechTalk su YouTube che ho visto alcune volte. Parla dei costruttori di Boa, ma non sono sicuro di cosa significhi ;-).

Penso davvero che alcune delle attuali VM, come la JVM, la Microsoft CIL e persino quella che ho visto sia stata scritta per Lua, dovrebbero essere scritte in alcuni di questi linguaggi basati su stack, per renderli portabili su ancora più piattaforme. Penso che questi linguaggi concatenativi manchino in qualche modo le loro chiamate come kit di creazione di VM e piattaforme di portabilità. Esiste anche pForth, un Forth "portatile" scritto in ANSI C, che potrebbe essere utilizzato per una portabilità ancora più universale. Qualcuno ha provato a compilarlo usando Emscripten o WebAssembly?

Con i linguaggi basati su stack, esiste uno stile di codice chiamato punto zero, perché puoi semplicemente elencare le funzioni da chiamare senza passare alcun parametro (a volte). Se le funzioni si incastrassero perfettamente, non avresti altro che un elenco di tutte le funzioni del punto zero, e quella sarebbe la tua applicazione (teoricamente). Se approfondisci Forth o Factor, vedrai di cosa sto parlando.

A Easy Forth , un simpatico tutorial online scritto in JavaScript, ecco un piccolo esempio (nota "sq sq sq sq sq" come esempio di stile di chiamata a punto zero):

: sq dup * ;  ok
2 sq . 4  ok
: ^4 sq sq ;  ok
2 ^4 . 16  ok
: ^8 sq sq sq sq ;  ok
2 ^8 . 65536  ok

Inoltre, se guardi alla fonte della pagina web Easy Forth, vedrai in fondo che è molto modulare, scritto in circa 8 file JavaScript.

Ho speso un sacco di soldi per quasi tutti i libri di Forth su cui avrei potuto mettere le mani nel tentativo di assimilare Forth, ma ora sto solo iniziando a farlo meglio. Voglio dare un colpo di testa a coloro che vengono dopo, se vuoi davvero ottenerlo (l'ho scoperto troppo tardi), prendi il libro su FigForth e implementalo. I Forth commerciali sono fin troppo complicati e la cosa più grande di Forth è che è possibile comprendere l'intero sistema, dall'alto verso il basso. In qualche modo, Forth implementa un intero ambiente di sviluppo su un nuovo processore, e nonostante la necessitàpoiché quello è sembrato passare con C su tutto, è ancora utile come rito di passaggio scrivere un Forth da zero. Quindi, se scegli di farlo, prova il libro FigForth: sono diversi Forth implementati contemporaneamente su una varietà di processori. Una specie di Rosetta Stone of Forths.

Perché abbiamo bisogno di uno stack: efficienza, ottimizzazione, punto zero, salvataggio dei registri in caso di interruzione e per gli algoritmi ricorsivi è "la forma giusta".

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.