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
 

 
Settembre 1988