Questo articolo vuole fare un pò di luce sul delicato argomento
dell'ottimizzazione. Non abbiamo più a che fare con progetti di qualche anno fa,
soprattutto per quanto riguarda Flash: l'ingresso in scena di Molehill,
senza considerare il crescere in maniera esponenziale di soluzioni dedicate per tutti i tipi di
piattaforme e scopi, ha contribuito ad una maggiore richiesta di codice sempre
più testato, performante ed allo stesso tempo senza intoppi.
Tuttavia, prima di cominciare a guardare cos'è l'ottimizzazione in flash e come raggiunge un livello accettabile,
occorre fare una premessa molto importante: bisogna evitare di partire con la pretesa di ottimizzare da subito.
Chiunque abbia esperienza a riguardo potrà confermarlo: il primo passo è scrivere codice pulito, leggibile e facilmente manutenibile.
Ovviamente esistono dei metodi e degli strumenti anche per verificare quali sono le "zone calde" del nostro codice. Uno degli
strumenti più usati è il Flash Builder Profiler della Adobe.
Utilizzandolo con il nostro progetto potremo facilmente localizzare tutti i momenti in cui è necessaria una maggiore elaborazione
da parte del sistema, e quindi una conseguente ottimizzazione da parte nostra.
Accorgimenti Basilari
I primi accorgimenti con i quali veniamo a contatto sono
decisamente elementari. A volte, infatti, i dettagli da sistemare sono i più
semplici, che apparentemente possono rivelarsi efficaci ma in fase di
elaborazione occupano più memoria del previsto.
Un primo consiglio riguarda le iterazioni. In molti casi,
per affrontare un ciclo, si utilizza il tipo "Number". L'uso di un
semplice int può migliorare notevolmente le cose. Soprattutto per quanto
riguarda Actionscript 3, i miglioramenti sono tangibili.
Inoltre, un altro consiglio molto utile è di rimanere sempre strong typed. In poche parole, evitare di utilizzare un qualsiasi oggetto in forma generica, ma assicurarsi di sapere sempre con cosa si ha a che fare. Insomma, se un oggetto è di classe "Cane" identificarlo sempre come "Cane" e non come Object. Un esempio può essere un oggetto di tipo Vector a tre dimensioni (x, y e z) che nel contesto di un metodo viene
preso genericamente come un Object. Lavorando con questi tipi di oggetti nel contesto di molte iterazioni (dalle mille ai dieci milioni) e facendo dei test, molti programmatori hanno notato un cambiamento di velocità sorprendente.
Specificando l'uso di oggetti di classe ben definite abbiamo
un guadagno di oltre metà del tempo che andrebbe invece speso per fare le
stesse elaborazioni con un Object generico. Nello specifico dell'esempio della
classe Vector, infatti, abbiamo un guadagno di tempo medio del 50% circa. Un
cambiamento sicuramente superfluo quando si ha a che fare con pochi elementi e poche strutture, che acquisice invece ben altra rilevanza con un programma che deve valutare o elaborare dati per milioni e milioni di campi, magari presi da un database. In quest'ottica il tempo inizia a pesare.
Sempre rimanendo nel campo "basilare"
dell'ottimizzazione del codice è necessario prendere provvedimenti anche per
quanto riguarda il Casting. Per casting si intende un'operazione esplicita di conversione partendo da un tipo di dato ad un altro definito. In alcuni casi, il casting aiuta sensibilmente a guadagnare tempo, specie quando si esegue l'accesso ad un campo di un determinato oggetto.
Quindi, un'istruzione come:
array[0].x = 1;
sarà molto più lenta di:
Vector3D(array[0]).x = 1;
Da non sottovalutare, inoltre, ciò che riguarda il riutilizzo
delle istanze per risparmiare spazio in memoria. Usare diverse istanze durante l'esecuzione del nostro progetto, rallenta enormemente la
memoria, fino ad arrivare a situazioni spiacevoli nel caso di cicli piuttosto
lunghi e complessi. La creazione di istanze infatti può creare un sacco di
"spazzatura" e il garbage collector è costretto a fare del lavoro
extra. Per iterazioni dell'ordine del milione, infatti, il tempo che viene "perso" possiamo quantificarlo nell'ordine di secondi.
Per capire meglio il discorso facciamo un esempio.
La sintassi
for(; i < n; i++) c1 = new Classe1(); c1.x = 0; c1. y = 2;
non è da tenere in considerazione, in quanto lenta e svantaggiosa. Ecco infatti la versione più "leggera" da usare:
c1 = new Classe1(); for(; i<n; i++) c1.x = 0; c1.y = 2;
Il risultato, utilizzando questa tecnica, è notevole:
il miglioramento può addirittura velocizzare l'esecuzione del codice con un guadagno di tempo di oltre il
90%.
Per concludere questa sezione base, un'osservazione
sui blocchi "try, catch". Quando c'è la possibilità di fare un
controllo con il valore "null", con un nullable, è opportuno preferire un semplice "if(x == null)" anzichè mobilitare un intero blocco di codice "try, catch".
Per rendere l'idea della potenza di un tale piccolo e
semplice accorgimento, basti pensare che lavorando nell'ordine del milione di
iterazioni in un ciclo il miglioramento è del 99%. In alcuni test eseguiti,
infatti, partendo da un tempo campione di 30 secondi per un milione di iterazioni si è arrivati addirittura a 10 millisecondi. Un cambiamento abissale.
Insomma, massima attenzione al proprio codice e a cosa si
può ottimizzare, dato che il guadagno di tempo può fare la differenza. Adesso
diamo uno sguardo a qualcosa di più avanzato: grafica, animazioni e chiamate ai
server.
Grafica, Animazioni e Chiamate ai Server
Nonostante gran parte del lavoro di ottimizzazione si basi
su alcune attente modifiche del codice "puro", altri accorgimenti
possono essere presi per quanto riguarda le altre "zone di lavoro".
Flash oggi è famoso soprattutto per i giochi, data la diffusione di prodotti
pensati anche per la piattaforma di Facebook quali Farmville e Music Challenge.
Quando si parla di giochi online si parla soprattutto di grafica e di dati
memorizzati su un server online.
Occorre quindi tenere a mente e conoscere qualche piccolo
trucco anche per quanto riguarda questi settori, in modo da capire se c'è la
possibilità di fare un lavoro migliore. Iniziamo a vedere qualcosa per quanto
riguarda la grafica.
-
In
primo luogo, evitare dei framerate troppo alti ed inutili. Far lavorare
un gioco a 60 frame per secondo può essere inutile, se il risultato può essere
raggiunto senza problemi e in maniera fluida a 30. Da notare che da 60 a 30 il
cambiamento è decisamente significativo: si tratta infatti di risparmiare, per
ogni secondo di gioco, la metà del tempo per controlli, elaborazioni, rendering
e tutte le altre operazioni; - Se
possibile, evitare in tutti i casi l'utilizzo di filtri grafici in fase di
runtime. Sono molto lenti e richiedono risorse in maniera significativa: se
si può ottenere lo stesso risultato tramite programmi esterni, in modo tale da
mandare in rendering il risultato già elaborato, tanto di guadagnato. Anche le
animazioni ne risentiranno positivamente, mostrandosi più fluide e senza scatti
fastidiosi; - Evitare
di disegnare oggetti con trasparenze al di sopra di altri oggetti con
trasparenze. I calcoli relativi ai canali alpha aumentano in maniera
significativa quando due oggetti con trasparenza si sovrappongono. Nel caso
fosse possibile, infatti, tale accorgimento può rendere il tutto molto più
fluido, anche se è comprensibile che non sempre si può raggiungere una tale
condizione; - Imparare
ad usare una proprietà quale "cacheAsBitmap" non è
sbagliato, anche se si tratta di un'arma a doppio taglio. Se
impostata su "true", infatti, permette di memorizzare nella memoria
cache una rappresentazione del bitmap del filmato. Questo può essere produttivo
nel caso di immagini statiche, ma per quanto riguarda le animazioni l'effetto
potrebbe essere diametralmente opposto.
Anche per quanto riguarda l'utilizzo di connessioni a
fonti esterne di dati e server remoti ci sono molti consigli utili sul
miglioramento dei propri progetti. In questo settore, tra l'altro, il tempo che
si può guadagnare può risultare molto più significativo di tutto ciò che
abbiamo visto finora.
- Evitare
caricamenti troppo frequenti, soprattutto se totalmente inutili. Si pensi
ad un programma che gestisce un insieme di utenti. Caricare il nome e i dati
dell'utente ogni volta è dispendioso: basta memorizzare tutto in locale
all'avvio della sessione, aggiornare eventualmente i dati in caso di modifica e
cancellare tutto alla fine dell'esecuzione. Si sa: che la memoria costa poco,
ma la banda no; - Per
ogni richiesta effettuata, bisogna trovare il giusto quantitativo di dati da
richiedere, ogni volta. Se la quantità è bassa si potrebbe perdere molto
tempo inutilmente ad aprire, chiudere connessioni e preparare il trasferimento.
Se invece la quantità è troppo alta si potrebbe bloccare l'esecuzione per un
bel pò; - Seguendo
il punto precedente, lavorando con files molto piccoli è meglio caricarne di
più per volta. In caso di tipologie diverse d'uso (magari una galleria di
immagini o di video) il discorso cambia, e conviene "mettere in coda"
i vari download necessari.
Adesso tocca all'ultimo argomento, sicuramente uno dei più
interessanti: la gestione del Garbage Collector.
Il Garbage Collector
Il Garbage Collector (da ora in poi GC) è uno strumento importantissimo quando
si parla di linguaggi che gestiscono in maniera automatica la memoria. Imparare
a capire come e quando un determinato oggetto venga effettivamente rimosso
dalla memoria è quindi essenziale a capire cosa fare per evitare dei
fastidiosi problemi con la memoria, che nei casi peggiori possono portare
ad un utilizzo spropositato di risorse utilizzabili più intelligentemente.
Per quanto riguarda Flash, tutto avviene senza l'ausilio
dell'utente: un qualsiasi oggetto in memoria viene automaticamente rimosso nel momento in cui non si contano suoi riferimenti all'interno del programma in
esecuzione. Quindi, volendo fare un esempio, se noi dichiarassimo
l'oggetto
ogg1 = new Classe1();
questo rimane nella memoria fin quando non impostiamo a "null" il riferimento ad ogg1, ovvero
ogg1 = null;
Tuttavia, a volte, per questioni legate al timing del
programma in esecuzione o ad alcune imperfezioni, accade che il GC non funzioni correttamente, e lasci in memoria ciò
che non deve rimanere. Ovviamente, anche a questa problematica c'è una
soluzione: basta infatti forzare l'esecuzione del GC, prendendone il
controllo per fugare ogni dubbio.
Ci sono due modi per forzare le operazioni del GC.
Il primo approccio, semplice, basilare e forse anche
meno elegante, consiste nel richiamare per due volte (si, scrivere la stessa
riga di codice per due volte) il metodo di richiamo del GC (System.gc()
) nel nostro codice in modo tale da essere sicuri al 100% che abbia funzionato.
Richiamando il GC una volta sola, infatti, non se ne ha la certezza totale.
Riprendendo l'esempio di prima, quindi, supponendo che le
nostre istruzioni siano contenute in un metodo "metodo1()":
ogg1 = new Classe1(); ogg1 = null; System.gc(); System.gc();
Il secondo metodo, invece, apparentemente più raffinato ma
che in realtà è tendenzialmente classificato come un semplice hack, prevede la creazione di un nuovo metodo che a sua volta usi il metodo LocalConnection. Di
seguito il codice necessario.
public function hackGC():void { try { new LocalConnection().connect('metodo1'); new LocalConnection().connect('metodo1'); } catch (e:Error) {} }
Ovviamente le possibilità di ottimizzazione non finiscono
qui, anzi, sono molte di più di quanto se ne possa immaginare. Molte, forse,
non sono ancora state scoperte, viste le possibilità di un qualsiasi linguaggio
di programmazione. Ad ogni modo il concetto chiave non cambia: alla base di una
buona ottimizzazione bisogna prima vedere cosa davvero necessita un
miglioramento prima di effettuare modifiche alla cieca, in modo da
bilanciare efficienza ma anche velocità di scrittura del codice e di
realizzazione.