PROGRAMMI
COME AUMENTARE LA VELOCITA'
DEI PROGRAMMI IN AMIGABASIC
SPEED BASIC
di Giorgio Dose
Avete appena scritto un programma in Basic sul vostro
Amiga e lo trovate un po' lento? Volete aumentarne il numero di giri? Seguendo
le tecniche usate dai compilatori e spremendo un po' le vostre meningi
è possibile incrementare considerevolmente la velocità di
esecuzione.
Esistono i metodi più svariati per migliorare un
programma e renderlo più veloce ed ognuno di noi conosce diversi
trucchetti per farlo. Quelle che troverete di seguito sono delle regole
generali che molti già conoscono ma che possono tornare utili ai
programmatori meno esperti. Alla fine dell'articolo inoltre sono riportati
alcuni programmi per valutare i vantaggi che si ottengono usando queste
tecniche.
Cosa fanno i compilatori
I vari compilatori C, Fortran, Pascal usano varie tecniche per
diminuire i tempi di esecuzione delle loro routine. Molti di essi esaminano
il codice oggetto e individuano dove e come va ottimizzato. Alcuni di essi
analizzano i passi in cui il programma impiega molto tempo e le variabili
poste all'interno dei loop, poi decidono con "intelligenza" i cambiamenti
da apportare al programma stesso. Altri compilatori invece fanno le loro
modifiche solo dopo aver creato il linguaggio macchina.
Le tecniche che verranno descritte di seguito possono
essere applicate a quasi tutti i linguaggi a livello sorgente.
Il metodo più semplice è conosciuto come
"costant folding ". Il compilatore esamina tutte le costanti che possono
essere combinate fra loro in modo da eliminare i relativi calcoli durante
l'esecuzione. Considerate la seguente funzione:
x = 11 + 2 * ( x * 9/6 + y ) * 8.6 + 4 * 5
dopo che il compilatore ha eseguito la trasformazione delle costanti
diventerà:
x = 17.2 * ( x * 1.5 + y) + 31
Sono state eliminate due moltiplicazioni, una divisione
e una addizione ed il tempo di esecuzione risulterà notevolmente
più breve.
Un altro metodo molto noto è il cosiddetto "code
motion" e consiste nel ridurre il numero di operazioni matematiche all'interno
di un loop. Il compilatore esamina quali espressioni non vengono mai modificate
all'interno di un loop e quindi porta le stesse al di fuori del ciclo in
modo da evitare la ripetizione dei calcoli. Consideriamo ad esempio il
seguente loop:
FOR I = 1 T0 360 STEP 10
FOR J = O T0 360 STEP
R = 2 0 t W
K = SIN(I)*SIN(J) + COS(I)*COS(J)
Z=Z+J+R
NEXT J
NEXT I
La prima funzione che salta all'occhio è ovviamente
R = 20 + W; non c'è nulla infati, all'interno del loop, che possa
modificare il valore di R o di W. Il compilatore allora porta l'espressione
fuori da entrambi i cicli FOR-NEXT. Osservando il programma con maggiore
attenzione si può notare che anche la variabile I rimane costante
nell'ambito del loop interno e così pure il valore di SIN(I) e COS(I).
E' quindi possibile portare anche questi due calcoli al di fuori del loop.
Il risultato ottimizzato sarà quindi:
R = 20 + W
FOR I = 1 T0 360 STEP 10
X1 = SIN(I)
X2 = COS(I)
FOR J=1 TO 360
K=X1*SIN(J) + X2 * COS(J)
Z = Z + J + R
NEXT J
NEXT I
L'aver portato fuori dal ciclo due funzioni come SIN e
COS incrementa significativamente la velocità di esecuzione del
programma perché riduce notevolmente il numero di cicli eseguiti.
Per ottenere questo miglioramento il compilatore ha dovuto generare due
proprie variabili interne X1 e X2 e rimpiazzare con esse le funzioni SIN(I)
e COS(I). Da rilevare che l'operazione di cui sopra aumenta le prestaizoni
ma occupa più memoria.
Veniamo al BASIC
I metodi visti or ora possono venire efficacemente usati nei vostri
programmi in AmigaBasic. L'AmigaBasic è un linguaggio interpretato
e come tale non è in grado di ottimizzare da solo i propri programmi;
siamo noi che dobbiamo decidere come e dove intervenire nei vari passi
del processo. Per scrivere programmi più veloci è bene rispettare
queste semplici regole:
1) Non usare variabili quando è possibile usare delle costanti.
Ogni volta che l'interprete Basic esamina una variabile deve guardare la
sua tabella delle variabili per trovarne il valore. Questo non richiede
molto tempo, però se la cosa si ripete molto spesso la velocità
scende rapidamente.
2) Non inserire commenti nei loop che richiedono molto tempo per essere
completati. L'esame dei commenti normalmente non comporta dei ritardi apprezzabili
al programma ma nei cicli FOR-NEXT aumenta senz'altro il tempo di attuazione
dei loop stessi. Questo non è un invito a non inserire mai i commenti
ma solo un consiglio a non metterli nei punti più critici. I commenti
infatti sono molto utili perché facilitano la lettura del programma
e la sua revisione o modifica anche diverso tempo dalla stesura ed inoltre
consentono ad altre persone di comprendere meglio il funzionamento del
programma stesso.
3) E importante conoscere tutte le funzioni dell'AmigaBasic; può
sembrare una cosa ovvia ma è molto facile dimenticarsi delle funzioni
meno comuni e creare al loro posto delle routine meno efficienti.
4) Evitare le chiamate frequenti alle subroutine. Se ad esempio un programma
chiama la stessa routine ripetutamente all'interno di un loop è
consigliabile inserire il codice della subroutine all'interno del ciclo.
Se le subroutine sono più d'una bisogna cercare, se possibile, di
fonderle insieme e crearne una sola. Ogni volta che viene chiamata una
subroutine infatti l'AmigaBasic mantiene traccia del punto in cui deve
ritornare dopo aver eseguito la subroutine. E ripetere questa operazione
più e più volte aumenta il tempo di effettuazione del programma.
Primo programma
Passiamo ora ad esaminare un programma esempio e vediamo come sia possibile
mettere in pratica quanto finora esposto.
Il listato uno contiene tre semplici routine per disegnare
cinque volte una funzione matematica con diversi valori dei parametri.
La prima routine chiamata SlowPlot è quella originale, è
la più lenta (più di due minuti per essere portata a termine)
e verrà migliorata al fine di ridurne il tempo di esecuzione.
Nella routine SlowPlot si notano immediatamente i commenti,
inseriti all'interno dei loop, che vanno eliminati o portati all'esterno
del ciclo, e la funzione FN che può essere tolta e sostituita da
una semplice variabile.
Osservando poi sullo schermo il disegno della funzione
si rileva che è simmetrico rispetto al centro: possiamo quindi trarre
vantaggio da questa simmetria, unire i loop interni e disegnare entrambi
i lati della funzione nello stesso tempo.
Le variazioni apportate alla routine SlowPlot si evidenziano
nella successiva routine QuickPlot. QuickPlot impiega circa 50 secondi
per essere condotta a termine, un buon risultato!
Ma vediamo come si possa ancora migliorare...
Nell'esecuzione del loop più interno della routine QucikPlot
si nota che solo l'espressione contenente la variabile presenta un valore
sempre diverso, mentre i calcoli con lo stesso valore della variabile x
vengono ripetuti cinque volte (una per ogni diverso valore di j). Invertendo
il loop interno con quello esterno per ogni punto della funzione disegnato
sullo schermo, i calcoli, comprendenti la variabile x, vengono eseguiti
una solva volta e non cinque volte come avveniva in precedenza.
Con le ultime variazioni siamo arrivati all'ultima routine
QuickerPlot che si completa in circa 40 secondi. Possiamo ben dire di aver
fatto un buon lavoro.
Le tre routine descritte sono state inserite nel programma una
di seguito all'altra in modo che si possa facilmente valutare il diverso
rendimento di ognuna.
'Listato 1
'Migliorare la velocita' dei programmi
'
'Esempio di "plotting"
'
DEF FNa(t) = (100/360*t)/3
'
'Disegna un punto alla volta e
'riporta il tempo impiegato
'
CLS
GOSUB SlowPlot
FOR i = 1 TO 10000: NEXT i
CLS
GOSUB QuickPlot
FOR i = 1 TO 10000: NEXT i
CLS
GOSUB QuickerPlot
LOCATE 1,1
PRINT "SlowPlot: Begin -- ";a1$;" End - ";a2$
PRINT "QuickPlot: Begin -- ";b1$;" End - ";b2$
PRINT "QuickerPlot: Begin -- ";c1$;" End - ";c2$
END
SlowPlot:
PRINT "SlowPlot"
a1$ = TIME$
FOR j = 20 TO 100 STEP 20
FOR x = -150 TO -1 STEP .2
'
' Prima disegna la parte destra
'
y = SIN(FNa(x))/(FNa(x))*j+50
PSET (x+150,y),1
NEXT x
FOR x = 1 TO 150 STEP .2
'
' Ora disegna la parte sinistra
'
y = SIN(FNa(x))/(FNa(x))*j+50
PSET (x+150,y),1
NEXT x
NEXT j
a2$ = TIME$
RETURN
QuickPlot:
PRINT "QuickPlot"
b1$ = TIME$
FOR j = 20 TO 100 STEP 20
FOR x = 1 TO 150 STEP .2
sy = x*.0925926
y1 = SIN(sy)/sy*j+50
PSET (150-x,y1),1
PSET (x+150,y1),1
NEXT x
NEXT j
b2$ = TIME$
RETURN
QuickerPlot:
PRINT "QuickerPlot"
c1$ = TIME$
FOR x = 1 TO 150 STEP .2
sy = x*.0925926
SinTemp = SIN(sy)/sy
FOR j = 20 TO 100 STEP 20
y1 = SinTemp*j+50
PSET (150-x,y1),1
PSET (x+150,y1),1
NEXT j
NEXT x
c2$ = TIME$
RETURN
|
Secondo programma
La tecnica di ottimizzazione che andiamo a presentare è
senzaltro la più efficace ma anche la più difficile. Dopo
aver completato un programma e aver fatto il possibile per aumentare le
sue prestazioni, facciamo mentalmente un passo indietro e chiediamoci:
le routine che abbiamo scritto sono veramente le migliori o potevamo fare
di meglio?
È molto difficile dare una risposta e lo è
ancor di più per i programmatori meno esperti. Possiamo fare un
piccolo test, non per valutare le nostre capacità ma bensì
per imparare, da un semplice esempio, che non sempre la soluzione più
logica è la migliore, anzi ...
Provate ad esempio a scrivere una routine per mescolare un mazzo di
carte, supponendo di avere un array già inizializzato con il nome
delle carte e di poter usare qualsiasi altra variabile. Avete fatto? Bene,
potete continuare nella lettura.
Molti programmatori, in un caso come questo, avrebbero
scritto una routine molto simile alla routine ShuffleA del listato numero
due. L'idea di base è di creare, oltre a quello contenente i nomi
delle carte, due nuovi array, uno per mettervi le carte mescolate man mano
che vengono selezionate ed uno per segnare quelle che sono già state
prelevate (non vogliamo avere duplicati).
La routine inizialmente pulisce l'array Check o meglio
lo inizializza completamente a zero. Poi, all'interno del ciclo FORNEXT,
usa un numero random per selezionare una carta a caso e controlla che la
carta non sia già stata estratta guardando nell'array Check. Se
era già stata scelta si preleva un'altro numero random finché
non si trova una carta che ancora non era stata toccata; a questo punto
si pone quest'ultima nell'array Shuffled. Il ciclo si ripete finché
tutte le 52 carte sono state selezionate e poste a caso nell'array.
Questo può inizialmente sembrare un buon algoritmo
ma esaminandolo attentamente si nota che esso potrebbe impiegare molto
tempo per estrarre tutte le carte del mazzo. Questo perché man mano
che le carte vengono scelte e posizionate nel nuovo array, aumentano le
probabilità che esca un numero random corrispondente ad una carta
già mescolata, con conseguente ripetizione del ciclo.
Per rimediare a questo difetto, se così possiamo
chiamarlo, è necessario inserire nella routive un certo livello
di intelligenza artificiale. E quanto si è cercato di fare nella
seconda routine chiamata ShuffleB. Questa routine usa meno memoria della
precedente e risulta più veloce nell'esecuzione. L'idea consiste
nel prelevare, all'interno di un ciclo FOR-NEXT, un numero random compreso
tra uno e 52 e quindi nello scambiare di posto, nell'array contenente il
nome delle carte, la carta corrente (avente il numero di posizione progressivo
uguale alla variabile I del loop) con quella il cui numero corrisponde
al numero random. Così facendo vengono sempre prelevati solo 52
numeri random e la routine impiega sempre lo stesso tempo per mescolare
tutte le carte.
Per mostrare con maggior evidenza la diversa velocità
di esecuzione delle due routine il programma del listato due inizializza
e mescola il mazzo di carte per ben dieci volte. La routine A impiega da
18 a 22 secondi per essere portata a termine mentre la routine B si completa
sempre in otto secondi.
Spero che gli esempi sopra riportati siano di stimolo
a migliorare sempre i propri programmi; non fermarsi mai alla prima idea
che viene in mente ma tentare continuamente nuovi e diversi modi di risolvere
un problema, può sempre capitare di inciampare in una soluzione
migliore e più veloce.
'Listato 2
'
'Migliorare la velocita' del programma
'
'Esempio "Come mescolare un mazzo di carte"
'
DIM card$(52), Shuffled$(52), Check(52)
DIM Spot$ (4)
Spot$(0) = " di cuori"
Spot$(1) = " di quadri"
Spot$(2) = " di fiori"
Spot$(3) = " di picche"
DATA "asso","due","tre","quattro","cinque","sei","sette"
DATA "otto","nove","dieci","jack","donna","re"
Main:
RANDOMIZE TIMER
'
' ShuffleA
'
PRINT "Shuffling ..."
a1$ = TIME$
FOR S = 1 TO 10
RESTORE
GOSUB Init
FOR I = 1 TO 52
Check(I) = 0
NEXT I
FOR I = 1 TO 52
Again:
X = INT(RND*52+1)
IF (Check(X) =
1) GOTO Again
Shuffled$(I) =
card$(X)
Check(X) = 1
NEXT I
NEXT S
a2$ = TIME$
PRINT "ShuffleA -- Start: ";a1$;" End ";a2$
'
'ShuffleB
'
PRINT "Shuffling ..."
b1$ = TIME$
FOR S = 1 TO 10
RESTORE
GOSUB Init
FOR I = 1 TO 52
Check(I) = 0
NEXT I
FOR I = 1 TO 52
X = INT(RND*52+1)
SWAP card$(I),card$(X)
NEXT I
NEXT S
b2$ = TIME$
PRINT "ShuffleB -- Start: ";b1$;" End ";b2$
END
Init:
X = 1
FOR J = 1 TO 13
READ A$
FOR I = 0 TO 3
card$(X) = A$+Spot$(I)
X = X + 1
NEXT I
NEXT J
RETURN
|
|