Home > Apple & Macintosh, Objective-C > Snow Leopard: che novità per gli sviluppatori?

Snow Leopard: che novità per gli sviluppatori?

Adesso che Snow Leopard è disponibile ho potuto finalmente toccare con pelle la nuova release. Ma ancora più interessante è stato leggere la documentazione delle nuove API. Oltre alle due tecnologie maggiormente pubblicizzate, Grand Central Dispatch e OpenCL, le novità per gli sviluppatori sono molte e quasi tutte volte al miglioramento delle prestazioni delle applicazioni. Immagino che l’utilizzo pervasivo di queste nuove API all’interno del sistema stesso abbia contribuito al miglioramento delle performance di cui tanto si parla. In questo “brain-dump” trovate una panoramica sia di GCD che di OpenCL, ma anche di quelle piccole aggiunte alla API che mi hanno colpito.

Grand Central Dispatch

La novità principale di SL è forse proprio GCD, una nuova API per sfruttare in modo efficiente i vari core della CPU. Alla base di questa nuova API ci sono i blocchi, un nuovo elemento del linguaggio C (e dei derivati C++ e Objective-C) introdotto da Apple in tutti i compilatori supportati da OS X (GCC e Clang). Si tratta, in pratica, dell’astrazione “lambda” dei linguaggi funzionali. I blocchi sono delle porzioni di codice anonimo generato a runtime che può essere passato come parametro o restituito come valore di funzioni. Con i blocchi, in pratica, il C ora supporta funzioni di ordine superiore. Facciamo un esempio:

void (^blocco) = ^{
   codice;
   codice;
};

La sintassi è simile a quella dei puntatori di funzioni. Se all’interno del blocco compaiono variabili esterne, il valore viene congelato al momento della dichiarazione del blocco. Una volta dichiarato il blocco può essere richiamato normalmente:

blocco();

Cosa ce ne facciamo dei blocchi? GCD permette di eseguire i blocchi in vari modi, aggiungendoli a delle code. I blocchi vengono eseguiti nell’ordine in cui sono stati messi in coda. Esistono code seriali e code parallele. Le code seriali eseguono sempre un blocco alla volta. Le code parallele potrebbero eseguire più blocchi in contemporanea. La parola chiave qui è potrebbero. GCD gestisce un thread pool che viene usato per eseguire i vari blocchi, ma sceglie quanti thread usare per ogni processo a seconda del carico del sistema e del numero di core disponibili, in modo da rendere l’esecuzione più efficiente possibile. Nella pratica si possono vedere alcuni esempi. Se abbiamo un ciclo di questo tipo:

for(int i = 0; i < 20; i++) {
   work(i);
}

Possiamo parallelizzarlo molto facilmente:

dispatch_apply(20, dispatch_get_global_queue(0,0), ^(int i){
   work(i);
});

La funzione dispatch_apply accoda in questo caso 20 volte lo stesso blocco, passando ogni volta un indice diverso. Con questo codice, sappiamo che verranno eseguiti contemporaneamente tanti blocchi quanti necessari per assicurare la massima efficienza.

Un altro esempio riguarda la reattività delle interfaccie grafiche. Se abbiamo un codice che potrebbe richiedere del tempo per essere eseguito, va scritto in modo asincrono, ma mentre prima dovevamo gestire thread e messaggi manualmente, adesso bastano poche righe, per esempio il metodo:

- (void) buttonClicked: (NSButton *)button
{
   int result = work(textField.intValue);
   textField.intValue = result;
}

diventa:

- (void) buttonClicked: (NSButton *)button
{
   dispatch_async(dispatch_get_global_queue(0,0), ^{
      int result = work(textField.intValue);
      dispatch_async(dispatch_get_main_queue(), ^{
         textField.intValue = result;
      });
   });
}

Ho colorato le righe aggiunte. La chiamata più “esterna” aggiunge un blocco ad una coda parallela e ritorna immediatamente, lasciando quindi il runloop principale libero in modo da evitare di bloccare la GUI. Quando il blocco viene eseguito fa quello che deve fare e poi aggiorna la GUI con il risultato. Ovviamente questo non può essere fatto da un thread diverso dal main(), quindi viene aggiunto un altro blocco alla main queue, che è una coda seriale che esegue blocchi sempre sul main thread. In realtà, in questo modo si aggiunge quel blocco al runloop principale. I runloop di Cocoa e di Core Foundation in SL sono implementati con GCD. Usando Grand Central pervasivamente in tutto il sistema, Apple ha ottenuto un livello di reattività veramente impressionante.

OpenCL

OpenCL è un framework e un linguaggio che permette di utilizzare la GPU dei computer come unità di computazione generica. Anche qui il parallelismo gioca un ruolo fondamentale. Le GPU sono delle unità di calcolo fortemente parallele, molto più delle CPU centrali. Se la CPU di un desktop può avere 2 o 4 core, una moderna GPU può avere decine di unità di calcolo indipendenti. Per questo, usando la GPU per il calcolo generico si possono ottenere vantaggi significativi, se il nostro codice deve essere eseguito parecchie volte su grandi set di dati, per esempio nella manipolazione di immagini, video, audio, ecc… Ma qualcuno di voi avrà già sentito questa storia. Non sembra uguale a CUDA e ad altre tecnologie di questo tipo? La computazione generica sulle GPU non l’ha certo inventata Apple, ma OpenCL ha qualche marcia in più:

  • E’ standard. Linguaggio e API sono stati ratificati dal Khronos Group, lo stesso che gestisce gli standard di OpenGL e OpenAL.
  • E’ indipendente dalla GPU. Su Mac OS X infatti sono attualmente supportati da OpenCL sia GPU nVidia sia ATI. Su ogni modello è garantito lo stesso livello di accuratezza matematica.
  • E’ multipiattaforma. Nonostante attualmente l’unica implementazione disponibile sia quella di OS X, il fatto che si tratti di uno standard industriale farà nascere a breve implementazioni anche per altre piattaforme.
  • Supporta tipi eterogenei di dispositivi. L’astrazione fornita da OpenCL non riguarda solo le GPU. Lo stesso codice può funzionare anche sulle normali CPU, e anche su acceleratori dedicati (come le unità vettoriali del Cell).

Proprio sull’ultimo punto vorrei insistere. Sebbene in rete molti si lamentino del numero esiguo di GPU supportate, chi ha una GPU diversa non deve sentirsi escluso. Un programma che usa OpenCL sceglie a runtime su che dispositivo eseguire il codice. Se nessuna GPU compatibile è disponibile, il codice può venire eseguito sulla CPU. Questa astrazione ulteriore, sebbene sembri inutile (tanto valeva scrivere il codice direttamente per la CPU, no?), fornisce dei vantaggi. Il linguaggio OpenCL, infatti, derivato dal C, fornisce dei tipi di dati in più, adatti a lavorare con dati vettoriali. In questo modo, il compilatore può generare codice che sfrutti i set di istruzioni vettoriali come SSE e compagnia, aumentando le prestazioni rispetto al normale codice scritto in un linguaggio generico.

LLVM

Con Mac OS X 10.6, LLVM diventa la scelta raccomandata per la compilazione dei progetti C e Objective-C. Su Snow Leopard, i Developer Tools installano 4 compilatori: gcc 4.0, gcc 4.2, gcc-llvm e clang. I primi due sono i classici compilatori GNU che sono stati sempre usati. Il terzo è il front-end di gcc abbinato al backend di llvm, mentre l’ultimo è un compilatore completamente riscritto su llvm. Nonostante gcc resti la scelta di default, clang è raccomandato per vari motivi:

  • Performance. Clang è molto più veloce di gcc durante la compilazione. Per compilare un mio progetto di 20 file sorgente, gcc impiega 20 secondi, mentre clang ci mette solo 2-3 secondi!! Inoltre sembra che produca eseguibili più veloci grazie a migliori tecniche di ottimizzazione, ma non ho modo di confermarlo direttamente..
  • Migliore gestione degli errori. Avete mai visto uno di quegli indecifrabili errori di GCC? Clang riporta gli errori in modo veramente molto più leggibile. Inoltre l’output prodotto in caso di errore fornisce più informazioni del solo numero di riga, comprendendo anche quale simbolo o espressione ha generato l’errore.
  • Static Analyzer. Clang fornisce un analizzatore statico molto utile per trovare possibili bug nel codice. Non si tratta di semplici warning, ma di un analisi sulla possibile esecuzione del codice.

Per fare un esempio dell’output dello Static Analyzer vi propongo uno screenshot:

CLang Analyzer output

CLang Analyzer output

Come vedete ha riconosciuto due possibili errori. Il primo è un memory-leak di un oggetto Objective-C, mentre l’altro è un return di una variabile non inizializzata. Entrambi sono errori che si sarebbero manifestati a runtime. Ma la cosa più sorprendente è l’integrazione dell’analyzer con l’IDE, se clicko sull’icona blu a forma di freccietta, infatti ottengo qualcosa di simile:

Navigazione interattiva dell'output di CLang

Navigazione interattiva dell'output di CLang

Il memory leak viene suddiviso in due step. Il primo è l’allocazione, e il secondo è il return dalla funzione senza la chiamata a release. Come vedete non solo posso navigare tra i due step, ma le freccie mi indicano anche il path di esecuzione che porta all’errore. Una cosa simile avviene per il secondo errore. Questo static analyzer sarà uno strumento utilissimo per il debugging, e probabilmente il basso numero di bug della nuova release di OS X, confrontati con le altre release 10.x.0, è dovuto all’uso intensivo di questo tool.

API varie ed eventuali

Sulla pagina di Apple dedicata a SL, viene citato il taglio del tempo necessario a fare lo shutdown. Sembra una cosa veniale, ma è proprio vero: il mio macbook pro si spegne in 2 secondi contro i 5-6 di prima. Che sia una cosa importante o no, viene da chiedersi da dove venga questo miglioramento. Di solito durante la procedura di logout, il sistema chiede ad ogni applicazione il permesso di chiudersi, dando la possibilità di chiedere all’utente di salvare il lavoro, dopodiche, a tutti i processi in esecuzione viene spedito un SIGTERM, un segnale che può essere gestito dai processi che vogliono chiudersi in modo “regolare”. Dopodichè, tutti gli altri processi vengono chiusi con un SIGKILL, che invece è immediato. SL salta tutto questo e spedisce direttamente un SIGKILL a tutti i processi che non hanno bisogno di fare niente in fase di chiusura. Come fa a saperlo? Tutti i processi che hanno chiamato il metodo -[NSProcessInfo enableSuddenTermination], o un equivalente funzione C di basso livello, vengono chiusi con un SIGKILL diretto. Esiste anche un metodo disableSuddenTermination, che ha l’effetto opposto. Aldilà dell’utilità di queste funzioni per la singola applicazione, è evidente che la riduzione drastica del tempo di shutdown è dovuta all’uso intensivo di questa feature in tutti i demoni di sistema.

Un’altra API molto utile per le performance è la classe NSCache. Si tratta di una specie di dizionario che, in pratica, distrugge automaticamente il proprio contenuto se il sistema va a corto di memoria. Può venire usato quando dei dati vengono tenuti in memoria per questioni di velocità, ma che possono essere ricavati di nuovo in qualsiasi momento. Mantenendoli in memoria, si toglie risorse ad altre applicazioni, ma con questo sistema di cache automatico si può usare quanta memoria si vuole per le proprie cache.

Per ultimo, voglio citare le nuove API per la gestione dei file basate su URL. Ora un URL con schema file:// permette di accedere a qualsiasi metadato riguardante il file, senza dover usare API di livello più basso come lstat() e simili.

A me sembra di aver detto tutto. La pagina What’s New in Mac OS X della sezione sviluppatori del sito di Apple elenca molte più cose, ma queste sono quelle che sono sembrate a me le più interessanti.

Bye bye.

Apple & Macintosh, Objective-C

  1. Gabriele
    9 settembre 2009 a 21:22 | #1

    Ottimo articolo. Sintetico quanto basta ma completo.
    Non mi pento affatto di avere il tuo blog tra i miei feed ;)
    Grazie

  1. Nessun trackback ancora...