1.08 Lezione 8 - follen99/ArchitetturaDeiCalcolatori GitHub Wiki
Il compilatore usato è il gcc. Il gcc è un progetto open source realizzato da GNU che tra i vari progetti ha anche creato questo compilatore.
Su questi tool è fondato il sistema LINUX.
Sulle basi della lezione di ieri, per eseguire un programma scritto in C, ci basta scrivere a linea di comando:
gcc hello.c
Con questo comando viene creato un file a.out eseguibile; possiamo verificarlo scrivendo sul terminale:
ls -l
Con questo comando vengono listati tutti i files in una directory, ecco l'output:
ls -l
total 72
-rwxr-xr-x 1 folly staff 8432 11 Gen 20:03 a.out
Capiamo che il file è eseguibile non dall'estensione come nei sistemi microsoft, ma dalle "x" all'inizio della stampa.
Per eseguirlo digitiamo sulla Shell:
./a.out
Nelle ultime versioni di MacOS, gli sviluppatori hanno deciso di passare ad un compilatore gcc più moderno, chiamato clang
.
Se continuiamo ad usare degli script che usano gcc
, viene invece usato il compilatore clang
.
Lanciando il gcc otteniamo anche un file oggetto, con estensione .o che deve essere opportunamente linkato alle librerie, in modo da ottenere un eseguibile a.out.
Se digitiamo sul terminale:
ls -la
Otteniamo:
MBP-di-Giuliano:FromCtoAssembly folly$ ls -la
total 72
drwxr-xr-x 7 folly staff 224 11 Gen 20:04 .
drwxr-xr-x 3 folly staff 96 11 Gen 19:41 ..
-rwxr-xr-x 1 folly staff 8432 11 Gen 20:03 a.out
-rw-r--r-- 1 folly staff 84 11 Gen 19:41 helloWorld.c
-rw-r--r-- 1 folly staff 784 11 Gen 19:57 helloWorld.o
-rw-r--r-- 1 folly staff 775 11 Gen 19:42 helloWorld.s
-rwxr-xr-x 1 folly staff 8432 11 Gen 20:04 programmaEseguibile
Notiamo che il file a.out (eseguibile) è il file più grande.
Quando eseguiamo una compilazione viene eseguito un linking di tipo dinamico e non statico.
Se negli esempi precedenti (la compilazione) avessimo usato un linking di tipo statico, il file a.out eseguibile, invece di pesare 8432 byte
, sarebbe pesato molto di più, proprio perchè il linker avrebbe aggiunto tutte le librerie utilizzate interamente.
Abbiamo quindi capito che il linker dinamico linka le librerie solo quando il programma è in esecuzione. Quindi all'interno del file a.out non è presente il codice delle librerie, ma viene linkata al momento dell'esecuzione.
Evita che il programma eseguibile diventi enorme, e le librerie vengono aggiunte siolo quando servono.
Inoltre in questo modo le librerie vengono aggiornate automaticamente, proprio perchè non sono incluse nel codice (hardwired).
Il linkaggio non viene eseguito a momento della compilazione, ma quando il programma viene lanciato in esecuzione.
Quando eseguo il programma, alla prima chiamata (ad esempio della printf) si salta non alla funzione ma al cosiddetto stub
il quale chiede al SO di caricare in un particolare indirizzo, l'indirizzo della funzione che si vuole chiamare.
Nei sistemi UNIX abbiamo delle librerie dinamiche (a linking dinamico) .so
, mentre quelle a linking statico sono .a
.
L'estensione so
sta per shared object. Microsoft invece chiama queste librerie dinamiche .dll
.
Cosa succede quando lancio un programma java?
La catena di operazioni è completamente diversa da quella appena vista.
Anche in Java esiste un compilatore: Javac
; questo compilatore non produce file assembly ne tantomeno files .o
.
Quello che viene in realtà prodotto dal compilatore javac è il cosiddetto java bytecode
; questo perchè java è nato per consentire la portabilità su sistemi diversi, e non era inteso per applicazioni in programmi particolarmente grandi (come in realtà accade).
Questo java bytecode
è progettato appositamente per essere particolarmente portabile; questo bytecode viene interpretato al volo sulla macchina su cui ci troviamo.
Basta quindi cambiare la Java Virtual Machine
per cambiare il modo in cui il bytecode viene interpretato, in modo da poterlo eseguire su processori e sistemi operativi diversi.
Un Interprete
è quindi molto diverso da un compilatore, infatti l'interprete interpreta
istruzione per istruzione, e questo compito è svolto dalla java virtual machine.
Grazie a questo meccanismo è possibile eseguire gli stessi già compilati .jar
su diversi processori, senza doverli compilare nativamente sulla macchina su cui devono essere eseguiti.
Questo tipo di compilazione (interprete) non è allo stesso livello di un compilatore vero e proprio, infatti questo meccanismo è molto meno efficiente rispetto, ad esempio, ad un programma scritto in C.
Per tentare di risolvere il problema, il runtime di java quando si rende conto che ci sono dei pezzi di codice che vengono eseguiti abbastanza spesso, entra automaticamente in funzione un compilatore just in time
; solo quel pezzo di codice viene (durante l'esecuzione) compilato (quindi a codice macchina) in modo da non dover interpretare continuamente lo stesso codice.
Questo permette quindi a java di raggiungere prestazioni simili a quelle di un compilatore C.
🏁 0:33 03-24
La tecnica usata è il bubble sort (che schifo).
void swap(int k, int v[]){
int temp;
temp = v[k]
v[k] = v[k+1];
v[k+1] = temp;
}
funzione swap
void sort(int v[], size n){
size i, j;
for(i = 0; i<n; i+=1){
for(j = i-1; j >=0 && v[j] > v[j+1]; j-=1)
swap(v,j);
}
}
In Assembly i registri sono stati "preparati" nel seguente modo:
- v in a0
- k in a1
- Temp in t0
swap:
slli t1, a1, 3 # moltiplicazione per 8
add t1, a0, t1 # posiziono il puntatore sulla posizione dell'array (a sinistra); in t1 ho v[k] (indirizzo)
ld t0, 8(t1) #
sd t2, 0(t1)
sd t0, 8(t1)
jalr zero, 0(ra)
funzione di Swap
- For(i = 0; i<n; i+=1)
li s3, 0
for1tst:
bge s3, a1, exit1
🏁 00:44
I compilatori C sono in grado di eseguire delle ottimizzazioni del codice, spostando opportunamente pezzi di codice, eliminando pezzi inutili, ecc.
Il programma viene quindi rimaneggiato spostando istruzioni, sostituendo alcune, ecc, in modo da far girare il codice più velocemente.
Come abilitiamo le ottimizzazioni?
E' possibile indicare sulla lina di comando gcc -o
+ numero.
Se digitiamo nel terminale man clang
(su UNIX) possiamo consultare le varie informazioni della compilazione.
Alcune di queste opzioni, potrebbero produrre un programma che non funziona correttamente (anche se solitamente non accade)
Ad esempio, con il nostro programma di bubble sort visto prima, solo aggiungendo l'ottimizzazione quando compiliamo, è possibile ottenere un programma ottimizzato che gira notevolmente più veloce.
Oltre alla velocità, si può avere anche un ottimizzazione di istruzioni eseguite, che anche in questo caso si traducono in un'ottimizzazione a livello di velocità.
Il compilatore può anche decidre di cambiare le istruzioni che abbiamo usato, in modo che le istruzioni da esso scelte vengano eseguite con un numero di cicli di clock minore.
La pratica di cambiare le istruzioni utilizzate, da parte del compilatore, è chiamata Strenght reduction.
Questa pratica è poco effettiva su processori di livello avanzato come quelli odierni, ma su un processore "vecchio" porta dei grandi vantaggi in termine di tempo (ad esempio sostituire le moltiplicazioni per potenza di 2 con degli shift).
mv t0, a0
slli t1, a1, 3
add t2, a0, t1
loop2:
sd zero, 0(t0)
addi t0, t0, 8
bltu t0, t2, loop2
Codice ottimizzato
Nel loop è presente semplicemente un incremento di 8 per passare alla doubleword successiva dopo aver azzerato l'indirizzo di memoria corrente.
Le istruzioni del MIPS sono molto simili a quelle del RISC. Anche in questo caso abbiamo istruzioni a 32 bit, con 32 registri, alla memoria si accede solo mediante load e store, quindi anche in questo caso non possiamo eseguire direttamente dalla memoria.
Sul RISC, però, sono state aggiunte le istruzioni di branch come: blt, bge, bltu, bgeu
.
Anche la codifica delle istruzioni è molto simile.
Fine lezione 8