Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Elaborazione dei dati: Parallelismo e prestazioni
Johnson M. Hart
L'elaborazione di raccolte di dati è un'attività informatica fondamentale con una vasta gamma di problemi pratici che ne scaturiscono in parallelo e che comporta potenzialmente migliori prestazioni e produttività nei sistemi multicore. Metterò a confronto diversi metodi distinti basati su Windows per risolvere problemi a elevato grado di parallelismo dei dati.
Il benchmark che utilizzerò per questo confronto è un problema di ricerca ("Geonames") tratto dal Capitolo 9 del libro di Troy Magennis intitolato "LINQ to Objects Using C# 4.0" (Addison-Wesley, 2010). Soluzioni alternative:
- PLINQ (Parallel Language Integrated Query) e C# 4.0 con e senza modifiche al codice originale.
- Codice nativo di Windows basato in cui viene utilizzato il linguaggio C, l'API di Windows, thread e file mappati alla memoria.
- Codice multithreading C#/Microsoft .NET Framework di Windows.
Il codice sorgente di tutte le soluzioni è disponibile sul sito mio Web (jmhartsoftware.com). Altre tecniche di parallelismo, quale la libreria TPL (Task Parallel Library) di Windows, non vengono esaminate direttamente benché PLINQ sia basato sulla libreria TPL.
Confronto e valutazione di soluzioni alternative
I criteri di valutazione della soluzione, in ordine di importanza, sono i seguenti:
- Prestazioni totali in termini di tempo impiegato per completare l'attività.
- Scalabilità con il grado di parallelismo (numero di attività), numero di core processore e dimensione delle raccolte dei dati.
- Semplicità, eleganza, facilità di gestione e fattori intangibili simili associati al codice.
Riepilogo dei risultati
In questo articolo verrà illustrato il concetto in base a un problema di ricerca di un benchmark rappresentativo:
- È possibile sfruttare con successo sistemi a 64 bit multicore per migliorare prestazioni in molti problemi di elaborazione dei dati e PLINQ può essere incluso nella soluzione.
- Prestazioni PLINQ competitive e scalabili richiedono oggetti per la raccolta dei dati indicizzati e il supporto dell'interfaccia IEnumerable non è sufficiente.
- Le soluzioni C#/.NET e in codice nativo sono le più rapide.
- La soluzione PLINQ originale è più lenta in base a un fattore prossimo a 10 e non offre scalabilità tra due attività, mentre le altre soluzioni garantiscono una scalabilità verticale fino a sei attività con sei core (situazione massima verificata). Tuttavia, le modifiche al codice migliorano la soluzione originale in modo significativo.
- Il codice PLINQ è il più semplice ed elegante sotto tutti i punti di vista poiché LINQ fornisce la capacità di query dichiarative per dati residenti in memoria ed esterni. Il codice nativo è pesante, mentre il codice C#/.NET è considerevolmente migliore del codice PLINQ ma non altrettanto semplice.
- Tutti i metodi offrono una scalabilità verticale in termini di dimensione dei file fino ai limiti di memoria fisica del sistema di test.
Il problema del benchmark: Geonames (nomi geografici)
L'idea del presente articolo deriva dal Capitolo 9 del libro di Magennis su LINQ, in cui viene illustrata la tecnologia PLINQ mediante l'esecuzione di ricerche in un database geografico contenente oltre 7,25 milioni di nomi di luoghi in un file di 825 MB (ovvero oltre una località ogni 1000 persone). Ciascun nome di luogo è rappresentato da un record riga di testo in formato UTF-8 (en.wikipedia.org/wiki/UTF-8) (a lunghezza variabile) con oltre 15 colonne di dati delimitati da tabulazioni. Nota: la codifica UTF-8 garantisce che non venga generato un valore di tabulazione (0x9) o avanzamento riga (0xA) nell'ambito di una sequenza multibyte; ciò è essenziale per diverse implementazioni.
Il programma Geonames di Magennis implementa una query hardcoded per identificare tutte le località con un'altitudine (colonna 15) superiore a 8.000 metri, indicando il nome della località, il paese e l'altitudine in ordine decrescente. Qualora ve lo stiate chiedendo, esistono 16 località simili e il monte Everest è il più alto con 8.848 metri.
Per l'esecuzione dei report di Magennis sono stati impiegati 22,3 secondi (un core processore) e 14,1 secondi (due core processore). Precedenti esperienze (ad esempio, vedere il mio articolo intitolato "Windows Parallelism, Fast File Searching and Speculative Processing" disponibile all'indirizzo informit.com/articles/article.aspx?p=1606242) hanno dimostrato che file di questa dimensione possono essere elaborati in pochi secondi e le prestazioni aumentano in modo uniforme in base al numero di core processore. Pertanto, ho deciso di tentare di replicare tale esperienza e di modificare il codice PLINQ di Magennis per ottenere migliori prestazioni. Le modifiche iniziali al codice PLINQ hanno raddoppiato quasi le prestazioni ma non hanno migliorato la scalabilità; ulteriori modifiche, tuttavia, hanno comportato un livello di prestazioni quasi uguale al codice multithreading C# e al codice nativo.
Questo benchmark è interessante per diversi motivi:
- L'oggetto (luoghi e attributi geografici) è intrinsecamente interessante ed è semplice generalizzare la query.
- Esiste un elevato grado di parallelismo dei dati; in teoria, ciascun record può essere elaborato contemporaneamente.
- La dimensione del file è modesta per gli standard odierni, ma è semplice eseguire test con file di dimensioni maggiori concatenando più volte il file Geonames allCountries.txt con sé stesso.
- L'elaborazione non è senza stati; è necessario stabile i limiti di riga e di campo per poter partizionare il file ed è necessario elaborare le righe per identificare i singoli campi.
Presupposto: presupponiamo che il numero di record identificati (in questo esempio, località con altitudine superiore a 8.000 metri) sia ridotto, in modo tale che i tempi di ordinamento e visualizzazione siano minimi rispetto al tempo di elaborazione totale (con conseguente analisi di ogni singolo byte).
Altro presupposto: i risultati in termini di prestazioni rappresentato il tempo richiesto per elaborare raccolte di dati residenti in memoria, quali dati prodotti durante un passaggio precedente all'esecuzione di un programma. Il programma di benchmark legge il file, ma i programmi di test vengono eseguiti più volte per garantire che il file sia residente in memoria. Tuttavia, indicherò il tempo richiesto per caricare inizialmente il file che corrisponde approssimativamente allo stesso valore per tutte le soluzioni.
Confronto delle prestazioni
Il primo sistema di test è un sistema desktop a sei core su cui è in esecuzione Windows 7 (AMD Phenom II, 2,80 GHz, 4 GB di RAM). Successivamente, illustrerò i risultati per altri tre sistemi con funzionalità HT (Hyper-Threading) (en.wikipedia.org/wiki/Hyper-threading) e numero di core processore differente.
Nella Figura 1 vengono mostrati i risultati per sei soluzioni Geonames diverse con tempo di completamento (secondi) come funzione DoP (Degree of Parallelization), ovvero il numero di attività parallele che è possibile impostare su un valore superiore al numero di processori; il sistema di test dispone di sei core, ma le implementazioni controllano il DoP. Sei attività sono ottimali; l'utilizzo di un numero di attività maggiore di sei comporta un peggioramento delle prestazioni. Per tutti i testi viene utilizzato il file di dati allCountries.txt di 825 MB del programma Geonames.
Figura 1 Prestazioni del programma Geonames come funzione del DoP (Degree of Parallelism)
Di seguito le implementazioni (seguiranno spiegazioni complete):
- Geonames Original. Questa è la soluzione PLINQ originale di Magennis. Le prestazioni non sono competitive e non aumentano in base al numero di processori.
- Geonames Helper. Questa è una versione migliorata in termini di prestazioni di Geonames Original.
- Geonames MMChar. Si tratta di un tentativo non riuscito di migliorare Geonames Helper con una classe di file mappato alla memoria simile a quello utilizzato in Geonames Threads. Nota: La mappatura alla memoria consente di fare riferimento a un file come se fosse in memoria senza operazioni di I/O esplicite con potenziali vantaggi in termini di prestazioni.
- Geonames MMByte. Questa soluzione modifica l'implementazione MMChar per l'elaborazione di singoli byte del file di input, mentre le precedenti tre soluzioni consentono di convertire i caratteri dal formato UTF-8 al formato Unicode (2 byte alla volta). Le prestazioni di questa implementazione sono le migliori delle quattro soluzioni e oltre il doppio rispetto alla soluzione Geonames Original.
- Geonames Threads non utilizza il codice PLINQ. Questa è l'implementazione C#/.NET basata su thread e un file mappato alla memoria. Le prestazioni sono migliori rispetto alla soluzione Geonames Index (illustrata di seguito) e quasi uguali a quelle della soluzione Geonames Native. Questa soluzione e la soluzione Geonames Native offrono la migliore scalabilità in termini di parallelismo.
- Geonames Index. Questa soluzione PLINQ pre-elabora il file di dati (circa nove secondi) per creare un oggetto List<byte[]> residente in memoria per l'elaborazione successiva del codice PLINQ. Il costo di preelaborazione può essere ammortizzato con più query, con prestazioni appena inferiori alle soluzioni Geonames Native e Geonames Threads.
- In Geonames Native (non mostrato nella Figura 1) non viene utilizzato il codice PLINQ. Questa è l'implementazione dell'API di Windows C basata su thread e un file mappato alla memoria come illustrato nel Capitolo 10 del mio libro intitolato "Windows System Programming" (Addison-Wesley, 2010). Un'ottimizzazione completa del compilatore è essenziale per questi risultati; l'ottimizzazione predefinita garantisce solo un livello di prestazioni dimezzato.
Tutte le implementazioni sono build a 64 bit. Le implementazioni a 32 bit funzionano nella maggior parte dei casi, ma non per file di dimensioni maggiori (vedere la Figura 2). Nella Figura 2 vengono illustrate le prestazioni basate su DoP 4 e file di dimensioni maggiori.
Figura 2 Prestazioni del programma Geonames come funzione della dimensione del file
Il sistema di test, in questo caso, dispone di quattro core processore (AMD Phenom Quad-Core, 2,40 GHz, 8 GB di RAM). I file più grandi sono stati creati concatenando più copie del file originale Nella Figura 2 vengono mostrate solo le soluzioni più rapide, tra cui Geonames Index, la soluzione PLINQ più rapida (senza considerare la preelaborazione dei file) e scalabilità verticale in termini di prestazioni con una dimensione del file massima pari al limite della memoria fisica.
Ora descriverà le implementazioni da due a sette e illustrerò le tecniche PLINQ più in dettaglio. Successivamente, illustrerò i risultati su altri sistemi di test e riepilogherò i risultati.
Soluzioni PLINQ modificate: Geonames Helper
Nella Figura 3 viene mostrata la soluzione Geonames Helper con le mie modifiche (in grassetto) al codice della soluzione Geonames Original.
Figura 3 Geonames Helper con modifiche al codice PLINQ originale evidenziate
class Program
{
static void Main(string[] args)
{
const int nameColumn = 1;
const int countryColumn = 8;
const int elevationColumn = 15;
String inFile = "Data/AllCountries.txt";
if (args.Length >= 1) inFile = args[0];
int degreeOfParallelism = 1;
if (args.Length >= 2) degreeOfParallelism = int.Parse(args[1]);
Console.WriteLine("Geographical data file: {0}.
Degree of Parallelism: {1}.", inFile, degreeOfParallelism);
var lines = File.ReadLines(Path.Combine(
Environment.CurrentDirectory, inFile));
var q = from line in
lines.AsParallel().WithDegreeOfParallelism(degreeOfParallelism)
let elevation =
Helper.ExtractIntegerField(line, elevationColumn)
where elevation > 8000 // elevation in meters
orderby elevation descending
select new
{
elevation = elevation,
thisLine = line
};
foreach (var x in q)
{
if (x != null)
{
String[] fields = x.thisLine.Split(new char[] { '\t' });
Console.WriteLine("{0} ({1}m) - located in {2}",
fields[nameColumn], fields[elevationColumn],
fields[countryColumn]);
}
}
}
}
Poiché molti lettori potrebbero non avere familiarità con PLINQ e C# 4.0, includerò alcuni commenti sulla Figura 3, tra cui descrizioni delle modifiche:
- Le righe 9-14 consentono all'utente di specificare il nome del file di input e il grado di parallelismo (il numero massimo di attività contemporanee) sulla riga di comando; tali valori sono stati impostati come hardcoded nell'originale.
- Le righe 16-17 iniziano a leggere le righe del file in modo asincrono e implicitamente consentono di digitare le righe come matrice di stringhe C#. I valori di riga non vengono utilizzate fino alle righe 19-27. In altre soluzioni, quali Geonames MMByte, viene utilizzata una classe diverse con relativo metodo ReadLines e tali righe di codice sono le uniche da modificare.
- Le righe 19-27 sono codice LINQ associate all'estensione AsParallel PLINQ. Il codice è simile a SQL e la variabile "q" è tipizzata in modo implicito come matrice di oggetti costituiti da un elevamento a potenza e una stringa. Tenere presente che il codice PLINQ esegue tutte le attività di gestione thread; il metodo AsParallel rappresenta tutto il necessario per trasformare il codice LINQ seriale in codice PLINQ.
- Riga 20. Nella Figura 4 viene mostrato il metodo Helper.ExtractIntegerField. Nel programma originale viene utilizzato il metodo String.Split in un modo simile a quello utilizzato per visualizzare i risultati nella riga 33 (Figura 3). Questo è il fattore chiave per garantire migliori prestazioni della soluzione Geonames Helper rispetto alla soluzione Geonames Original, poiché non è più necessario allocare oggetti String per ciascun campo di ogni riga.
Figura 4 Classe Geonames Helper e metodo ExtractIntegerField
class Helper
{
public static int ExtractIntegerField(String line, int fieldNumber)
{
int value = 0, iField = 0;
byte digit;
// Skip to the specified field number and extract the decimal value.
foreach (char ch in line)
{
if (ch == '\t') { iField++; if (iField > fieldNumber) break; }
else
{
if (iField == fieldNumber)
{
digit = (byte)(ch - 0x30); // 0x30 is the character '0'
if (digit >= 0 && digit <= 9)
{ value = 10 * value + digit; }
else // Character not in [0-9]. Reset the value and quit.
{ value = 0; break; }
}
}
}
return value;
}
}
Tenere presente che il metodo AsParallel utilizzato nella riga 19 può essere utilizzato in qualsiasi oggetto IEnumerable. Come menzionato in precedenza, nella Figura 4 viene mostrato il metodo ExtractIntegerField della classe Helper, il quale consente di estrarre e valutare semplicemente il campo specificato (l'elevamento in questo caso), evitando i metodi di libreria per migliorare le prestazioni. Nella Figura 1 viene mostrato come tale modifica raddoppia le prestazioni con DoP 1.
Geonames MMChar e Geonames MMByte
Geonames MMChar rappresenta un tentativo non riuscito di migliorare le prestazioni mediante mappatura del file di input alla memoria, tramite una classe personalizzata denominata FileMmChar. Geonames MMByte, tuttavia, comporta vantaggi significativi poiché i byte del file di input non vengono estesi alla codifica Unicode.
Geonames MMChar richiede una nuova classe FileMmChar che supporta l'interfaccia IEnumerable<String>. La classe FileMmByte è simile ed è costituita da oggetti byte[] piuttosto che oggetti String. L'unica modifica significativa al codice è illustrata nella Figura 3, righe 16-17, ovvero:
var lines = FileMmByte.ReadLines(Path.Combine(
Environment.CurrentDirectory, inFile));
Il codice di
public static IEnumerable<byte[]> ReadLines(String path)
che supporta l'interfaccia IEnumerable<byte[]> in FileMmByte consente di costruire un oggetto FileMmByte e un oggetto IEnumerator<byte[]> che cerca singole righe nel file mappato.
Tenere presente che le classi FileMmChar e FileMmByte non sono "sicure" poiché consentono di creare e utilizzare puntatori per accedere ai file e utilizzano l'interoperabilità C#/del codice nativo. L'utilizzo di tutti i puntatori è, tuttavia, isolato in un assembly separato e nel codice vengono utilizzate matrici piuttosto la risoluzione dei riferimenti ai puntatori. La classe MemoryMappedFile di .NET Framework 4 non risulta utile, poiché è necessario utilizzare funzioni accessorie per spostare i dati dalla memoria mappata.
Geonames Native
Geonames Native sfrutta l'API di Windows, i thread e la mappatura dei file alla memoria. I modelli di codice di base sono descritti nel Capitolo 10 del libro intitolato "Windows System Programming". Il programma deve gestire i thread direttamente e deve inoltre rigorosamente mappare il file alla memoria. Le prestazioni sono molto migliori rispetto a tutte le implementazioni PLINQ eccetto Geonames Index.
Esiste, tuttavia, un'importante distinzione tra il problema Geonames e una ricerca o trasformazione di file senza stato e semplice. Il problema consiste nello stabilire il metodo corretto per partizionare i dati di input in modo da assegnare diverse partizioni ad attività differenti. Non esiste un modo diretto per stabilire i limiti di riga senza la scansione dell'intero file. Pertanto, non è possibile assegnare una partizione a dimensione fissa a ciascuna attività. Tuttavia, la soluzione è immediata quando viene illustrata con DoP 4:
- Suddividere il file di input in quattro partizioni uguali, con il percorso di inizio della partizione comunicato a ciascun thread all'interno dell'argomento della funzione thread.
- Faremo ora in modo che ciascun thread elabori tutte le righe (tutti i record) che iniziano nella partizione. Ciò significa che un thread analizzerà probabilmente la successiva partizione per poter completare l'elaborazione dell'ultima riga che ha inizio nella partizione.
Geonames Threads
In Geonames Threads viene utilizzata la stessa logica di Geonames Native; infatti, parte del codice è uguale o quasi uguale. Tuttavia, le espressioni lambda, i metodi di estensione, i contenitori e altre funzionalità C#/.NET semplificano la programmazione in modo significativo.
Come con le soluzioni MMByte e MMChar, la mappatura dei file alla memoria richiede classi "non sicure" e l'interoperabilità con il codice C#/codice nativo per poter utilizzare puntatori alla memoria mappata. Ne vale, tuttavia, la pena poiché le prestazioni di Geonames Threads sono uguali alle prestazioni di Geonames Native a fronte di un codice più semplice.
Geonames Index
I risultati del codice PLINQ (Original, Helper, MMChar e MMByte) sono deludenti rispetto ai risultati delle soluzioni Native e .NET Threads. Esiste un modo per sfruttare la semplicità e l'eleganza del codice PLINQ senza sacrificare le prestazioni?
Benché sia impossibile stabilire esattamente il modo in cui il codice PLINQ elabori la query (righe 16-27 nella Figura 3), è probabile che il codice PLINQ non includa un modo efficiente per partizionare le righe di input per consentirne l'elaborazione parallela da parte di attività separate. Come ipotesi di lavoro, presupponiamo che il partizionamento possa essere la causa del problema di prestazioni del codice PLINQ.
In base al libro di Magennis (pagine 276-279), la matrice String lines supporta l'interfaccia IEnumerable<String> (vedere anche il libro di John Sharp intitolato "Microsoft Visual C# 2010 Step by Step" [Microsoft Press, 2010], Capitolo 19). Tuttavia, la matrice lines non è indicizzata, quindi nel codice PLINQ viene probabilmente utilizzato un "partizionamento a blocchi". Inoltre, i metodi IEnumerator.MoveNext per le classi FileMmChar e FileMmByte sono lenti poiché devono analizzare ciascun carattere fino all'individuazione della nuova riga successiva.
Cosa accadrebbe se la matrice String lines fosse indicizzata? Potremmo migliorare le prestazioni del codice PLINQ, specialmente mediante integrazione della mappatura del file di input alla memoria? La soluzione Geonames Index dimostra come questa tecnica migliori le prestazioni, comportando risultati paragonabili al codice nativo. In generale, tuttavia, o esiste necessariamente un dispendio iniziale per trasferire le righe in un elenco o matrice in memoria indicizzato (il costo può essere ammortizzato su più query) oppure il file o un'altra origine dati è già indicizzata, probabilmente durante la generazione in un passaggio precedente del programma con conseguente eliminazione dell'onere di pre-elaborazione.
L'operazione di indicizzazione iniziale è semplice; viene eseguito l'accesso a ciascuna riga alla volta che verrà aggiunta a un elenco. Si consiglia di utilizzare l'oggetto list nelle righe 16-17, come illustrato nella Figura 3 e nel seguente frammento di codice in cui viene mostrata la pre-elaborazione:
// Preprocess the file to create a list of byte[] lines
List<byte[]> lineListByte = new List<byte[]>();
var lines =
FileMmByte.ReadLines(Path.Combine(Environment.CurrentDirectory, inFile));
// ... Multiple queries can use lineListByte
// ....
foreach (byte[] line in lines) { lineListByte.Add(line); }
// ....
var q = from line in lineListByte.AsParallel().
WithDegreeOfParallelism(degreeOfParallelism)
Tenere presente che è leggermente più efficiente elaborare i dati ulteriormente convertendo l'elenco in una matrice, benché tale operazione comporti un aumento dei tempi di elaborazione.
Un miglioramento finale delle prestazioni
Le prestazioni della soluzione Geonames Index possono essere migliorate ulteriormente mediante l'indicizzazione dei campi in ciascuna riga in modo tale che per il metodo ExtractIntegerField non sia necessaria l'analisi di tutti i caratteri di una riga nel campo specificato.
L'implementazione Geonames IndexFields comporta la modifica del metodo ReadLines in modo tale che una riga restituita corrisponda a un oggetto contenente sia una matrice byte[] che una matrice uint[] array contenente le posizioni di ciascun campo. Ciò comporta un miglioramento delle prestazioni pari a circa il 33% rispetto alla soluzione Geonames Index e avvicina molto le prestazioni a quelle delle soluzioni in codice nativo e C#/.NET. L'implementazione Geonames IndexFields è inclusa nel download del codice. Inoltre, è ora molto più semplice costruire query più generiche poiché i singoli campi sono facilmente accessibili.
Limitazioni
Tutte le soluzioni efficienti richiedono dati residenti in memoria e i vantaggi in termini di prestazioni non si estendono a raccolte di dati molto grandi. Per "molto grandi", in questo caso, si riferisce a dimensioni dei dati che si avvicinano alla dimensione della memoria fisica di sistema. Nell'esempio Geonames, è stato possibile elaborare il file di 3.302 MB (quattro copie del file originale) nel sistema di test con una memoria RAM di 8 GB. Tuttavia, il test con otto copie concatenate del file è risultato lento con tutte le soluzioni.
Come menzionato in precedenza, le prestazioni risulteranno migliori quando i file di dati sono "attivi", nel senso che hanno di recente subito un accesso e che probabilmente sono in memoria. Per il paging nel file di dati durante l'esecuzione iniziale possono occorrere 10 o più secondi ed è paragonabile all'operazione di indicizzazione nel frammento di codice precedente.
In conclusione, i risultati illustrati in questo articolo si applicano a strutture di dati residenti in memoria e le dimensioni e i prezzi della memoria di oggi consentono l'utilizzo di oggetti dati di dimensioni significative, ad esempio un file con 7,25 milioni di nomi di località residente in memoria.
Risultati su un altro sistema di test
Nella Figura 5 vengono illustrati i risultati del test su un altro sistema (Intel i7 860, 2,80 GHz, quattro core processore, otto thread, Windows 7, 4 GB di RAM). Il processore supporta la funzionalità HT (Hyper-Threading), pertanto i valori DoP testati sono 1, 2, ..., 8. L'illustrazione della Figura 1 è basata su un sistema di test con sei core processore AMD; il sistema non supporta la funzionalità HT.
Figura 5 Intel i7 860, 2,80 GHz, quattro core processore, otto thread, Windows 7, 4 GB di RAM
Altre due configurazioni di testi hanno prodotto risultati simili (i dati completi sono disponibili sul mio sito Web):
- Intel i7 720, 1,60GHz, quattro core processore, otto thread, Windows 7, 8 GB di RAM
- Intel i3 530, 2,93 GHz, due core processore, quattro thread, Windows XP 64 bit, 4 GB di RAM
Caratteristiche interessanti in termini di prestazioni:
- La soluzione Geonames Threads offre costantemente le migliori prestazioni insieme alla soluzione Geonames Native.
- La soluzione Geonames Index è la soluzione PLINQ più rapida che si avvicina alle prestazioni della soluzione Geonames Threads. Nota: l'implementazione GeonamesIndexFields è leggermente più rapida ma non viene riportata nella Figura 5*.*
- Fatta eccezione per la soluzione Geonames Index, tutte le soluzioni PLINQ presentano livelli di scalabilità negativi con il risultato DoP for DoP maggiore di due, ovvero le prestazioni diminuiscono all'aumentare del numero di attività parallele. In base a questo esempio, il codice PLINQ produce buone prestazioni solo se utilizzato con oggetti indicizzati.
- Il contributo in termini di prestazioni della funzionalità HT è trascurabile. Pertanto, le prestazioni delle soluzioni Geonames Threads e Geonames Index non aumentano significativamente per un valore DoP maggiore di quattro. Questa scarsa scalabilità della funzionalità HT potrebbe essere una conseguenza della pianificazione di due thread su processori logici dello stesso core, piuttosto che fare in modo che vengano eseguiti su core processore distinti quando possibile. I sistemi AMD senza funzionalità HT (Figura 1) hanno prodotto prestazioni comprese tra tre e quattro volte superiori al DoP maggiore di quattro, rispetto ai risultati del DoP 1 per le soluzioni Threads, Native e Index. Nella Figura 1 viene mostrato come le migliori prestazioni si verificano quando il valore DoP corrisponde al numero di core processore in cui le prestazioni multithreading sono 4,2 volte le prestazioni del DoP 1.
Riepilogo dei risultati
Il codice PLINQ offre un modello eccellente per l'elaborazione di strutture di dati in memoria ed è possibile migliorare le prestazioni del codice esistente con alcune semplice modifiche (ad esempio, come mostrato nella soluzione Helper) o con alcune tecniche più avanzate, come mostrato nella soluzione MMByte. Tuttavia, nessuna di queste semplice modifiche offrono prestazioni simili a quelle offerte dal codice nativo o da codice C#/.NET multithreading. Inoltre, la modifica non garantisce maggiori prestazioni all'aumentare dei core processore e del DoP.
Il codice PLINQ consente di avvicinarsi alle prestazioni del codice C#/.NET, ma richiede l'utilizzo di oggetti dati indicizzati.
Utilizzo del codice e dei dati
Tutto il codice è disponibile sul mio sito Web (jmhartsoftware.com/SequentialFileProcessingSupport.html), seguendo le istruzioni sotto riportate:
- Andare alla pagina per il download di un file ZIP contenente il codice PLINQ e il codice della soluzione Geonames Threads. Tutte le variazioni del codice PLINQ sono nel progetto GeonamesPLINQ (Visual Studio 2010; Visual Studio 2010 Express è sufficiente). Geonames Threads si trova nel progetto GeonamesThreads di Visual Studio 2010. I progetti sono entrambi configurati per versioni finali a 64 bit. Il file ZIP contiene anche un foglio di calcolo con i dati non elaborati sulle prestazioni nelle Figure 1, 2 e 5. Con un semplice commento di utilizzo nella parte iniziale del file viene spiegata un'opzione della riga di comando per la selezione del file di input, del DoP e dell'implementazione.
- Andare alla pagina di supporto relativa al libro intitolato "Windows System Programming" (jmhartsoftware.com/comments_updates.html) per scaricare il codice e i progetti della soluzione Geonames Native (in un file ZIP) in cui sarà incluso il progetto Geonames. Nel file ReadMe.txt viene spiegata la struttura.
- Scaricare il database del programma GeoNames dall'indirizzo download.geonames.org/export/dump/allCountries.zip.
Domande inesplorate
In questo articolo sono state paragonate le prestazioni di diverse tecniche alternative per risolvere lo stesso problema. L'approccio consisteva nell'utilizzo di interfacce standard e di un semplice modello a memoria condivisa per i processori e i thread. Non abbiamo, tuttavia, sostanzialmente analizzato in modo approfondito le implementazioni sottostanti o le funzionalità specifiche della macchine di test e in futuro sarà possibile approfondirne diversi aspetti. Ecco alcuni esempi:
- Qual è l'effetto di mancati riscontri nella cache ed esiste un metodo per ridurre le conseguenze?
- Quali sarebbero le conseguenze di dischi SSD (Solid-State Disk)?
- Esiste un modo per ridurre il divario prestazionale tra la soluzione PLINQ Indexe le soluzioni Threadse Native? Gli esperimenti di riduzione della quantità di dati copiati nei metodi FileMmByte IEnumerator.MoveNext e Current non hanno dimostrato alcun vantaggio significativo.
- Le prestazioni si avvicinano al massimo teorico secondo quanto stabilito dalla larghezza di banda della memoria, dalla velocità della CPU e da altre funzionalità architetturali?
- Esiste un modo per ottenere prestazioni scalabili su sistemi HT (vedere la Figura 5) paragonabili ai sistemi senza HT (Figura 1)?
- È possibile utilizzare la raccolta informazioni e gli strumenti di Visual Studio 2010 per identificare e rimuovere i colli di bottiglia delle prestazioni?
Mi auguro che sia possibile approfondire ulteriormente il discorso.
Johnson (John) M. Hart è consulente specializzato nell'architettura, nello sviluppo, nella formazione tecnica e nella creazione di applicazioni Microsoft Windows e Microsoft .NET Framework. Ha molti anni di esperienza come programmatore, responsabile tecnico e architetto presso Cilk Arts Inc. (da quando è stata acquisita da Intel Corp.), Sierra Atlantic Inc., Hewlett-Packard Co. e Apollo Computer. È stato docente di informatica per molti anni e ha pubblicato quattro edizioni del libro intitolato "Windows System Programming" (Addison-Wesley, 2010).
Un ringraziamento ai seguenti esperti tecnici per la revisione dell'articolo: Michael Bruestle,Andrew Greenwald, Troy Magennis e CK Park