MokaByte Numero 29  - Aprile 1999
 
Ottimizzaizone 
di codice Java
di
Andrea Fasce
Non è detto che Java sia per forza un linguaggio lento


Alcuni trucchi e soluzioni per rendere  Java un po' meno lento o perlomeno per evitare di rallentarlo troppo 

Introduzione

Spesso mi e’ capitato di scontrarmi (bonariamente di intende….:) con alcuni amici sull’efficienza e velocita’ del java in applicazioni realtime tanto che dopo l’ennesima discussione ho deciso di dimostrare le mie tesi programmando qualcosa che non avrebbe lasciato possibilita’ di risposta da parte dei miei interlocutori: una engine 3d completamente software; il mio obiettivo era quello di ottenere una applet compatta e veloce, che fosse compatibile con tutti i browsers e che fosse facile da usare; e finalmente dopo molto tempo passato a programmare, ma soprattutto a cercare vie alternative per ottenere algoritmi efficienti, ho sviluppato un kit di sviluppo completo per importare scene create con il 3dsmax e con il vrml (e presto anche con il lightwave) dentro ad una applet.
Molti sono i problemi di efficienza e di occupazione con cui mi sono scontrato, ma che sono riuscito a risolvere grazie soprattutto ad alcuni interessanti trucchetti che ho scoperto giocando direttamente con codice in bytecode (che ricordiamo e’ il codice intermedio in cui e’ rappresentata una classe java).
Il risultato e’ una applet molto contenuta (poco piu’ di 40kb che diventano 20kb se la si comprime con il jar) in grado di caricare scene salvate in un formato compresso particolare (a3d) compatibile con tutti i browsers ma soprattutto in grado di aggiungere, con 50kb di spazio totale occupato (classi+scene+textures) ottimi effetti 3d a qualsiasi pagina web supportando svariate  modalita’ di rendering e filtri di post-processing digitale.
L’applet e’ visibile sul sito www.anfyjava.com sotto la voce anfy3d, ed e’ ovviamente scaricabile da chiunque gratuitamente.
Come accennato prima, lo sviluppo di questa engine e’ stato molto utile per mettere a nudo la reale potenza di java, ma soprattutto per farmi un’idea dei sistemi di ottimizzazione adottabili su questa architettura. 
 
 
 

 Ma a cosa puo’ servire ottimizzare una classe java ?!?

Un esempio e’ proprio anfy3d, che dovendo rappresentare scene tridimensionali aggiornate in realtime, deve sottostare a certi vincoli temporali in modo da mostrare l’animazione in un modo ‘decentemente’ fluido.
Ma l’ottimizzazione di una classe java e’ altresi’ importante sia per la realizzazione di sistemi embedded (che sono o meglio erano lo scopo reale del java), sia per interfacce di interazione con l’utente molto pesanti; inoltre una buona conoscenza del tipo di bytecode generato dal nostro sorgente java permette di ottenere classi piu’ compatte e piu’ efficienti.
Le note che seguono, che indirizzano nella ricerca di vie piu’ brevi per scrivere un algoritmo, tentano di minimizzare lo scarto tra le varie JVM, cioe’ sono sistemi che danno miglioramenti di performance su tutte le implementazioni di una JVM, e non solo su una particolare; ovviamente esistono particolari ottimizzazioni specifiche per ciascuna implementazione e release della JVM, ma l’idea di fondo che mi ha spinto nella mia ricerca era quella di ottenere un buon bilanciamento globale delle prestazioni.
Possiamo suddividere i tipi di ottimizzazioni possibili in 3 categorie:
  • A- vincoli temporali intrinseci alla Java VM
  • B- vincoli temporali dovuti ai metodi nativi
  • C- garbage collector (“lo spazzino”)

 
 

 Vincoli temporali intrinseci alla Java VM

Uno studio approfondito della Java VM e di come i vari compilatori java trasformano un sorgente in bytecode, permettono di ricavare le seguenti informazioni:
-Se si ha a che fare con un loop molto pesante in termini di tempo, conviene osservare la presenza di eventuali termini costanti e tirarli fuori dal loop. Purtroppo i compilatori java attuali non fanno questa semplice ottimizzazione supportata invece dalla quasi totalita’ di compilatori C/C++
Es:
 for (int i=0; i<12;i++){
   pippo[i] = a*b*c;
}
risulta piu’ veloce se fatta cosi’:
 int d = a*b*c;
 for (int i=0; i<12;i++){
   pippo[i] =d;
}
Durante i calcoli conviene osservare la presenza di parti uguali in modo da non ripeterli. Ad esempio 
 
 int e= a*b*(c*d/2);
 int f= c*d/2;

 diventa:

 int f=c*d/2;
 int e=a*b*f;


Le due seguenti operazioni anche se identiche come risultato, sono diverse come codice:
 

 a[i] = a[i] + 4;
 e
 a[i] += 4;


la seconda e’ piu’ veloce e piu’ corta.
A causa della struttura della java VM le prime 4 variabili di un metodo (3 se il metodo non e’ di tipo statico) sono richiamate piu’ velocemente. I metodi dichiarati final sono chiamati piu’ velocemente degli altri. I metodi synchronized sono i piu’ lenti in assoluto ( causa ovviamente del check sui monitors). 
Tipi supportati: la java VM supporta per i calcoli matematici solo 4 tipi: int, float, double, long e per gli array supporta solamente il tipo int.
Se vengono usati byte oppure short, questi vengono convertiti internamente in int e poi riconvertiti in byte o short. Conviene usare gli int. Comunque questo e’ variabile dal jit usato (comunque gli int sono sempre i piu’ veloci). 
 
 
 

Vincoli temporali dovuti ai metodi nativi

Purtroppo per noi maniaci della velocita’, le classi native fornite dalla Sun o da altri produttori di JVM non sono assolutamente lo stato dell’arte dell’ottimizzazione ed il risultato e’ che spesso conviene riscriversi le funzioni critiche perche’ quelle fornite dal java, sebbene native, sono piu’ lente. Un esempio sono gli stream di dati, (DataInputStream e DataOutputStream), che sebbene una delle parti teoricamente piu’ utili, sono anche una delle parti piu’ lente di tutte. 
Un’altra parte notevolmente lenta e’ il supporto delle aree grafiche e delle funzioni matematiche: L’imageProducer si e’ rivelato essere una delle parti piu’ lente di tutte le librerie fornite con il java, ma purtroppo non puo’ essere scavalcato; in compenso, una parte che spesso puo’ essere critica, puo’ essere velocizzata con piccoli accorgimenti. Sto parlando delle funzioni matematiche del java, ed in particolare della libraria matematica del java, che gestisce unicamente dati di tipo double (i piu’ lenti in assoluto....). Se dobbiamo usare funzioni quali la radice etc, purtroppo abbiamo poco da fare; nel migliore dei casi conviene all’inizio del programma precalcolarsi una tabella gia’ convertita, magari, in float, da usare per i nostri usi: supponiamo ad esempio di avere a che fare con funzioni trigonometriche (come in una routine per ruotare punti) e immaginiamo di usare molto spesso la funzione Math.cos(angle); la prima cosa che si puo’ osservare e’ che tale funzione e’ periodica, e quindi in realta’ ci interessano solamente i valori di angle compresi nell’intervallo [0,2pi); a questo punto possiamo decidere di suddividere questo intervallo in una quantita’ fissata a priori (ad esempio 32768 valori) e creare un array di float contenente i giusti valori:

 

float a[] = new float[32768];
for (int i=0; i<32768; i++){
  a[i] = Math.cos(i*2*Math.PI/32768);
}


ora, supponendo di utilizzare al posto dei radianti la nostra nuova unita’ di misura (che varia da 0 a 32767), possiamo ricavare il coseno con un unico lookup da tabella.
Grazie alle tabelle e’ possibile fare molti tipi di ottimizzazioni, che incrementano, spesso notevolmente, i calcoli matematici.
Ma cosa succede quando abbiamo troppi calcoli in float, e desideriamo una velocita’ maggiore a discapito di un po’ di precisione ?!?
Possiamo usare una tecnica chiamata Fixed Int: si tratta di usare un int opportunamente shiftato dove i 16 bit piu’ significativi sono usati come parte intera, e quelli meno significativi come parte decimale.
Ad esempio, le seguenti assegnazioni
 

int a = 1<<16;
int b = (int)(1.25*65536);


assegnano 1 ad a, nel nuovo formato, e trasformano un float (1.25) nel nuovo formato; dopo cio’ e’ possibile fare calcoli con i nostri numeri (tenendo conto delle ovvie limitazioni di scala) e alla fine
 

Float c = b/65536.0;
in modo da riconvertire in float (supposto che ci servano i float). Oppure utilizzare il risultato di tipo int con un semplice shift. Questo tipo di rappresentazione e’ molto utile anche per altri motivi: ad esempio e’ molto semplice fare il modulo di un numero (basta un masheramento con un and) ed implementare le funzioni di ceiling e di floor (molto utili nel campo della grafica tridimensionale in realtime). Un’altra classe fonte di lentezze inaspettate, e’ la classe String; di regola non andrebbe usata molto, visto che ogni operazione fatta su una String genera sempre nuovi oggetti, e solitamente si tende a fare molte operazioni sulle String, con conseguente generazione di migliaia di oggetti, spesso senza neanche accorgersene; se ad esempio ci servono molte concatenazioni e’ meglio utilizzare la classe StringBuffer.
 
 
 

 Infine  lo spazzino..

Il garbage collector fornito dal java e’ decisamente comodo e potente poiche’ ci libera completamente dalla necessita’ di tracciare la memoria disponibile, risolvendo molti problemi legati alla memoria che si incontrano quando si programma in C/C++ (e che sono fonte di innumerevoli bugs); ed e’ proprio questa una delle cause principali di rallentamenti quando abbiamo bisogno di codice veloce.. Purtroppo non e’ possibile determinare a priori quando il garbage collector entrera’ in azione, e se in quel momento stavamo disegnando un nuovo schermo, il risultato sara’ quello di una pausa (molto fastidiosa) tra un frame ed il successivo di una animazione. Ma questo e’ un problema che puo’ essere aggirato in un modo molto semplice: utilizzare il methodo gc() della classe System.
In linea di massima bisogna operare in questo modo:
  • preallocare tutti gli oggetti o array  che saranno necessari in un loop che vogliamo sia veloce (purtroppo ogni new, soprattutto nelle vecchie JVM, richiede parecchio tempo e quindi e’ meglio tenerlo fuori).
  • forzare la JVM ad eseguire il garbage collector tramite il comando System.gc() in punti non critici del codice. Ad esempio, dopo avere allocato una grossa struttura di dati, si puo’ chiamare subito il System.gc() (E volendo prima di questo si puo’ anche chiamare il runFinalization() che fa parte della classe Runtime) in modo da essere sicuri che java abbia liberato tutta la spazzatura generata dal processo di costruzione.
Tutte queste note, vanno ovviamente presi come consigli per ottimizzare il codice, e non e’ detto che saranno sempre corretti: magari saranno, anzi ,dannosi per hotspot o futuri jit. Quel che e’ certo e’ che al momento la stragrande maggioranza del pianeta usa ancora java1.0 e 1.1, o comunque jit della vecchia generazione, e questi sono validi sistemi per rubare un po’ di tempo macchina alla jvm.

 
 

MokaByte rivista web su Java

MokaByte ricerca nuovi collaboratori
Chi volesse mettersi in contatto con noi può farlo scrivendo a mokainfo@mokabyte.it