Migliorare le prestazioni alloc/init
Spesso si tende a fare un uso eccessivo della programmazione orientata agli oggetti cadendo più o meno volontariamente nell’errore di trascurare tutto un insieme di overhead che accumulandosi, strato su strato, possono appesantire notevolmente l’esecuzione del nostro codice.
Col passare del tempo e col crescere della potenza di calcolo a disposizione, questo approccio alla programmazione è stato perfino incoraggiato portanto come giustificazione quella che un codice lento (sulle CPU) oggi sarà comunque migliore (su quelle di) domani (tengo a sottolineare che una citazione del genere è venuta fuori durante un corso universitario).
Va comunque da se che l’ottimizzazione non deve essere ne il primo passo della stesura di un programma, ne quello fondamentale: in moltissimi programmi questo overhead potrebbe risultare trascurabile o perfino ignorabile a fronte di una migliore mantenibilità del codice (in genere è preferibile partire con un codice funzionante e stabile e solo successivamente migliorarne le prestazioni in esecuzione: ecco qui una buona lettura in merito).
Ci sono però dei casi in cui ottimizzare il proprio codice venendo a patti con l’eleganza OOP può farvi guadagnare preziosi secondi. Spesso si tratta perfino di cambiamenti “minori” al codice: usare stringhe C anzichè NSString può velocizzare un codice anche di un +700% (senza nessuna esagerazione).
In questo articolo andremo a verificare come migliorare le prestazioni sull’allocazione di un elevato numero di classi NSObject.
Al fine di avere una corretta comprensione di quanto verrà descritto è necessario avere in mente il funzionamento, anche di base, che sta dietro l’allocazione di un oggetto (in sostanza alloc/init/autorelease;
qualche tempo fa abbiamo ne abbiamo parlato ampiamente: ecco il link all’articolo).
Un paio di trucchi
Il primo consiglio è quello di evitare, quando possibile autorelease. Poniamo il caso di dover creare un oggetto e di doverlo immediatamente aggiungere ad un array. Il codice in questione potrebbe essere il seguente:
NSMutableArray *lista = [[NSMutableArray alloc] init];
NSString *stringa;
stringa = [NSString stringWithString: @"Ciao Mondo!"];
[lista addObject: obj];
una versione più efficiente potrebbe essere:
NSMutableArray *lista = [[NSMutableArray alloc] init];
NSString *stringa;
stringa = [[NSString alloc] initWithString:@"Ciao Mondo!"];
[lista addObject: stringa];
[stringa release];
Benchè possa sembrare maggiore il carico (ci sono 2 chiamate in più rispetto alla prima versione) il lavoro sotto il cofano è molto minore; generalmente si guadagna +40% in più di tempo.
Un altro miglioramento possiamo ottenerlo evitando la chiamata alloc. In effetti alloc su un oggetto non fa altro che chiamare allocWithZone:.
La funzione in questione infatti è composta in maniera molto schematica come:
+ (void) alloc {
return( [self allocWithZone:NULL]);
}
Quindi chiamando allocWithZone:NULL anzichè alloc otterremo un altro (minimo in questo caso) vantaggio.
Pool di oggetti
Un’altra idea per migliorare le prestazioni è quella di mantenere gli oggetti deallocati in un pool a parte e riutilizzarli in caso di necessità: nel momento in cui ci sarà bisogno di una nuova istanza basterà infatti prenderne uno dal pool e associargli lo stato richiesto. In questo modo ci risparmieremo, ove possibile, il costo di una chiamata malloc (che in C è una delle operazioni più sostanziose).
Vediamo allora un esempio.
Il pool viene realizzato con un buffer circolare di grandezza fissa. Se il buffer è vuoto allora gli oggetti richiesti vengono allocati nella maniera classica; se il buffer è pieno la memoria occupata dagli oggetti viene realmente liberata al sistema. Durante l’utilizzo normale invece gli oggetti vengono riutilizzati ‘pescandoli’ dal pool e rilasciati inserendoli nel pool.
Ecco l’implementazione reale:
#import
// Questa è il nostro Pool. Ci sarà un pool per ogni tipo di oggetto.
// Nel nostro esempio prendiamo in considerazione una generica classe PoolObject
// adattata per fare uso del pool.
typedef struct {
Class poolClass; // contiene il genere di classe che gestisce il pool
unsigned int low;
unsigned int high;
unsigned int poolSize; // dimensione del pool
id *pool;
} Pool;
// Ecco un oggetto PoolObject che usa il Pool durante per restituire le istanze
// di se stesso.
@interface PoolObject : NSObject {
}
+ (Pool *) instancePool;
@end
#import "PoolObject.h"
#import "objc/objc.h"
#import "objc/objc-class.h"
#define kPoolSize 1000; // dimensione del buffer di pool
static NSString *UNHANDLED_POOL_FROM_CLASS = @"La classe %@ deve implementare il metodo + (Pool *) instancePool";
// Ecco come dovrebbe apparire una subclass di PoolObject:
/* + (Pool *) instancePool {
static Pool myPool;
return( &myPool);
}";
*/
@implementation PoolObject
// allocazione del buffer di pool
+ (id) allocWithZone:(NSZone *) zone {
Pool *pool;
id obj;
pool = [self instancePool]; // ottiene il pool relativo alla classe
if( ! pool->poolClass) { // ... primo call. Dobbiamo allocare la struttura del pool
pool->poolClass = self;
pool->poolSize = kPoolSize;
// allochiamo lo spazio necessario a contenere kPoolSize oggetti del tipo in questione
pool->pool = malloc( sizeof( id) * pool->poolSize);
} else // ... chiamata successiva alla allocazione
if( pool->poolClass != self) // non si tratta di una classe che può gestire il pool
[NSException raise:NSGenericException UNHANDLED_POOL_FROM_CLASS, self];
// se il pool è vuoto dobbiamo necessariamente allocare l'oggetto richiesto
if( pool->low == pool->high) {
obj = NSAllocateObject( self, 0, NULL);
return( obj);
}
// altrimenti possiamo riutilizzarne uno (lo facciamo e lo rimuoviamo dal pool)
obj = pool->pool[ pool->low];
pool->low = (pool->low + 1) % pool->poolSize;
memset( obj + 1, 0, ((struct objc_class *) self)->instance_size - 4);
return obj;
}
- (void) dealloc {
// La deallocazione di un oggetto porta allo spostamento dello stesso
// all'interno del nostro pool, in attesa di poter essere nuovamente
// riutilizzato.
Pool *pool;
unsigned int next;
pool = [isa instancePool];
next = (pool->high + 1) % pool->poolSize;
// ...ops...il pool è pieno
if( next == pool->low) {
NSDeallocateObject( self);
return;
}
// aggiungo l'oggetto al pool, sarà a disposizione dei prossimi 'alloc'
pool->pool[ pool->high] = self;
pool->high = next;
}
+ (Pool *) instancePool {
[NSException raise:NSGenericException UNHANDLED_POOL_FROM_CLASS, self];
return( 0);
}
@end
A questo punto una generica sottoclasse di PoolObject non dovrà fare altro che implementare il metodo +istancePool:
+ (Pool *) instancePool {
static Pool myPool;
return( &myPool);
}
Una struttura del genere può far guadagnare anche il 34% di velocità rispetto ad una classica struttura a malloc; il pool funziona a partire dal primo dealloc di un oggetto (il numero delle allocazioni diminuisce mano mano che il pool incomincia ad essere esausto); il contro è dato da un consumo di memoria che è sicuramente molto maggiore (è necessario che gli oggetti siano ancora disponibili per riempire il buffer prima di essere effettivamente rimossi dalla memoria).