Migliorare le prestazioni alloc/init

Programmazione, Snippets and Tutorials on December 23rd, 2009 No Comments

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).

Tags: , ,

No Responses to “Migliorare le prestazioni alloc/init”

Leave a Reply