Avanti
Indietro
Indice
Beh, non vorrei interferire con ciò che state facendo, ma
ecco alcuni consigli derivanti da una esperienza
ottenuta faticosamente.
I vantaggi dell'assembly
L'assembly può esprimere cose molto a basso livello:
- potete accedere a registri e ad I/O dipendenti dalla macchina.
- potete controllare l'esatto comportamento di codice
in sezioni critiche che potrebbe comportare il bloccarsi di
hardware o I/O.
- potete trasgredire le convenzioni del vostro compilatore abituale,
il che potrebbe permettere alcune ottimizzazioni
(come ad esempio violare temporaneamente le regole per il garbage
collecting, threading, ecc).
- ottenere accesso a modi di programmazione insoliti del vostro
processore (ad esempio codice a 16 bit per l'avvio o l'interfaccia
BIOS sui PC Intel).
- potete costruire interfacce tra frammenti di codice che usano
convenzioni incompatibili
(ad esempio prodotti da compilatori diversi
o separati da una interfaccia a basso livello).
- potete produrre codice ragionevolmente veloce per cicli stretti
per far fronte ad un compilatore non-ottimizzante di qualità
scadente (ma dopotutto sono disponibili compilatori ottimizzanti
liberi!).
- potete produrre codice ottimizzato a mano che risulta ottimo per
la vostra particolare configurazione hardware, ma non
per quella di chiunque altro.
- potete scrivere del codice per il compilatore ottimizzante del
vostro nuovo linguaggio
(è qualcosa che poche persone fanno, ed anche loro
non lo fanno molto spesso).
Gli svantaggi dell'assembly
L'assembly è un linguaggio molto a basso livello
(più in basso c'è solo la codifica a mano
delle istruzioni in codice binario).
Ciò significa:
- All'inizio è lungo e tedioso da scrivere.
- È notevolmente soggetto ad errori.
- Gli errori saranno molto difficili da scovare.
- È molto difficile da comprendere e modificare,
in altre parole da mantenere.
- Il risultato è decisamente non portabile verso altre architetture,
esistenti o future.
- Il vostro codice verrà ottimizzato solo per una certa
implementazione di una stessa architettura:
ad esempio, tra le piattaforme compatibili Intel,
avere CPU diverse o differenti configurazioni (ampiezza del bus,
velocità e dimensioni relative di CPU/cache/RAM/bus/dischi,
presenza di FPU, estensioni MMX, ecc.) può richiedere tecniche
di ottimizzazione radicalmente diverse.
I tipi di CPU comprendono già
Intel 386, 486, Pentium, PPro, Pentium II;
Cyrix 5x86, 6x86; AMD K5, K6.
Inoltre, continuano ad apparire nuovi tipi, perciò
non aspettatevi che questo elenco o il vostro codice siano aggiornati.
- Il vostro codice potrebbe inoltre non essere portabile verso piattaforme
con sistemi operativi differenti ma con la stessa architettura per
la mancanza di strumenti adeguati
(beh, GAS pare funzionare su tutte le piattaforme; a quanto sembra,
NASM funziona o può essere reso funzionante su tutte le
piattaforme Intel).
- Perdete più tempo su pochi dettagli e non potete concentrarvi
sulla progettazione algoritmica su piccola e grande scala che,
come è noto, porta il maggior contributo
alla velocità del programma.
Per esempio, potresete perdere del tempo per scrivere in assembly
delle primitive molto veloci per la manipolazione di liste o matrici,
quando sarebbe bastato utilizzare una tabella hash per accelerare molto
di più il vostro programma. Magari, in un altro contesto,
sarebbe servito un albero binario, o qualche struttura ad alto
livello distribuita su un cluster di CPU.
- Un piccolo cambiamento nell'impostazione algoritmica potrebbe
far perdere ogni validità a tutto il codice assembly
già esistente.
Perciò o siete pronti a (ed in grado di) riscriverlo tutto,
oppure siete vincolati ad una particolare impostazione algoritmica.
- Per quanto riguarda il codice che non si scosta troppo da quello
che è presente nei benchmark convenzionali,
i compilatori ottimizzanti commerciali permettono di ottenere
prestazioni migliori rispetto all'«assembly manuale»
(beh, ciò è meno vero sulle architetture x86
rispetto alle architetture RISC e forse meno vero per compilatori
largamente disponibili/liberi; comunque, per codice C tipico, GCC se
la cava discretamente).
- E in ogni caso, come dice il moderatore John Levine su comp.compilers,
«i compilatori rendono decisamente più facile utilizzare
strutture dati complesse, non si stancano a metà strada
e ci si può aspettare che generino codice abbastanza buono».
Inoltre provvederanno a propagare correttamente trasformazioni
di codice attraverso tutto il (lunghissimo) programma quando
si tratterà di ottimizzare codice tra i confini delle procedure
e dei moduli.
Valutazioni
Tutto sommato, potreste notare che
nonostante l'uso dell'assembly sia talvolta
necessario (o semplicemente utile, in alcuni casi),
sarà il caso che:
- minimizziate l'uso del codice assembly;
- incapsuliate questo codice in interfacce ben definite;
- facciate generare automaticamente il vostro codice assembly
da strutture espresse in un linguaggio a più alto livello
rispetto all'assembly stesso (ad esempio le macro dell'assembly
inline di GCC);
- facciate tradurre in assembly questi programmi da strumenti automatici;
- facciate ottimizzare questo codice, se possibile;
- tutti i punti di cui sopra,
cioè scriviate (un'estensione ad) un backend
per un compilatore ottimizzante.
Anche nei casi in cui l'assembly è necessario (ad esempio, nello
sviluppo di sistemi operativi),
scoprirete che non ne serve poi molto
e che i principi precedenti continuano a valere.
A questo riguardo, date un'occhiata ai sorgenti del kernel di Linux:
poco assembly, giusto lo stretto necessario,
il che ha come risultato un sistema operativo veloce, affidabile, portabile
e mantenibile.
Anche un gioco di successo come DOOM è stato scritto quasi completamente
in C, con solo una minuscola parte scritta in assembly per renderlo
più veloce.
Procedura generale per ottenere codice efficiente
Come dice Charles Fiterman su comp.compilers
circa il confronto tra codice assembly generato
a mano o automaticamente,
«L'uomo dovrebbe sempre vincere, ed eccone i motivi:
- Primo passo: l'uomo scrive il tutto in un linguaggio ad alto livello.
- Secondo passo: provvede ad un profiling per trovare i punti in cui
si perde più tempo.
- Terzo passo: fa produrre al compilatore codice assembly per quelle
piccole sezioni di codice.
- Quarto passo: le perfeziona a mano cercando piccoli miglioramenti
rispetto al codice generato dalla macchina.
L'uomo vince perché sa usare la macchina.»
Linguaggi con compilatori ottimizzanti
I linguaggi quali
ObjectiveCAML, SML, CommonLISP, Scheme, ADA, Pascal, C, C++,
tra gli altri,
dispongono di compilatori ottimizzanti liberi
che ottimizzeranno il grosso dei vostri programmi
(e spesso otterranno risultati migliori rispetto all'assembly manuale
anche per cicli stretti),
permettendovi nel frattempo di concentrarvi su dettagli più
ad alto livello, il tutto senza vietarvi di ottenere qualche punto
percentuale di prestazioni in più nella maniera espressa
sopra, una volta che il vostro progetto
avrà raggiunto un'impostazione stabile.
Certo, ci sono anche compilatori ottimizzanti commerciali per la maggior
parte di quei linguaggi!
Alcuni linguaggi hanno compilatori che producono codice C, che può
essere ulteriormente ottimizzato da un compilatore C.
LISP, Scheme, Perl e molti altri
fanno parte di questa categoria.
La velocità è abbastanza buona.
Procedura generale per accelerare il vostro codice
Per quanto riguarda l'accelerazione del vostro codice,
dovreste restringerla alle parti di un programma che uno
strumento di profiling ha decisamente identificato
come un collo di bottiglia.
Perciò, se identificate qualche porzione di codice come troppo
lenta, dovreste:
- prima di tutto provare ad usare un algoritmo migliore;
- poi provare a compilarla invece di interpretarla;
- poi provare ad abilitare e raffinare l'ottimizzazione per il vostro
compilatore;
- poi dare al compilatore dei consigli su come ottimizzare
(informazione sui tipi in LISP; uso di register con GCC;
un mucchio di opzioni nella maggior parte dei compilatori, ecc.);
- infine, se è il caso, ripiegate sulla programmazione assembly.
Come ultima cosa, prima che vi riduciate a scrivere assembly,
dovreste ispezionare il codice generato,
per controllare che il problema risieda proprio nella cattiva generazione
del codice, visto che potrebbe anche non essere così:
il codice generato dal compilatore potrebbe essere migliore di quanto
avreste potuto fare voi,
specialmente sulle moderne architetture multi-pipelined!
Le parti lente di un programma potrebbero essere intrinsecamente tali.
I più grossi problemi sulle architetture moderne con processori veloci
sono dovuti a ritardi di accesso alla memoria, cache-miss, TLB miss,
e page fault;
l'ottimizzazione sui registri diventa inutile, ed otterrete risultati
migliori riprogettando le strutture dati ed il threading per ottenere
una miglior località nell'accesso alla memoria.
Potrebbe forse essere d'aiuto un approccio completamente diverso al problema.
Ispezione del codice generato dal compilatore
Ci sono molte ragioni per ispezionare il codice assembly generato dal
compilatore.
Ecco cosa potete fare con tale codice:
- controllate se il codice generato
può essere migliorato in maniera ovvia con assembly manuale
(o con le opportune opzioni per il compilatore).
- Quando è il caso, partite da codice generato e modificatelo,
invece di ripartire da zero.
- Più in generale, utilizzate il codice generato come
stub da modificare. In questo modo, almeno, viene
gestito correttamente il modo in cui le vostre routine
assembly si interfacciano col mondo esterno.
- Rintracciare dei bug nel vostro compilatore (raramente, si spera).
Il modo canonico per far generare codice assembly
è invocare con il flag -S
il vostro compilatore.
Ciò funziona con la maggior parte dei compilatori UNIX,
compreso il compilatore C di GNU (GCC), ma nel vostro caso le cose potrebbero
andare diversamente.
Nel caso di GCC, con l'opzione -fverbose-asm
verrà prodotto
codice assembly più comprensibile.
Certo, se volete ottenere buon codice assembly, non dimenticate di dare
i soliti consigli e le solite opzioni per l'ottimizzazione!
Avanti
Indietro
Indice