Nella seconda parte dell'articolo sui simulatori di sistemi informatici, continuerò a parlare in modo semplice dei simulatori informatici, in particolare della simulazione su piattaforma completa, con cui gli utenti comuni si imbattano più spesso, nonché del modello a tacchi e delle tracce, che sono più comuni nei circoli degli sviluppatori.

In Ho parlato di cosa siano i simulatori in generale, e dei livelli di modellazione. Ora, sulla base di queste conoscenze, propongo di immergerci un po' più a fondo e discutere della simulazione su piattaforma completa, di come costruire le tracce, cosa fare con esse e della microarchitettura a tacchi.
Il simulatore su piattaforma completa (full platform simulator), o 'Uno nel campo non è un guerriero'
Se è necessario esplorare il funzionamento di un singolo dispositivo specifico, come una scheda di rete, o scrivere un firmware o un driver per quel dispositivo, è possibile modellare separatamente tale dispositivo. Tuttavia, utilizzarlo scollegato dal resto dell'infrastruttura non è molto comodo. Per avviare il driver corrispondente sarà necessario un processore centrale, memoria, accesso al bus per la trasmissione dei dati e altro. Inoltre, per far funzionare il driver è necessaria un sistema operativo (OS) e uno stack di rete. Inoltre, potrebbe essere necessario un generatore di pacchetti separato e un server di ricezione delle risposte.
Il simulatore su piattaforma completa crea un ambiente per eseguire l'intero stack software, che include tutto, dal BIOS e dal bootloader fino al sistema operativo stesso e ai suoi vari sottosistemi, come lo stesso stack di rete, i driver e le applicazioni di livello utente. A tal fine, sono modelli software della maggior parte dei dispositivi del computer: processore e memoria, disco, dispositivi di input/output (tastiera, mouse, display), nonché la già citata scheda di rete.
Di seguito è riportato il diagramma a blocchi del chipset x58 di Intel. In un simulatore computerizzato a piena piattaforma basato su questo chipset, è necessaria l'implementazione della maggior parte dei dispositivi elencati, compresi quelli all'interno dell'IOH (Input/Output Hub) e dell'ICH (Input/Output Controller Hub), che non sono disegnati in dettaglio nel diagramma a blocchi. Tuttavia, come dimostra l'esperienza, ci sono molti dispositivi che non vengono utilizzati dal software che intendiamo eseguire. Non è necessario creare modelli di tali dispositivi.

Spesso i simulatori a piena piattaforma vengono implementati a livello di istruzioni del processore (ISA, vedi ). Questo consente di creare relativamente rapidamente e a basso costo il simulatore stesso. Il livello ISA è inoltre vantaggioso perché rimane più o meno costante, a differenza del livello API/ABI che cambia più frequentemente. Inoltre, un'implementazione a livello di istruzioni consente di eseguire il cosiddetto software binario non modificato, cioè eseguire codice già compilato senza alcuna modifica, proprio come viene utilizzato sull'hardware reale. In altre parole, è possibile creare una copia (
Le prestazioni dei simulatori

Come accennato poc'anzi, l'intero processo di simulazione dell'intero sistema, cioè di tutti i suoi dispositivi, è piuttosto lento. Se poi si implementa tutto a un livello davvero dettagliato, ad esempio a livello microarchitetturale o logico, l'esecuzione diventa estremamente lenta. Tuttavia, il livello di istruzioni rappresenta una scelta adeguata e consente a sistemi operativi e programmi di funzionare a velocità sufficienti per un'interazione confortevole da parte dell'utente.
Qui è pertinente affrontare il tema delle prestazioni dei simulatori. Solitamente vengono misurate in IPS (istruzioni al secondo), più precisamente in MIPS (milioni di IPS), cioè nel numero di istruzioni del processore eseguite dal simulatore in un secondo. Allo stesso tempo, la velocità di simulazione dipende anche dalle prestazioni del sistema su cui avviene la simulazione. Pertanto, forse è più corretto parlare di «riduzione della velocità» (slowdown) del simulatore rispetto al sistema originale.
I simulator full-platform più comuni disponibili sul mercato, come QEMU, VirtualBox o VmWare Workstation, offrono buone prestazioni. Per l'utente potrebbe anche non essere evidente che il lavoro avviene in un simulatore. Questo accade grazie alla speciale funzionalità di virtualizzazione implementata nei processori, agli algoritmi di traduzione binaria e ad altri aspetti interessanti. Questa è un'argomento per un articolo separato, ma in breve, la virtualizzazione è la capacità hardware dei moderni processori che consente ai simulatori di non simulare le istruzioni, ma di eseguirle direttamente su un processore reale, se ovviamente le architetture del simulatore e del processore sono simili. La traduzione binaria consiste nella conversione del codice di macchina guest in codice di macchina host e nella successiva esecuzione su un processore reale. Di conseguenza, la simulazione è solo leggermente più lenta, circa 5-10 volte, e spesso funziona alla stessa velocità di un sistema reale. Anche se molti fattori influiscono su questo. Ad esempio, se desideriamo simulare un sistema con diverse decine di processori, la velocità diminuirà immediatamente di tale fattore. D'altro canto, simulatori come Simics nelle ultime versioni supportano l'hardware host multiprocessore e parallelizzano efficacemente i core simulati sui core del processore reale.
Parlando della velocità della simulazione microarchitetturale, questa è generalmente più lenta di diversi ordini di grandezza, circa 1000-10000 volte, rispetto all'esecuzione su un computer normale senza simulazione. Le implementazioni a livello di elementi logici sono ancora più lente di diversi ordini di grandezza. Pertanto, come emulatore a questo livello, si utilizzano le FPGA, il che consente di aumentare notevolmente le prestazioni.
Il grafico sottostante mostra la dipendenza approssimativa della velocità di simulazione dalla complessità del modello.

Simulazione no. di cicli
Nonostante la bassa velocità di esecuzione, i simulatori microarchitettonici sono abbastanza diffusi. La modellazione dei blocchi interni del processore è necessaria per simulare con precisione il tempo di esecuzione di ogni istruzione. Qui potrebbe sorgere un malinteso: perché non programmare semplicemente il tempo di esecuzione per ogni istruzione? Ma un tale simulatore sarebbe molto impreciso, poiché il tempo di esecuzione della stessa istruzione può variare da chiamata a chiamata.
Un esempio semplice è l'istruzione di accesso alla memoria. Se la cella di memoria richiesta è disponibile nella cache, il tempo di esecuzione sarà minimo. Se l'informazione non è presente nella cache ("cache miss"), ciò aumenterà notevolmente il tempo di esecuzione dell'istruzione. Pertanto, è necessaria una modellazione della cache per una simulazione precisa. Tuttavia, non ci si limita solo al modello della cache. Il processore non aspetterà semplicemente di ricevere i dati dalla memoria se non sono presenti nella cache. Invece, inizierà a eseguire le istruzioni successive, selezionando quelle che non dipendono dal risultato della lettura dalla memoria. Questa è chiamata esecuzione "fuori ordine" (OOO, out of order execution), necessaria per minimizzare il tempo di inattività del processore. Tenere conto di tutto questo nel calcolo del tempo di esecuzione delle istruzioni richiede la modellazione dei blocchi corrispondenti del processore. Tra queste istruzioni, eseguite mentre si attende il risultato della lettura dalla memoria, può comparire un'operazione di salto condizionale. Se il risultato della condizione non è noto al momento, il processore non interrompe l'esecuzione, ma fa un "supposizione", esegue il salto corrispondente e continua a eseguire preventivamente le istruzioni dal punto del salto. Questo blocco, chiamato branch predictor, deve essere implementato anche nel simulatore microarchitettonico.
L'immagine sottostante mostra i principali blocchi del processore; non è necessario conoscerla, è fornita solo per illustrare la complessità dell'implementazione microarchitettonica.

Il funzionamento di tutti questi blocchi in un processore reale è sincronizzato attraverso segnali di clock speciali, e lo stesso avviene nel modello. Un simulatore di microarchitettura di questo tipo è chiamato simulatore a ciclo (cycle accurate). Il suo scopo principale è prevedere con precisione le prestazioni del processore in fase di sviluppo e/o calcolare il tempo di esecuzione di un programma specifico, ad esempio, di un benchmark. Se i valori sono inferiori a quelli necessari, sarà necessario rivedere gli algoritmi e i blocchi del processore o ottimizzare il programma.
Come mostrato sopra, la simulazione a ciclo è molto lenta, pertanto viene utilizzata solo per l'analisi di specifici aspetti del funzionamento del programma, dove è necessario conoscere la reale velocità di esecuzione e valutare le prestazioni future del dispositivo, il cui prototipo è in fase di simulazione.
Per simulare il restante tempo di esecuzione del programma viene utilizzato un simulatore funzionale. Come avviene realmente questo utilizzo combinato? Inizialmente si avvia il simulatore funzionale, sul quale viene caricato il sistema operativo e tutto il necessario per avviare il programma in analisi. Infatti, non ci interessa né il sistema operativo né le fasi iniziali di avvio del programma, la sua configurazione e altro. Tuttavia, non possiamo neppure saltare queste parti e passare direttamente all'esecuzione del programma a metà. Pertanto, tutte queste fasi preliminari vengono elaborate sul simulatore funzionale. Dopo che il programma è stato eseguito fino al momento di nostro interesse, ci sono due opzioni. Possiamo sostituire il modello con uno a ciclo e continuare l'esecuzione. La modalità di simulazione in cui viene utilizzato il codice eseguibile (ovvero i normali file di programma compilati) è chiamata simulazione in esecuzione (execution driven simulation). Questa è la forma di simulazione più comune. È possibile anche un altro approccio: la simulazione basata su tracce (trace driven simulation).
Simulazione basata su tracce
Consiste in due fasi. Utilizzando un simulatore funzionale o un sistema reale, le azioni del programma vengono registrate e scritte in un file di log. Questo log è chiamato traccia (trace). A seconda di ciò che viene analizzato, la traccia può includere istruzioni eseguibili, indirizzi di memoria, numeri di porta, informazioni sulle interruzioni.
Il passo successivo è la "riproduzione" della traccia, quando il simulatore a ciclo di clock legge la traccia ed esegue tutte le istruzioni in essa registrate. Alla fine otteniamo il tempo di esecuzione di questo pezzo di programma, così come varie caratteristiche di questo processo, come la percentuale di colpi nella cache.
Una caratteristica importante del lavoro con le tracce è la determinazione, il che significa che, eseguendo la simulazione nel modo descritto sopra, riproduciamo la stessa sequenza di azioni ogni volta. Questo ci consente, modificando i parametri del modello (dimensioni della cache, dei buffer e delle code) e utilizzando diversi algoritmi interni o configurandoli, di esplorare come un determinato parametro influisce sulle prestazioni del sistema e quale opzione offre i risultati migliori. Tutto questo può essere fatto con il modello prototipo del dispositivo prima della creazione del prototipo hardware reale.
La complessità di questo approccio risiede nella necessità di un'esecuzione preliminare dell'applicazione e nella raccolta della traccia, nonché nelle enormi dimensioni del file di traccia. Tra i vantaggi c'è il fatto che è sufficiente modellare solo la parte del dispositivo o della piattaforma di interesse, mentre la simulazione di esecuzione richiede normalmente il modello completo.
Quindi, in questo articolo abbiamo esaminato le caratteristiche della simulazione a piattaforma completa, abbiamo parlato della velocità delle implementazioni a diversi livelli, simulazione a ciclo di clock e tracce. Nel prossimo articolo descriverò i principali scenari di utilizzo dei simulatori, sia per scopi personali che dal punto di vista dello sviluppo nelle grandi aziende.
Fonte: habr.com
