MokaByte Numero 19 - Maggio 1998

 
PERC,
 la via "dura" per Java real-time
di
Piergiuseppe
Spinelli
Nell'affollato panorama dei produttori di sistemi embedded interessati a Java, il PERC non usa mezze misure: Pure real-time in Pure Java

 

Nell'articolo pubblicato sul Mokabyte di aprile(1) abbiamo effettuato una panoramica sulle tante iniziative che ruotano attorno all'applicazione di Java ai sistemi real-time ed embedded.
Questo mese cerchiamo di approfondire le caratteristiche del PERC, un prodotto che si rivela particolarmente interessante, non tanto per la sua posizione di mercato o la sua attuale diffusione, quanto, una volta tanto, per le soluzioni tecniche e l'impostazione teorica volta all'applicazione in settori come l'aereonautica o il nucleare dove la rispondenza o meno a criteri rigorosamente real-time si misura in termini di vite umane e salvagurdia dell'ambiente più che in semplici montagne di dollari!
Un ringraziamento a Kelvin Nielsen per averci permesso di utilizzare materiale tratto direttamente dalla documentazione ufficiale del sito NewMonics.
 
 

Origini del PERC
Il papà del PERC, Kelvin Nilsen, spiega in questo modo la ragion d'essere del suo prodotto sin dall'inizio del progetto: "Dal momento che molte delle applicazioni che Java è inteso servire hanno caratteristiche emddeded real-time, abbiamo recentemente intrapreso lo sviluppo di un insieme di estensioni standard per fornire i programmatori Java dell'abilità di descrivere i requisiti real-time nelle loro applicazioni Java. Le estensioni standard sono incorporate nel PERC, un prodotto commerciale recentemente presentato, che porta le capacità di Java nell'arena dei sistemi embedded real-time".(2)

Nell'affrontare il compito di creare un Java real-time, bisogna affrontare due principali categorie di problemi:

Questi temi sono approfonditi nella documentazione originale del PERC(2), ma è possibile accedere ad una trattazione assai più informale e divulgativa nel mio articolo dello scorso mese.(1). Da queste fonti è anche possibile approfondire i motivi per cui, comunque, vale la pena di cercare di applicare Java al mercato embedded.

In questa sede riassumiamo i punti salienti per cui una comune implementazione Java, anche costruita su un sistema operativo RT (RTOS), fallisce nel rispettare i vincoli richiesti da applicazione effettivamente real-time:

Oltre alle suelencate problematiche di tipo teorico, esistono una serie di altri problemi dovuti alle correnti implementazioni delle JVM che non sono ottimizzate per l'esecuzione in spazi ridotti, usando per esempio 32 bit anche per rappresentare il tipo byte. E' inoltre evidente che la tecnologia su cui punta la SUN per ottenere performance professionali sui normali computer, ovvero la compilazione dinamica, non è assolutamente compatibile con i sistemi RT, non solo a causa dei tempi di compilazione attivati in modo asincrono, ma anche perchè i calcoli sui tempi d'esecuzione del codice JVM vengono stravolti non appena qualche routine venga arbitrariamente compilata dal JIT (comunque la NewMonics sta lavorando su un JIT che consenta di mantenere il controllo dei tempi d'esecuzione dei vari metodi).
 
 

La via "dura" del PERC
Per conservare tutti i vantaggi di Java e mantenere il pieno rispetto dei vincoli RT, il PERC, al contrario di molti altri prodotti che si affacciano sul mercato, non sceglie la strada dell'adattamento o dell'implementazione ad hoc dell JVM. La scelta radicale è, invece, quella di introdurre alcuni nuovi costrutti nel linguaggio, come vedremo in seguito, modificare le politiche di scheduling e la JVM in modo da gestire le informazioni addizionali necessarie ad un sistema RT ed aggiungere nei file di classe, che del resto già prevedono un meccanismo per l'estensione, tutti i dati già computabili staticamente in fase di compilazione. L'uso delle estensioni viene semplificato da specifiche librerie di classi.
Il risultato è, a mio avviso, sorprendente: la PVM (PERC Virtual Machine) conserva la piena compatibilità con il codice Java e con il formato standard dei file di classe. E' quindi in grado di eseguire, nel tempo residuo di CPU lasciato dai task RT, qualsiasi programma Java standard. Ma ancora di più, i programmi PERC sono eseguibili dalle normali JVM perdendo unicamente le caratteristiche real-time. In effetti è anche difficile distinguere un programma scritto in PERC da un normale programma Java, se non fosse per un paio di costrutti addizionali, il che apre potenzialmente le porte di nuovi campi d'applicazione anche a programmatori provenienti da settori dell'informatica da tavolo, e questo è un aspetto da non sottovalutare in un mercato che richiede una sempre più stretta integrazione tra i programmi manageriali, i sistemi di progettazione e quelli di controllo di processo.
Il PERC è attualmente un prodotto commerciale distribuito dalla NewMonics Inc.
 
 

Un po' di teoria
Il mese scorso ho introdotto il modelo di computazione real-time su cui si fonda il PERC(1), adesso daremo uno sguardo, non troppo formale, ai fondamenti teorici su cui il linguaggio è stato costuito, sempre prendendo come riferimento il lavoro di Nielsen(2).

Uno dei concetti fondamentali è il rate-monotonic scheduling, ovvero il meccanismo che consente di determinare in fase di sviluppo se un insieme di task RT verrà eseguita entro un tempo determinato. La tecnica utilizzata prevede la computazione, per ogni thread, del tempo massimo d'esecuzione (wrost execution time) e della sua massima frequenza d'esecuzione. La priorità di un task risulta direttamente proporzionale alla sua frequenza d'esecuzione. Traduco testualmente (nei limiti del mio pessimo inglese) un esempio fatto da Nielsen in (2) per spiegare il calcolo della percentuale d'occupazione della CPU da parte di un determinato task RT:

"Sia Ci il tempo di computazione del task i e sia Ti il periodo minimo d'esecuzione del task i. Per esempio, il task 1 e responsabile di visualizzare i frame aggiornati a 20 frames al secondo ed ogni aggiornamento richiede 10 ms di tempo CPU, quindi C1 è 10 ms e T1 è (1/20) s = 50 ms. Notare che questo task utilizza 1/5=20% del tempo totale della CPU di sistema. L'utilizzazione totale, Utotal, di un sistema di n task real-time è data da

(...) il limite d'utilizzazione UB(n) per questo insieme di n task real-time è dato da:



per n grandi, UB(n) approssima ln 2, che è circa 69%. Fin tanto che Utotal < UB(n), ogni task completerà l'esecuzione prima del prossimo periodo in cui è richiesta l'esecuzione."

Nell'esempio non sono previsti blochi di sincronizzazione per dati condivisi. Per questi casi vengono attuate analisi più complesse. Un punto da tener presente in ogni sistema che compia delle analisi al volo della schedulabilità, è che il peso computazionale dell'analisi stessa venga minimizzato. Il tipo di analisi quì riportato ha un peso relativamente basso e, soprattutto, presenta un andamento lineare al crescere del numero dei task.
Come ormai ogni lettore immaginerà, un altro concetto fondamentale del PERC è la realizzazione di un real time garbage collector che assicuri la disponibilità di memoria ai vari task senza interferire con i vincoli real-time di qusti ultimi. Esitono principalmente due modi in cui un GC può rompere le uova nel paniere ad un sistema RT: ritardando, o dando in tempi non deterministici, la memoria richiesta e fermando in modo asincrono l'intero sistema, a seguito di un'allocazione fallita, per ristitemare la memoria frammentata. Secondo Nielsen, la caratteristica principale di un RT-GC è quella di progredire in modo incrementale dividendo il lavoro totale in piccoli intervalli di tempo che vengano schedulati dal real-time kernel alla pari degli altri task RT e, quindi, assicurando che il suo peso computazionale (in particolare la percentuale di tempo di CPU occupata) sia conosciuto dal sistema. Resta il problema di assicurare che l'avanzamento del lavoro del GC sia sufficiente a garantire il fabbisogno di memoria dell'intero sistema. Per capire come si regola il PERC ricorrerò ancora una volta ad un esempio preso, per gentile concessione di K. Nielsen, dalla documentazione del PERC(2):

"Supponiamo, ad esempio, che la memoria totale disponibole sia M bytes e sia conosciuto il tempo S di CPU necessario a compiere una completa garbage collection. Supponiamo inoltre che il fabbisogno totale di memoria delle attivita real-time del sistema sia U bytes in totale e che il througput combinato sia di V bytes totali allocati al secondo. Infine, sia R la frazione di tempo di CPU dedicata al garbage collector. Si noti che il tempo reale richiesto per completare la garbage collection incrementale è S / R. Si consideri lo stato di memoria immediatamente seguente il completamento della garbage collection. Nel peggior stato stabile ci sono U bytes di memoria viva e V (S / R) bytes di memoria morta occupante al momento la heap. Se iniziamo il prossimo passo di garbage collection appena è stato completato il precedente, una quantità addizionale di V (S / R) bytes di memoria sarà allocata mentre questo passo di garbage collection è in esecuzione. Di conseguenza, l'ampiezza dello spazio richiesto per sopportare questo carico di lavoro, M, misurato in bytes, deve essere maggiore o uguale a U + 2V(S/R). In base ai fabbisogni combinati di memoria e alla massima frequenza d'allocazione sopra descritta, la frazione minima di tempo di CPU che deve essere trascorso in garbage collection e dato da:

Si noti che R è proporzionale alla massima frequenza alla quale la memoria è allocata moltiplicata per il tempo richiesto per eseguire un passo stop-and-wait di garbage collection. R è inversamente proporzionale alla differenza tra la quantità di memoria disponibile e la massima quantità di memoria viva."

Per una discussione più dettagliata dell'argomento vedere (4) e (5). Ciò che invece interessa noi è la possibilità, avendo a disposizione le necessarie informazioni, di computare in modo deterministico il peso del GC nel sistema e, di conseguenza, poter mantenere una programmazione Pure Java anche nell'ambito dei sistemi più strettamente real-time piuttosto che rinunciare del tutto al GC, come fanno alcuni dei sistemi che intendono essere Java RT.

Vorrei comunque fare un'annotazione personale. L'indirizzo preso dagli ingegneri coinvolti nell'evoluzione di Java è tale da rendere effettivamente difficile il costruire sistemi che non degradino le proprie prestazioni nel tempo a causa dell'utilizzo sovrabbondante dell'allocazione di memoria. Un porgramma Java, attualmente, crea allegramente oggetti in continuazione, anche quando in altri linguaggi ci si limiterebbe a modificare il contenuto degli oggetti già esistenti. Un esempio tra tutti: la serializzazione. Per propria specifica, il meccanismo di serializzazione non memorizza in uno stream più di una copia dello stesso oggetto, anche se il contenuto dell'oggetto stesso cambia mentre lo stream è aperto. In effetti il motore di serializzazione tiene presente solo il reference (o puntatore) che identifica l'oggetto. Se consideriamo che la serializzazione è il meccanismo di base per comunicare tra sistemi remoti, ad esempio via RMI, ci troviamo nell'obbligo di creare un nuovo oggetto ogni volta che i dati da esso contenuti devono essere scambiati con un altro device. In altre parole, il seguente codice non funziona:
 
 

class Message implements Serializable{
        int code;
        long[] data;
}
classe Test{
        ...
        ObjectOutputStream oos;
        ...
        Message mess=new Message();
        while(true){
                int c=waitForDataChanged();
                mess.code = c;
                mess.data = getDataFromBlock(c);
                oos.writeObject(mess); //I dati inviati saranno sempre uguali a quelli spediti la prima volta
        }
        ...
}
La versione funzionante, invece, costringe alla continua creazione di nuovi oggetti di tipo Message:
class Message implements Serializable{
        int code;
        long[] data;
}
classe Test{
        ...
        ObjectOutputStream oos;
        ...
        while(true){
                int c=waitForDataChanged();
                Message mess=new Message();
                mess.code = c;
                mess.data = getDataFromBlock(c);
                oos.writeObject(mess);
        }
        ...
}
Mi sono imbattuto in questo problema dovendo spedire i dati collezionati, più o meno in real-time, da un programma di controllo di processo a sistemi di monitoraggio remoto. Ho cercato nella bug parade della Java Developper Connection qualche notizia in merito.Ebbene, ho scoperto di non essere l'unico ad ever avuto il medesimo problema ma, con mia grande sorpresa, il punto era considerato chiuso da SunSoft e liquidato come normale comportamento della serializzazione. Questa e molte altre impostazioni del JDK indicano una precisa volontà di utilizzare al massimo l'allocazione dinamica assistita dal GC, anche se questo finisce per essere uno dei motivi principali di degrado delle prestazioni e, chiaramente, va in direzione contraria degli sforzi che la SUN sta compiendo per rendere Java un'alternativa credibile per la programmazione di sistemi embedded.
 
 

Componenti del PERC
In primo luogo il PERC fornisce una sua libreria standard per aggevolare lo sviluppo di sistemi RT. Parti principali di tale libreria sono:

Come già detto, il PERC introduce alcune estensioni alla sintassi standard del linguaggio:         x = computeApproximation();
        i = 0;
        timed (10 ms) {
                for ( ; ; ) {
                        z = refineApproximation(x);
                        atomic {
                                x = z;
                                i++;
                        }
                }
        }
 

Architettura e sistema di sviluppo
 


Per gentile concessione di K. Nielsen, NewMonics Inc.
I componenti principali del sistema di siviluppo del linguaggio PERC sono i seguenti: p2jpp, un preprocessore che converte il codice sorgente PERC in normale codice Java compilabile con javac. In particolare p2jpp traduce i costrutti aggiuntivi timed ed atomic simulandoli, entro certi limiti, con i normali costrutti Java. Il Percolator, invece, sostituisce javac compilando il codice PERC direttamente in Annotated JavaByte code, completamente compatibile con il Jcode normale ma con informazioni aggiuntive sulla richiesta di risorse registrate direttamente nel file di classe. Si noti la completa compatibilta dell codice prodotto dal Percolator con le normali JVM, pur rinunciando all'esecuzione real-time. L'ultimo componente del PERC e la PVM (PERC VM), prodotta in proprio dalla NewMonics, ditta fondata da Nielsen, senza partire dalla licenza originale SUN.
 Sono interessanti le annotazioni riportate nella presentazione del prodotto rispetto l'esecuzione del codice PERC sulle normali JVM. Principalmente vengono perse le seguenti caratteristiche: Nondimeno il codice PERC mantiene alcune caratteristiche utili anche se eseguito su JVM classiche:
 

picoPERC
Il PERC, con la sua piena compatibilità con Java, è sicuramente pensato per sistemi di dimensioni medio-grandi. La NewMonic sta elaborando lo standard picoPERC per piccoli sistemi embedded seguendo una strada minimalista più vicina a quella di altri produttori. In particolare il garbage-collector viene sostituito con funzioni di disallocazione esplicita della memoria. Il picoPERC sarà commercializzato entro l'anno.
 
 
 

Conclusioni
Tirando le somme, il PERC dimostra che oggi esiste almeno un'alternativa per sviluppare sistemi strettamente real-time in Java.

Rimangono tuttavia una serie di questioni, principalmente legate alle performances, che sono strutturali del linguaggio e non potranno essere risolte con compilatori più o meno dinamici. Ad esempio il meccanismo di chiamata ai metodi, a causa dell'implementazione dell'ereditarietà e del polimorfismo, è decisamente più lento dell'invocazione di una funzione C. Gli attuali garbage-collector, RT o meno, degradano sensibilmente le performances generali rispetto all'uso esplicito di malloc/free. Anche la memoria minima richiesta tende a essere maggiore dei minimi irrisori necessari per far girare programmi embedded scritti in C o assembler.

In conclusione, i sistemi real-time scritti in PERC (o comunque in Java) avranno bisogno di processori mediamente più veloci e di maggiori risorse. Questo ci porta a valutare l'introduzione di Java nel campo dei piccoli dispositivi (quelli per i piccoli elettrodomestici, ad esempio) un cammino in salita. Al contrario, i vantaggi in termini di economia di progetto e di manutenzione, nonchè di sicurezza di funzionamento, che si potranno ottenere adottando Java, magari in varianti come il PERC, su sistemi medio-grandi saranno tali che possiamo fin d'ora prevedere una rapida diffuzione del linguaggio nell'industria magari fino a soppiantare di fatto alcuni dei sistemi tradizionali.
 

Risorse

  1. Spinelli, P. Quattro chiacchiere su Java ed i sistemi embedded. Mokabyte, Aprile 1998.
  2. http://www.newmonics.com/WebRoot/technologies/PERC/escw.html
  3. Nilsen, K. Progress in Hardware-Assisted Real-Time Garbage Collection. in Lecture Notes in Computer Science #986. 1995. Kinross, Scotland: Springer-Verlag.
  4. Nilsen, K., Reliable Real-Time Garbage Collection of C++. Computing Systems, 1994. 7(4): p. 467-504.
  5. Nilsen, K., Real-Time Java (v. 1.1). 1996, Iowa State University, Ames, Iowa.
  6. Nilsen, K., Issues in the Design and Implementation of Real-Time Java. 1995, Iowa State University: Ames, Iowa.
  7. Carli, M, Garbage Collector in Java. Mokabyte, Marzo 1998.

Una mini-ricerca sull'allocazione tipizzata
Mi sono trovato più volte a dovere affrontare, tentando di dare ai programmi una veste seppur lascamente real-time, il problema del surplus di oggetti allocati, in particolare di quelli che si possono definire oggetti consumabili, caratterizzati da un'alta frequenza di creazione e da un ciclo di vita brevissimo: quel tanto che basta a trasmettere un'informazione da un thread ad un altro (magari remoto) o a fungere da wrapper per dati base da passare come argomenti a metodi che si aspettano un tipo reference. Il peggior effetto di questo abuso dell'allocazione è il degrado delle prestazioni con l'avanzare, nel tempo, della frammentazione della memoria che richiede frequenti stop-and-wait del programma per consentire al GC di ricreare uno stato gestibile della heap(7).

In questi casi, ove possibile, ho cercato di ammortizzare il numero di allocazioni effettive (quelle fatte con new e poi abbondanate al tenero abbraccio del garbage collector). Una tecnica che a volte riscuote qualche risultato consiste nel riservare, staticamente o dinamicamente, un pool di oggetti preallocati appartenenti alla stessa classe. L'applicazione di tale tecnica è sottoposta a severi vincoli, esistendo situazioni di totale inapplicabilità ed altre dove essa può compromettere la sicurezza del sistema. Inoltre è neccesario, caso per caso, valutare l'overcharge di usare routine ad alto livello di allocazione e disallocazione nei confronti della rapidissima new (rapidissima, certo, a patto di trovare memoria contigua immediatamente disponibile). Altro fattore da considerare è che la ritenzione di memoria usata solo parzialmente può incidere sull'economia generale, specialmente per programmi di grosse dimensioni.

Fatte tutte queste precisazioni, i dati forniti dalle piccole prove riportate in questo articolo rivelano effetti interessanti sull'ottimizzazione dell'uso del tempo e della memoria e, a puro scopo didattico, suggeriscono come le tecniche di allocazione tipizzata, a cui si è fatto cenno nell'aricolo sul PERC, implementate a livello di JVM, possano effettivamente impattare sulla predicibilità delle prestazioni del garbage collector.

Partiamo con il definire una classe astratta per gli oggetti passibili di allocazione tipizzata:

package PJSoft.TypedPool;

public abstract class Allocable{
        Allocable next=null;
        protected Allocable(){}
}

In questo caso ho preferito utilizzare una lista collegata piuttosto che degli array di reference che potrebbero fornire prestazioni superiori. Questo per semplicità di implementazione e per poter focalizzare queste poche righe d'esposizione sul nucleo della tecnica piuttosto che su lunghi dettagli implementativi. L'uso di una classe astratta e di un costruttore protected è già di per sé un vincolo introdotto che impedisce l'uso di oggetti non appositamente pensati per questo tipo di utilizzo.

Vediamo ora la classe creata per gestire il pool di oggetti preallocati appartenenti ad una stessa classe derivata a Allocable:

package PJSoft.TypedPool;
import java.util.*;

public final class TypedPool{
        private volatile Allocable first=null;
        private Class c;
        private int lMax=50;
        private int lCnt=0;

        TypedPool(Class c, int lMax){
                this.c=c;
                this.lMax=lMax;
        }

        public synchronized Allocable obtainObject(){
                Allocable a=null;
                if(first==null){
                        try{
                                a =(Allocable)c.newInstance();
                        }catch(Exception e){
                                e.printStackTrace();
                                System.exit(1);
                        }
                }else{
                        a = first;
                        first = first.next;
                        a.next = null;
                        lCnt--;
                }
                return a;
        }

        public synchronized void releaseObject(Allocable obj) throws TypedPoolCastException {
                if(!(obj==null || c.isInstance(obj))) throw(new TypedPoolCastException ());
                if(lCnt>=lMax) return;
                obj.next = first;
                first = obj;
                lCnt++;
        }

        void setMax(int lMax){this.lMax = lMax;}

        public int getCount(){return lCnt;}

}

Uno dei limiti dell'uso di una lista collegata, oltre alla maggiore occupazione di memoria, è la difficoltà di rilasciare la memoria eventualmente allocata oltre certi limiti (ad esempio per un picco di eventi) quando questa si riveli superflua per il fabbisogno medio del sistema. In effetti aggiungere un contatore e percorrere parte della lista per rilasciare gli oggetti uno ad uno potrebbe essere molto penalizzante. In questo caso ho scelto di inserire un limite lMax, impostabile per ogni singolo pool, oltre il quale gli oggetti restituiti tramite releaseObject non vengono reinseriti nella lista.

Ora che abbiamo visto la classe pool, è più semplice inquadrare alcuni dei limiti della tecnica descritta:

  1. E' necessaria la piena collaborazione tra tutti i thread che entrano in possesso di un riferimento ad un oggetto di tipo Allocable onde assicurare che nessuno di essi sia ancora in uso al momento della restituzione dell'oggetto stesso al suo TypedPool. In questo senso si torna alla situazione del buon vecchio linguaggio C.
  2. Le classi derivate da Allocable non possono implementare l'interfaccia Serializable in quanto, come visto in precedenza, la serializzazione tiene in conto solo i riferimenti e non i contenuti degli oggetti. Quindi, un oggetto ripescato più volte dal pool, pur contendendo nuovi dati, non viene inserito in uno stream più di una volta.
A controllare che le classi per cui si desidera creare un pool corrispondano a certe caratteristiche, viene fornita la classe TypedPoolFactory:
package PJSoft.TypedPool;
import java.util.*;

public final class TypedPoolFactory {
        private static Hashtable pools=new Hashtable();

        private TypedPoolFactory(){}

        public static synchronized TypedPool obtainPool(Class c, int lMax) throws TypedPoolCastException {
                Class sc=c.getSuperclass();
                while(sc!=null && !sc.getName().equals("PJSoft.TypedPool.Allocable")){
                        sc=sc.getSuperclass();
                }
                if(sc==null) throw(new TypedPoolCastException ());
                Class is[] = c.getInterfaces();
                for(int i=0;i<is.length;i++){
                        if(is[i].getName().equals("Serializable")) throw(new TypedPoolCastException ());
                }
                TypedPool pool=(TypedPool)pools.get(c.getName());
                if(pool==null){
                        pool=new TypedPool(c, lMax);
                        pools.put(c.getName(), pool);
                }else{
                        pool.setMax(lMax);
                }
                return pool;
        }

        public static synchronized void deletePool(Class c){
                try{pools.remove(c.getName());}catch(Exception e){}
        }

}

Ecco un programma di test che ci permette di paragonare l'andamento nel tempo delle perfomances, in termini di numero di oggetti allocati a parità di tempo, nelle seguenti situazioni: Il programma, dal nome fantasioso Test, accetta i seguenti parametri: numero di task (per ognuno dei due tipi), grana di campionamento dei dati da rilevare, numero di iterazioni, flag sul tipo di allocazione (NEW, POOL, BOTH) ed infine un flag sull'attivazione di un thread contatore.

La possibilità di azionare un thread counter che si limita ad incrementare una variabile e ad autosospendersi immediatamente con Thread.yield(), serve a dare un ulteriore vista neutra sulla ripartizione globale del carico del sistema.

Tutti i thread girano a priorità normale su un pentium 200 con WinNT Server, 64 Mb di ram e JDK 1.2 Beta2.

Vediamo i risultati:

Numero di oggetti allocati 10 thread utilizzanti new 10 thread utilizzanti un TypedPool 5 thread utilizzanti new e
5 thread utilizzanti un TypedPool
10 sec. X 10 iterazioni [0] new: 12300 
[1] new: 24700 
[2] new: 37050 
[3] new: 49400 
[4] new: 61800 
[5] new: 74250 
[6] new: 86700 
[7] new: 99100 
[8] new: 111450 
[9] new: 123800 
Total: [123801]
[0] Pool: 21100 
[1] Pool: 42250 
[2] Pool: 58200 
[3] Pool: 74100 
[4] Pool: 90000 
[5] Pool: 105900 
[6] Pool: 121800 
[7] Pool: 137700 
[8] Pool: 153600 
[9] Pool: 169500 
Total: [169501] 
Elements in the pool: 51
[0] new: 6850 Pool: 6950 
[1] new: 13800 Pool: 13950 
[2] new: 20750 Pool: 20850 
[3] new: 27600 Pool: 27700 
[4] new: 34450 Pool: 34550 
[5] new: 41300 Pool: 41400 
[6] new: 48200 Pool: 48300 
[7] new: 55050 Pool: 55150 
[8] new: 61900 Pool: 62000 
[9] new: 68750 Pool: 68900 
New: [68801] - Pool 1: [68901] - Total: [137702] 
Elements in the pool: 51
10 sec. X 10 iterazioni 
con counter
[0] new: 12450 
[1] new: 24600 
[2] new: 36850 
[3] new: 49100 
[4] new: 61400 
[5] new: 73700 
[6] new: 86000 
[7] new: 98300 
[8] new: 110600 
[9] new: 122900 
Counter=37497 
Total 0: [123001]
[0] Pool: 15899 
[1] Pool: 31499 
[2] Pool: 47149 
[3] Pool: 62749 
[4] Pool: 78349 
[5] Pool: 93949 
[6] Pool: 109549 
[7] Pool: 125149 
[8] Pool: 146649 
[9] Pool: 162249 
Counter=1763945 
Total 1: [165900] 
Elements in the pool: 51
[0] new: 6750 Pool: 6800 
[1] new: 13600 Pool: 13700 
[2] new: 20550 Pool: 20600 
[3] new: 27400 Pool: 27450 
[4] new: 34250 Pool: 34300 
[5] new: 41150 Pool: 41250 
[6] new: 48150 Pool: 48200 
[7] new: 55100 Pool: 55200 
[8] new: 62100 Pool: 62200 
[9] new: 69050 Pool: 69150 
Counter=1385 
New: [69251] - Pool: [69301] - Total [138552] 
Elements in the pool: 51
 Difficile trarre conclusioni certe, ma alcune tendenze sono interessanti: In alcuni casi, piuttosto che soluzioni arzigogolate come un TypedPool, sono sufficienti modifiche di piccola entità per abbattere l'impatto del garbage collector sulle performance di un programma. Ad esempio ho visto migliorare in modo ravvisabile "ad occhio nudo" le prestazioni di un thread che creava continuamente oggetti serializzabili per spedirli ad un sistema remoto tramite un ObjectOutputStream. La modifica è consistita nell'allocare un unico oggetto di una classe (non serializzabile) contenente un metodo apposito per spedire i propri fields tramite un normale DataOutputStream e nel modificare il contenuto dei campi piuttosto che allocare continuamente nuove istanze della classe. Provare per credere...

Concludendo: se i nostri programmi hanno qualche vincolo di tempo, è preferibile non crogiolarsi nell'illusione della memoria autogestita dal sistema, cercando di ottimizzare gli oggetti consumabili in modo da minimizzare l'impegno del garbage collector.


Piergiuseppe Spinelli svolge attività di analista/programmatore dal 1980. Si è occupato di training e di sviluppo di sistemi, particolarmente nel campo della supervisione di processo. Ha lavorato per aziende dei gruppi Saint Gobaing, Angelini, Procter&Gamble, Alcatel/Telettra, SIV e per vari enti pubblici e privati. E contattabile all'indirizzo spinellip@sgol.it o al sito www.GeoCities.com/Eureka/Enterprises/9607.
 
 
 
  

 

MokaByte rivista web su Java

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