Dato un sistema informatico specifico, è possibile stimare il tempo di esecuzione preciso effettivo di un pezzo di codice Assembly


23

questo è un pezzo di codice assembly

section .text
    global _start       ;must be declared for using gcc
_start:                     ;tell linker entry point
    mov edx, len    ;message length
    mov ecx, msg    ;message to write
    mov ebx, 1      ;file descriptor (stdout)
    mov eax, 4      ;system call number (sys_write)
    int 0x80        ;call kernel
    mov eax, 1      ;system call number (sys_exit)
    int 0x80        ;call kernel

section .data

msg db  'Hello, world!',0xa ;our dear string
len equ $ - msg         ;length of our dear string

Dato un sistema informatico specifico, è possibile prevedere con precisione il tempo di esecuzione effettivo di un pezzo di codice Assembly.


30
"Esegui il codice su quel computer e usa un cronometro" è una risposta valida?
Draconis,

4
Sospetto che la maggior parte del tempo impiegato nell'esecuzione di questo pezzo di codice sia in attesa di I / O. Il tempo necessario per eseguire le singole istruzioni è in qualche modo prevedibile se si conosceva la posizione della memoria del codice e tutti i dettagli sul processore (che sono estremamente complessi al giorno d'oggi), ma la velocità è influenzata anche dalla memoria e dal disco, quindi " dovrei conoscere anche una grande quantità di dettagli su di loro. Quindi, a meno che non si considerino fenomeni fisici (che influenzano anche il tempo), si potrebbe dire che è prevedibile, ma inimmaginabilmente difficile farlo.
IllidanS4 vuole che Monica torni il

4
è sempre possibile stimare ...
sudo rm -rf slash

3
Non è impossibile anche questo a causa del problema di arresto? Possiamo provare per qualche codice se si fermerà, ma non possiamo avere un algoritmo che lo determini per tutti i possibili codici.
Kutschkem,

2
@Falco Sarebbe una proprietà del sistema dato. Alcune implementazioni C indipendenti non hanno sistema operativo; tutto ciò che è in esecuzione è un loop principale (o nemmeno un loop ;-)) che può o meno leggere dagli indirizzi hardware per l'input.
Peter - Ripristina Monica il

Risposte:


47

Posso solo citare dal manuale di una CPU piuttosto primitiva, un processore 68020 del 1986: "Calcolare il tempo di esecuzione esatto di una sequenza di istruzioni è difficile, anche se si ha una conoscenza precisa dell'implementazione del processore". Che non abbiamo. E rispetto a un processore moderno, quella CPU era primitiva .

Non riesco a prevedere il tempo di esecuzione di quel codice, e nemmeno tu. Ma non puoi nemmeno definire cosa sia il "runtime" di un pezzo di codice, quando un processore ha enormi cache e enormi capacità fuori servizio. Un tipico processore moderno può avere 200 istruzioni "in volo", ovvero in varie fasi di esecuzione. Quindi il tempo che intercorre tra il tentativo di leggere il primo byte di istruzione e il ritiro dell'ultima istruzione può essere piuttosto lungo. Ma il ritardo effettivo rispetto a tutti gli altri lavori che il processore deve svolgere potrebbe essere (e in genere lo è) molto meno.

Naturalmente fare due chiamate al sistema operativo lo rende completamente imprevedibile. Non sai cosa faccia realmente la "scrittura su stdout", quindi non puoi prevedere l'ora.

E non puoi conoscere la velocità di clock del computer nel momento preciso in cui esegui il codice. Potrebbe essere in una modalità di risparmio energetico, il computer potrebbe avere una velocità di clock ridotta a causa del surriscaldamento, quindi anche lo stesso numero di cicli di clock può richiedere tempi diversi.

Tutto sommato: totalmente imprevedibile.


12
Penso che le tue conclusioni siano troppo forti. Latenza e throughput sono parametri comuni per misurare il "runtime" di un programma. Inoltre, puoi semplicemente scegliere una definizione adatta di "runtime". Inoltre, se si dispone di un'istantanea completa dello stato del sistema, hw e sw e una perfetta conoscenza degli interni della CPU, è possibile prevedere il runtime. In Intel possono probabilmente stimare il tempo di esecuzione, anche qui su SO possiamo prevedere latenze e tput con accuratezza del ciclo. In questo caso, oltre ai syscalls, non è nemmeno così difficile.
Margaret Bloom,

10
@MargaretBloom nemmeno allora. Posiziono il telefono troppo vicino al forno, la CPU si sblocca per gestire la temperatura, la stima del tempo di esecuzione è improvvisamente troppo bassa. E anche se conti in cicli e non esegui syscall, altri thread e CPU potrebbero giocare bene con il contenuto della RAM, oppure potrebbero scaricare la tua memoria sul disco rigido mentre sei scambiato, in base a circostanze imprevedibili, che vanno dalla potenza rallenta il disco rigido quanto basta affinché un thread in competizione ottenga abbastanza memoria in tempo per distruggere il tuo, fino ai thread che tirano su tirando i dadi per vedere quanto tempo perdere.
John Dvorak,

6
Oltre a ciò, "la completa conoscenza dello stato del sistema, hw e sw" è un ordine piuttosto elevato, pensa. Aggiungi "10 ms in anticipo" e stai già chiedendo l'impossibile. E se l'implementazione della tua CPU di generazione di numeri casuali hardware utilizza fenomeni quantistici (probabilmente lo fa), e alcuni thread sulla CPU lo chiamano, allora nemmeno conoscere lo stato completo dell'universo 3000 km attorno al computer ti salverà. E in MWI, non puoi nemmeno indovinare.
John Dvorak,

8
@Nat: Anche nella crittografia, "tempo costante" non significa in realtà assolutamente costante - significa solo che il tempo di esecuzione non ha variazioni sistematiche che dipendono da dati segreti e che potrebbero essere statisticamente correlate con esso. E in pratica, si presume spesso che se il percorso del codice adottato e il modello degli accessi alla memoria eseguiti non dipendono da dati segreti, e se si evitano istruzioni specifiche note per richiedere un tempo variabile (o i loro input mascherati speriamo di eliminare la correlazione), probabilmente è abbastanza buono. Oltre a ciò, devi davvero misurarlo.
Ilmari Karonen,

2
Un 68020 è una bestia complessa ... prova un MCS51 ....
rackandboneman

30

Non puoi farlo in generale, ma in un certo senso puoi farlo davvero, e ci sono stati alcuni casi storici in cui davvero dovevi farlo.

L' Atari 2600 (o Atari Video Computer System) era uno dei primi sistemi di videogiochi domestici ed è stato rilasciato per la prima volta nel 1978. A differenza dei sistemi successivi dell'epoca, Atari non poteva permettersi di fornire al dispositivo un frame buffer, nel senso che la CPU aveva per eseguire il codice su ogni scanline per determinare cosa produrre - se questo codice impiegasse 17.08 microsecondi (l'intervallo HBlank), la grafica non verrebbe impostata correttamente prima che la scanline iniziasse a disegnarli. Peggio ancora, se il programmatore voleva disegnare contenuti più complessi di quanto normalmente consentito dall'Atari, doveva misurare i tempi esatti per le istruzioni e cambiare i registri grafici mentre veniva disegnato il raggio, con un arco di 57.29 microsecondi per l'intera linea di scansione.

Tuttavia, l'Atari 2600, come molti altri sistemi basati sul 6502, aveva una caratteristica molto importante che consentiva la gestione attenta del tempo richiesta per questo scenario: la CPU, la RAM e il segnale TV funzionavano tutti con un clock basato sullo stesso master orologio. Il segnale TV scorreva su un orologio da 3,98 MHz, suddividendo i tempi sopra in un numero intero di "orologi a colori" che gestivano il segnale TV, e un ciclo di orologi CPU e RAM era esattamente tre orologi a colori, consentendo al clock della CPU di essere una misura precisa del tempo relativa all'attuale segnale TV di avanzamento. (Per ulteriori informazioni al riguardo, consulta la Guida del programmatore Stella , scritta per l' emulatore Stella Atari 2600 ).

Questo ambiente operativo, inoltre, significava che ogni istruzione CPU aveva una quantità definita di cicli che avrebbe richiesto in ogni caso, e molti 6502 sviluppatori hanno pubblicato queste informazioni nelle tabelle di riferimento. Ad esempio, considera questa voce per l' CMPistruzione (Confronta memoria con accumulatore), presa da questa tabella :

CMP  Compare Memory with Accumulator

     A - M                            N Z C I D V
                                    + + + - - -

     addressing    assembler    opc  bytes  cycles
     --------------------------------------------
     immediate     CMP #oper     C9    2     2
     zeropage      CMP oper      C5    2     3
     zeropage,X    CMP oper,X    D5    2     4
     absolute      CMP oper      CD    3     4
     absolute,X    CMP oper,X    DD    3     4*
     absolute,Y    CMP oper,Y    D9    3     4*
     (indirect,X)  CMP (oper,X)  C1    2     6
     (indirect),Y  CMP (oper),Y  D1    2     5*

*  add 1 to cycles if page boundary is crossed

Utilizzando tutte queste informazioni, Atari 2600 (e altri 6502 sviluppatori) sono stati in grado di determinare con esattezza il tempo impiegato per l'esecuzione del loro codice e di costruire routine in grado di soddisfare le esigenze di temporizzazione del segnale TV di Atari. E poiché questo tempismo era così esatto (specialmente per istruzioni che fanno perdere tempo come NOP), sono stati persino in grado di usarlo per modificare la grafica mentre venivano disegnati.


Naturalmente, l'Atari 6502 è un caso molto specifico, e tutto ciò è possibile solo perché il sistema aveva tutto quanto segue:

  • Un master clock che eseguiva tutto, compresa la RAM. I sistemi moderni hanno clock indipendenti per CPU e RAM, con il clock della RAM spesso più lento e i due non necessariamente sincronizzati.
  • Nessuna memorizzazione nella cache di alcun tipo: il 6502 accedeva sempre direttamente alla DRAM. I sistemi moderni hanno cache SRAM che rendono più difficile prevedere lo stato - mentre è forse ancora possibile prevedere il comportamento di un sistema con una cache, è sicuramente più difficile.
  • Nessun altro programma in esecuzione contemporaneamente: il programma sulla cartuccia aveva il controllo completo del sistema. I sistemi moderni eseguono più programmi contemporaneamente utilizzando algoritmi di pianificazione non deterministici.
  • Una velocità di clock abbastanza bassa da consentire ai segnali di viaggiare nel tempo nel sistema. Su un sistema moderno con velocità di clock di 4 GHz (ad esempio), sono necessari un fotone di luce 6,67 cicli di clock per percorrere la lunghezza di una scheda madre di mezzo metro: non ci si può aspettare che un processore moderno interagisca con qualcos'altro sulla scheda in un solo ciclo, poiché è necessario più di un ciclo affinché un segnale sulla scheda raggiunga anche il dispositivo.
  • Una velocità di clock ben definita che cambia raramente (1,19 MHz nel caso dell'Atari): le velocità della CPU dei sistemi moderni cambiano continuamente, mentre un Atari non può farlo senza influenzare anche il segnale TV.
  • Tempi di ciclo pubblicati: x86 non definisce quanto tempo impiegano le sue istruzioni.

Tutte queste cose si sono unite per creare un sistema in cui era possibile creare serie di istruzioni che richiedevano un esatto ammontare di tempo - e per questa applicazione, questo è esattamente ciò che è stato richiesto. La maggior parte dei sistemi non ha questo grado di precisione semplicemente perché non ce n'è bisogno: i calcoli vengono eseguiti quando vengono eseguiti o se è necessario un tempo esatto, è possibile eseguire una query su un orologio indipendente. Ma se la necessità è giusta (come su alcuni sistemi integrati), può ancora apparire e sarai in grado di determinare con precisione il tempo necessario per l'esecuzione del codice in questi ambienti.


E dovrei anche aggiungere l'enorme dichiarazione di non responsabilità che tutto ciò si applica solo alla costruzione di una serie di istruzioni di assemblaggio che richiederanno un esatto tempo. Se quello che vuoi fare è prendere un pezzo arbitrario di assemblaggio, anche in questi ambienti, e chiedere "Quanto tempo richiede l'esecuzione?", Non puoi categoricamente farlo - questo è il problema di Halting , che è stato dimostrato irrisolvibile.


EDIT 1: In una versione precedente di questa risposta, ho affermato che l'Atari 2600 non aveva modo di informare il processore di dove si trovasse nel segnale TV, il che lo costringeva a mantenere l'intero programma contato e sincronizzato sin dall'inizio. Come ho sottolineato nei commenti, questo è vero per alcuni sistemi come lo ZX Spectrum, ma non è vero per l'Atari 2600, poiché contiene un registro hardware che arresta la CPU fino a quando si verifica il successivo intervallo di blanking orizzontale, nonché una funzione per iniziare a piacere l'intervallo di soppressione verticale. Pertanto, il problema dei cicli di conteggio è limitato a ciascuna linea di scansione e diventa esatto solo se lo sviluppatore desidera cambiare contenuto mentre viene disegnata la linea di scansione.


4
Va anche notato che la maggior parte dei giochi non ha funzionato perfettamente: è possibile vedere molti artefatti nell'output video a causa della temporizzazione non corrispondente del segnale video, o a causa dell'errore del programmatore (stima errata della tempistica della CPU) o semplicemente avendo troppo lavoro da fare. Era anche molto fragile: se avessi bisogno di correggere un bug o aggiungere nuove funzionalità, molto probabilmente avresti rotto i tempi, a volte inevitabilmente. È stato divertente, ma anche un incubo :) Non sono nemmeno sicuro che la velocità dell'orologio sia sempre stata corretta, ad esempio in caso di surriscaldamento, interferenze, ecc. Ma dimostra sicuramente che è stato difficile anche allora.
Luaan,

1
Buona risposta, anche se vorrei precisare che non è necessario contare il numero di cicli per ciascuna istruzione sull'Atari 2600. Ha due funzioni per aiutarti a non farlo: un conto alla rovescia che si inizializza e quindi esegui il polling per vedere se ha raggiunto 0 e un registro che arresta la CPU fino all'inizio del successivo blanking orizzontale. Molti altri dispositivi, come lo ZX Spectrum, non hanno nulla del genere e in realtà devi contare ogni singolo ciclo trascorso dopo l'interruzione del blanking verticale per sapere dove ti trovi sullo schermo.
Martin Vilcans,

1
Direi che il problema di Halting non si applica strettamente all'Atari. Se si escludono le funzionalità I / O di Atari e si limita a una tipica ROM a cartuccia, allora c'è una quantità finita di memoria. A quel punto hai una macchina a stati finiti, quindi qualsiasi programma su di essa deve fermarsi o entrare in uno stato in cui è entrato prima, portando a un ciclo infinito dimostrabile in tempo finito.
user1937198

2
@ user1937198 128 byte di stato (più qualunque cosa sia nei registri) è PIÙ quindi sufficiente spazio di stato per fare la differenza tra quello e il nastro infinito teorico della macchina di Turing una distinzione che conta solo in teoria. Inferno, non possiamo praticamente cercare i 128 BIT di qualcosa come una chiave AES .... Lo spazio degli stati cresce rapidamente quando si aggiungono i bit. Non dimenticare che l'equivalente di "Disabilita gli interrupt; fermarsi 'sarebbe stato quasi certamente possibile.
Dan Mills,

1
"Questo è il problema di Halting, che è stato dimostrato irrisolvibile. Se ci si imbatte in questo, allora è necessario rompere il cronometro ed effettivamente eseguire il codice." - questo non ha senso. Non puoi eludere la prova di Turing "eseguendo" effettivamente il codice invece di simularlo. Se si ferma, puoi stabilire quanto tempo ci vuole per fermare. Se non si ferma, non puoi mai essere sicuro (in generale) se si fermerà in futuro o se correrà per sempre. È lo stesso problema con un cronometro reale o simulato. Almeno in una simulazione è possibile ispezionare più facilmente lo stato interno per rilevare eventuali segni di loop.
ben

15

Ci sono due aspetti in gioco qui

Come sottolinea @ gnasher729, se conosciamo le istruzioni esatte da eseguire, è ancora difficile stimare il tempo di esecuzione esatto a causa di cose come la cache, la previsione dei rami, il ridimensionamento, ecc.

Tuttavia, la situazione è ancora peggiore. Dato un pezzo di assemblaggio, è impossibile sapere quali istruzioni verranno eseguite o anche quante istruzioni verranno eseguite. Ciò è dovuto al teorema di Rice: se potessimo determinarlo con precisione, allora potremmo usare quell'informazione per risolvere il problema Halting, che è impossibile.

Il codice assembly può contenere salti e rami, sufficienti per rendere la traccia completa di un programma possibilmente infinita. C'è stato un lavoro su approssimazioni conservative dei tempi di esecuzione, che dà limiti superiori all'esecuzione, attraverso cose come la semantica dei costi o sistemi di tipo annotato. Non ho familiarità con nulla di specifico per il montaggio, ma non sarei sorpreso se esistesse qualcosa del genere.


4
Voglio dire, il problema di Halting si applica direttamente qui, poiché se sapessimo il tempo di esecuzione sapremmo se si ferma. Anche il fatto che non ci siano condizionali non aiuta nemmeno qui, poiché in x86, movè Turing-Complete
BlueRaja - Danny Pflughoeft

7
Rice e the Halting Problem sono dichiarazioni su qualsiasi (qualunque) programma arbitrario, ma l'OP qui ha specificato una specifica parte di codice nella domanda. È possibile determinare le proprietà semantiche e di arresto su singole o limitate categorie di programmi, giusto? È solo che non esiste una procedura generale che copra tutti i programmi.
Daniel R. Collins,

2
Possiamo definitivamente sapere quale istruzione verrà eseguita dopo, ciò che non possiamo dire è se mai colpiamo un sys_exite quindi fermiamo il cronometro. Se ci limitiamo a terminare i programmi, il che è ragionevole per una domanda così pratica, allora la risposta è in realtà sì (a condizione che tu abbia una perfetta istantanea dello stato, hw e sw, del sistema appena prima di avviare il programma).
Margaret Bloom,

1
@ BlueRaja-DannyPflughoeft Mov è completo, ma non nel pezzo di codice che l'OP ha qui. Ma questo è a parte il punto - gli ints possono eseguire codice arbitrario, attendere operazioni I / O arbitrarie ecc.
Luaan

2

La scelta del "sistema informatico" includerebbe i microcontrollori? Alcuni microcontrollori hanno tempi di esecuzione molto prevedibili, ad esempio la serie PIC a 8 bit ha quattro cicli di clock per istruzione a meno che l'istruzione non si dirami verso un indirizzo diverso, legga da flash o sia un'istruzione speciale di due parole.

Gli interrupt interromperanno inavvertitamente questo tipo di timimg, ma è possibile fare molto senza un gestore di interrupt in una configurazione "bare metal".

Utilizzando assembly e uno speciale stile di codifica è possibile scrivere codice che richiederà sempre lo stesso tempo per essere eseguito. Non è così comune ora che la maggior parte delle varianti PIC ha più timer, ma è possibile.


2

Nell'era dei computer a 8 bit, alcuni giochi hanno fatto qualcosa del genere. I programmatori utilizzerebbero l'esatto tempo impiegato per eseguire le istruzioni, in base al tempo impiegato e alla velocità di clock nota della CPU, per sincronizzarsi con i tempi esatti dell'hardware video e audio. A quei tempi, il display era un monitor a tubo catodico che scorreva su ogni linea di schermo a una velocità fissa e dipingeva quella fila di pixel accendendo e spegnendo il raggio catodico per attivare o disattivare i fosfori. Poiché i programmatori dovevano dire all'hardware video cosa visualizzare subito prima che il raggio raggiungesse quella parte dello schermo, e adattare il resto del codice in qualsiasi tempo rimanesse, hanno chiamato "corsa il raggio".

Non funzionerebbe assolutamente su nessun computer moderno o per codice come il tuo esempio.

Perchè no? Ecco alcune cose che rovinerebbero i tempi semplici e prevedibili:

La velocità della CPU e i recuperi di memoria sono entrambi colli di bottiglia al momento dell'esecuzione. È uno spreco di denaro eseguire una CPU più velocemente di quanto possa recuperare le istruzioni da eseguire o installare memoria in grado di fornire byte più velocemente di quanto la CPU possa accettarli. Per questo motivo, i vecchi computer funzionavano entrambi allo stesso tempo. Le moderne CPU funzionano molto più velocemente della memoria principale. Ci riescono avendo cache di istruzioni e dati. La CPU continuerà a bloccarsi se deve attendere byte che non si trovano nella cache. Le stesse istruzioni verranno quindi eseguite molto più velocemente se sono già nella cache che se non lo sono.

Inoltre, le moderne CPU hanno tubazioni lunghe. Mantengono alto il loro rendimento facendo fare in modo che un'altra parte del chip esegua i lavori preliminari sulle successive istruzioni in preparazione. Questo fallirà se la CPU non sa quale sarà la prossima istruzione, cosa che può accadere se c'è un ramo. Pertanto, le CPU tentano di prevedere i salti condizionali. (Non ne hai alcuno in questo frammento di codice, ma forse c'è stato un salto condizionato errato ad esso che ha intasato la pipeline. Inoltre, una buona scusa per collegare quella risposta leggendaria.) Allo stesso modo, i sistemi che chiamano int 80per intercettare la modalità kernel in realtà stanno utilizzando una complicata funzione della CPU, un gate di interruzione, che introduce un ritardo imprevedibile.

Se il tuo sistema operativo utilizza il multitasking preventivo, il thread che esegue questo codice potrebbe perdere la sua fetta temporale in qualsiasi momento.

La corsa del raggio funzionava anche solo perché il programma era in esecuzione sul metallo nudo e sbatteva direttamente sull'hardware. Qui stai chiamando int 80per effettuare una chiamata di sistema. Ciò passa il controllo al sistema operativo, il che non offre alcuna garanzia di tempismo. Dici quindi di eseguire l'I / O su un flusso arbitrario, che potrebbe essere stato reindirizzato a qualsiasi dispositivo. È troppo astratto per dirti quanto tempo impiega l'I / O, ma sicuramente dominerà il tempo impiegato nell'esecuzione delle istruzioni.

Se si desidera un tempismo esatto su un sistema moderno, è necessario introdurre un loop di ritardo. Devi eseguire le iterazioni più veloci alla velocità di quella più lenta, non è possibile il contrario. Uno dei motivi per cui le persone lo fanno nel mondo reale è quello di impedire la fuga di informazioni crittografiche a un utente malintenzionato in grado di determinare le richieste che richiedono più tempo di altre.


1

Questo è in qualche modo tangenziale, ma lo space shuttle aveva 4 computer ridondanti che dipendevano dall'essere sincronizzati accuratamente, cioè la loro corrispondenza di runtime esattamente.

Il primissimo tentativo di lancio dello space shuttle è stato cancellato quando il computer Backup Flight Software (BFS) si è rifiutato di sincronizzarsi con i quattro computer Primary Avionics Software System (PASS). Dettagli in "The Bug Heard Round the World" qui . Lettura affascinante su come il software è stato sviluppato per abbinare ciclo per ciclo e potrebbe darti un background interessante.


0

Penso che stiamo mescolando due diversi problemi qui. (E sì, so che questo è stato detto da altri, ma spero di poterlo esprimere più chiaramente.)

Per prima cosa dobbiamo passare dal codice sorgente alla sequenza di istruzioni che viene effettivamente eseguita (che richiede la conoscenza dei dati di input e del codice: quante volte si procede in un ciclo? Quale ramo viene preso dopo un test? ). A causa del problema di arresto, la sequenza di istruzioni può essere infinita (non terminazione) e non è sempre possibile determinarla staticamente, anche con la conoscenza dei dati di input.

Dopo aver stabilito la sequenza di istruzioni da eseguire, si desidera quindi determinare il tempo di esecuzione. Ciò può certamente essere stimato con una certa conoscenza dell'architettura del sistema. Ma il problema è che su molte macchine moderne, il tempo di esecuzione dipende fortemente dalla memorizzazione nella cache dei recuperi di memoria, il che significa che dipende tanto dai dati di input che dalle istruzioni eseguite. Dipende anche dalla corretta supposizione delle destinazioni del ramo condizionate, che di nuovo dipende dai dati. Quindi sarà solo una stima, non sarà esatto.

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.