MokaByte
Numero 19 - Maggio 1998
|
|||
|
la via "dura" per Java real-time |
||
Piergiuseppe Spinelli |
|
||
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:
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:
L'implementazione
del garbage-collector(7), tanto caro ad
ogni programmatore Java, costituisce uno dei problemi principali nel controllo
dei tempi e della disponibilità di risorse. Il GC di Java risente
dei seguenti problemi:
la memoria
viene allocata e rilasciata in modo non tipizzato e, inoltre, non esiste
alcun meccanismo di recupero degli oggetti "vivi" dalla memoria
rilasciata. Questo provoca una progressiva frammentazione della memoria
che, da un lato, tende a degradare nel tempo le prestazioni di un'applicazione
Java e, dall'altro, richiede periodiche interruzioni del normale flusso
d'esecuzione dei programmi per ricompattare la memoria in modo da ottenere
spazio contiguo sufficiente a soddisfare le successive richieste di allocazione.
In effetti, così come la politica di schedulazione della CPU, il
GC di Java è pensato nella direzione di ottimizzare la condivisione
delle risorse da parte dei vari task così che, anche se la memoria
totale del sistema è inferiore alla somma delle massime richieste
di ogni thread, l'esecuzione di questi può di norma proseguire in
quanto tali richieste massime non avengono contemporaneamente. In caso
di situazioni critiche, l'esecuzione di qualche task viene penalizzata
in quanto l'istruzione new può fallire lanciando un'eccezione
che, eventualmente, può venire catturata per implementare un meccanismo
di attesa fino a che la memoria neccessaria non si renda disponibile. Tutto
ciò e semplicemente l'opposto dei requisiti di un programma RT.
Tanto è vero che, spesso, i sistemi real-time classici non prevedono
l'allocazione dinamica della memoria obbligando i vari task ad allocare
a priori tutte le risorse necessarie per ogni successivo stato d'esecuzione,
con uno spreco sull'economia generale durante l'esecuzione media
ma
con la garanzia di avere sempre a disposizione tutto il necessario per
un'esecuzione effettivamente real-time. In fondo a questo
articolo vedremo una piccola ricerca che ho svolto per evidenziare
vantaggi e svantaggi di una gestione tipizzata della memoria.
Per un thread
non c'è modo di sapere di quanta memoria ha bisogno, quanta è
richesta da tutti gli altri thread ospitati dal sistema e quanta memoria
abbia a disposizione l'intera JVM. La reliability d'esecuzione diventa
quindi una vera e propria scommessa.
Non esistono
metodi per stabilire i budget di memoria necessari per l'esecuzione di
ogni attività. Questo è vero sia in compilazione che a tempo
d'esecuzione. Questo non consente politiche di configurazione per
contratto,
ovvero dove il sistema garantisce ad un'oggetto allocato la disponibilità
piena della sua massima richiesta. Inoltre, è evidente, un qualsiasi
task può candidamente richiedere tutte le risorse di memoria del
sistema lasciando gli altri all'asciutto!
Un sistema
real-time ha bisogno di conoscere a priori per quanto tempo ogni task manterrà
il lock su ogni singola risorsa. Il sistema si sincronizzazione tipo monitor
di Java non fornisce informazioni sufficienti per questo tipo di analisi.
In particolare mancano dati su:
Le informazioni
sulle necessita di tempo e memoria dei vari thread non sono note a priori
(ad esempio registrate nei file di classe). Di conseguenza:
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{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.
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);
}
...
}
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:
Include meccanismi
per consentire l'analisi e la misura dei tempi richiesti per l'esecuzione
di particolari segmenti di codice e della memoria richiesta per i vari
oggetti. Fornisce inoltre un'astrazione per l'accesso agli oggetti persistenti
in memoria flash o tamponata. Le applicazioni real time sono composte da
diverse attività ognuna comprendente uno o più task
RT (1). A sua volta ogni task comprende
componenti essenziali ed opzionali, a seconda della definizione data dal
programmatore.
Prima di
poter essere eseguita, una attività deve prima essere presentata
al sistema. Il real-time executive chiama a turno il metodo configure()
di ogni attività per determinare il fabbisogno di risorse di tutti
i task componenti l'attività RT, in termini di quantità minime
e desiderate.
Ottenute
le informazioni necessarie dal metodo configure(), il sistema tenta di
soddisfare le richieste di risorse. Esso propone ad ogni attività
RT un budget di risorse invocando il metodo
negotiate(). Il budget
proposto dal sistema può essere inferiore a quello richiesto dall'attività
che, a questo punto, ha la scelta tra accettare o rifiutare il budget proposto.
In caso di rifiuto, il real-time executive può decidere di non caricare
l'attività o, puttosto, di esigere risorse precedentemente allocate
per altre attività, sempre tramite la negoziazione. Se il sistema
riesce a raccattare nuove risorse, propone un nuovo budget all'attività
in attesa, continuando così la negoziazione.
Il PERC fornisce
un meccanismo di sincronizzazione dei task RT alternativo ai monitor di
Java, introducendo la nuova keywod
atomic. Il codice racchiuso in
un blocco atomic è eseguito senza interruzioni (preemption) o fino
al completamento o per niente. L'implementazione dei blocchi atomici può
variare dalla semplice disabilitazione degli interrupts a meccanismi raffinbati
d'analisi, per i sistemi RT spinti. In questo caso il sistema verifica
che il tempo di CPU rimanente nella fetta di tempo corrente del task sia
sufficiente al completamento del blocco atomic.
Altra keyword
aggiuntiva del PERC è timed che dichiara il limite massimo
di tempo entro il quale debe essere eseguito un blocco di codice. In caso
di sforamento del tetto stabilito, il sistema lancia una eccezione
di timeout. Ecco un esempio tratto dalla documentazione del PERC(2):
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. |
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
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;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.public abstract class Allocable{
Allocable next=null;
protected Allocable(){}
}
Vediamo ora la classe creata per gestire il pool di oggetti preallocati appartenenti ad una stessa classe derivata a Allocable:
package PJSoft.TypedPool;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.
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;}
}
Ora che abbiamo visto la classe pool, è più semplice inquadrare alcuni dei limiti della tecnica descritta:
package PJSoft.TypedPool;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:
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){}
}}
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 |
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 ricerca
nuovi collaboratori
|
||
|