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.