Sto lavorando su una libreria di parallelizzazione per il linguaggio di programmazione D. Ora che sono abbastanza soddisfatto delle primitive di base (foreach parallela, map, reduce e tasks/futures), sto iniziando a pensare ad alcuni algoritmi paralleli di più alto livello. Tra i candidati più ovvi per la parallelizzazione c'è l'ordinamento.(Quando) sono pratiche pratiche parallele e come si scrive in modo efficiente?
mia prima domanda è, sono parallelized versioni di algoritmi di ordinamento utili nel mondo reale, o sono per lo più accademico? Se sono utili, dove sono utili? Personalmente li userò raramente nel mio lavoro, semplicemente perché di solito leggo tutti i miei core al 100% usando un livello di parallelismo a grana molto più grossa di una singola chiamata sort().
In secondo luogo, sembra che l'ordinamento rapido sia quasi in modo imbarazzante parallelo per i grandi array, ma non riesco a ottenere gli aumenti quasi lineari che credo dovrei ottenere. Per un ordinamento rapido, l'unica parte intrinsecamente seriale è la prima partizione. Ho provato a parallelizzare un ordinamento rapido, dopo ogni partizione, ordinando i due sottoarray in parallelo. In pseudocodice semplificata:
// I tweaked this number a bunch. Anything smaller than this and the
// overhead is smaller than the parallelization gains.
const smallestToParallelize = 500;
void quickSort(T)(T[] array) {
if(array.length < someConstant) {
insertionSort(array);
return;
}
size_t pivotPosition = partition(array);
if(array.length >= smallestToParallelize) {
// Sort left subarray in a task pool thread.
auto myTask = taskPool.execute(quickSort(array[0..pivotPosition]));
quickSort(array[pivotPosition + 1..$]);
myTask.workWait();
} else {
// Regular serial quick sort.
quickSort(array[0..pivotPosition]);
quickSort(array[pivotPosition + 1..$]);
}
}
Anche per matrici molto grandi, dove il tempo prima partizione prende è trascurabile, posso solo ottenere circa un aumento di velocità del 30% su un dual core, rispetto ad una versione puramente seriale dell'algoritmo . Immagino che il collo di bottiglia sia l'accesso alla memoria condivisa. Qualche idea su come eliminare questo collo di bottiglia o cos'altro potrebbe essere il collo di bottiglia?
Edit: La mia piscina attività ha un numero fisso di thread, pari al numero di nuclei nel sistema meno 1 (poiché il filo principale fa anche lavoro). Inoltre, il tipo di attesa che sto utilizzando è un'aspettativa di lavoro, ad esempio se l'attività è avviata ma non terminata, il thread che chiama workWait()
ruba altri lavori fuori dal pool e li esegue finché non viene completato quello in attesa. Se l'attività non è avviata, viene completata nel thread corrente. Ciò significa che l'attesa non è inefficiente. Finché c'è lavoro da fare, tutti i thread saranno tenuti occupati.
Non so come rendere il vostro Quicksort parallelizzare meglio, ma c'è una variante denominata Samplesort che è intrinsecamente molto più veloce di un Quicksort vaniglia, e, per quanto posso vedere, dovrebbe essere ugualmente parallelizzabile –