(Когда) практичны параллельные сорта и как вы пишете эффективный? - PullRequest
12 голосов
/ 14 февраля 2010

Я работаю над библиотекой распараллеливания для языка программирования D. Теперь, когда я довольно доволен базовыми примитивами (параллельный foreach, map, Reduce и tasks / futures), я начинаю думать о некоторых параллельных алгоритмах более высокого уровня. Среди наиболее очевидных кандидатов на распараллеливание - сортировка.

Мой первый вопрос: являются ли параллельные версии алгоритмов сортировки полезными в реальном мире, или они в основном академические? Если они полезны, где они полезны? Лично я редко использовал бы их в своей работе, просто потому что я обычно привязывал все свои ядра на 100%, используя гораздо более детальный уровень параллелизма, чем один вызов sort ().

Во-вторых, кажется, что быстрая сортировка почти смущающе параллельна для больших массивов, но я не могу получить почти линейные ускорения, которые, я полагаю, мне следовало бы получить. Для быстрой сортировки единственной неотъемлемой последовательной частью является первый раздел. Я попытался распараллелить быструю сортировку, отсортировав после каждого раздела два подмассива параллельно. В упрощенном псевдокоде:

// 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..$]);
    }
}

Даже для очень больших массивов, где время, которое занимает первый раздел, ничтожно мало, я могу получить только около 30% ускорения на двухъядерном процессоре по сравнению с чисто последовательной версией алгоритма. Я предполагаю, что узким местом является доступ к общей памяти. Любое понимание того, как устранить это узкое место или что еще может быть узким местом?

Изменить: Мой пул задач имеет фиксированное количество потоков, равное количеству ядер в системе минус 1 (поскольку основной поток также работает). Кроме того, тип ожидания, который я использую, - это ожидание работы, то есть, если задача запущена, но не завершена, поток, вызывающий workWait(), крадет другие задания из пула и выполняет их до тех пор, пока не будет выполнено ожидание. Если задача не запущена, она завершается в текущем потоке. Это означает, что ожидание не является неэффективным. Пока есть работа, все потоки будут заняты.

Ответы [ 5 ]

7 голосов
/ 14 февраля 2010

Имейте в виду, я не эксперт по параллельной сортировке, и люди делают исследовательскую карьеру параллельной, но ...

1) они полезны в реальном мире.

Конечно, если вам нужно отсортировать что-то дорогое (например, строки или что-то еще), и вы не привязываете все ядра.

  • думаю, что код пользовательского интерфейса, где вам нужно отсортировать большой динамический список строк на основе контекста
  • Подумайте, что-то вроде симулятора n-тел Барнса, где вам нужно отсортировать частицы

2) Быстрая сортировка, похоже, даст линейное ускорение, но это не так. Шаг разделения является последовательным узким местом, вы увидите это, если будете профилировать, и он будет иметь тенденцию к увеличению в 2-3 раза на четырехъядерном ядре.

Если вы хотите добиться хорошего ускорения на небольшой системе, вам нужно убедиться, что ваши накладные расходы действительно малы, и в идеале вы должны убедиться, что у вас не слишком много запущенных потоков, то есть не намного больше, чем 2 на двухъядерном. Пул потоков, вероятно, не является правильной абстракцией.

Если вы хотите добиться хорошего ускорения в более крупной системе, вам нужно взглянуть на параллельные сортировки, основанные на сканировании, об этом есть бумаги. Битонная сортировка также легко распараллеливается, как и сортировка слиянием. Параллельная сортировка по основанию также может быть полезна, она есть в PPL (если вы не против Visual Studio 11).

3 голосов
/ 14 февраля 2010

Я не эксперт но ... вот что я бы посмотрел:

Прежде всего, я слышал, что, как правило, алгоритмы, которые с самого начала смотрят на небольшие кусочки проблемы, лучше работают как параллельные алгоритмы.

Рассматривая вашу реализацию, попробуйте заставить параллельный / последовательный коммутатор пойти другим путем: разбить массив и отсортировать параллельно, пока у вас не будет N сегментов, а затем перейти к последовательному. Если вы более или менее захватываете новый поток для каждого параллельного случая, то N должно быть ~ вашим количеством ядер. OTOH, если ваш пул потоков имеет фиксированный размер и действует как очередь короткоживущих делегатов, то я бы использовал N ~ 2+ раз вашего подсчета ядер (чтобы ядра не простаивали, потому что один раздел завершился быстрее). *

Другие настройки:

  • пропускает myTask.wait(); на локальном уровне и, скорее, имеет функцию-оболочку, которая ожидает всех задач.
  • Создайте отдельную последовательную реализацию функции, которая позволяет избежать проверки глубины.
1 голос
/ 07 марта 2012

Я не знаю, применимы ли ответы здесь больше или мои предложения применимы к D.

В любом случае ...

Предполагая, что D позволяет это, всегда есть возможность предоставления подсказок предварительной выборки кешам. Ядро, о котором идет речь, требует, чтобы данные, которые скоро (не сразу), были загружены в определенный уровень кэша В идеальном случае данные будут получены к тому времени, когда ядро ​​начнет работать с ним. Скорее всего, процесс предварительной выборки будет происходить в большей или меньшей степени, что, по крайней мере, приведет к меньшему количеству состояний ожидания, чем если бы данные были выбраны «холодными».

Вы по-прежнему будете ограничены общей пропускной способностью кэш-памяти, поэтому вам необходимо организовать данные таким образом, чтобы в эксклюзивных кэшах ядра находилось столько данных, чтобы оно могло проводить там немало времени. перед записью обновленных данных.

Код и данные должны быть организованы в соответствии с концепцией строк кэша (выборочных блоков по 64 байта в каждой), которая является наименьшей по размеру единицей в кэше. Это должно привести к тому, что для двух ядер работа должна быть организована так, чтобы система памяти работала вдвое меньше на ядро ​​(при условии 100% -ной масштабируемости), чем раньше, когда работало только одно ядро, а работа не была организована. Для четырех ядер четверть столько и так далее. Это довольно сложная задача, но ни в коем случае не невозможная, она зависит только от того, насколько вы изобретательны в реструктуризации работы. Как всегда, есть решения, которые невозможно придумать ... пока кто-то не сделает это!

Я не знаю, как WYSIWYG D сравнивается с C - который я использую - но в целом я думаю, что процесс разработки масштабируемых приложений улучшается за счет того, насколько разработчик может влиять на компилятор при его фактической генерации машинного кода. Для интерпретируемых языков переводчик будет выполнять так много работы с памятью, что вы рискуете не заметить улучшения от общего «фонового шума».

Однажды я написал многопоточную оболочку, которая работала на 70% быстрее на двух ядрах по сравнению с одним и на 100% на трех ядрах по сравнению с одним. Четыре ядра работали медленнее, чем три. Итак, я знаю дилеммы, с которыми вы сталкиваетесь.

1 голос
/ 18 февраля 2010

«Мой первый вопрос - это параллельные версии алгоритмов сортировки, полезные в реальном мире» - зависит от размера набора данных, над которым вы работаете в реальной работе. Для небольших наборов данных ответ - нет. Для больших наборов данных это зависит не только от размера набора данных, но и от конкретной архитектуры системы.

Одним из сдерживающих факторов, которые будут препятствовать ожидаемому увеличению производительности, является структура кэша системы. Если данные могут уместиться в кэш-памяти L1 ядра, то при сортировке по нескольким ядрам выиграть будет мало, так как вы понесете штраф за пропуск кэш-памяти L1 между каждой итерацией алгоритма сортировки.

То же самое относится к микросхемам с несколькими кэшами L2 и архитектурами NUMA (неоднородный доступ к памяти). Таким образом, чем больше ядер вы хотите распределить по сортировке, тем меньше нужно будет увеличивать константу smalllestToParallelize.

Другим ограничивающим фактором, который вы определили, является доступ к общей памяти или конфликт по шине памяти. Поскольку шина памяти может удовлетворить только определенное количество обращений к памяти в секунду; наличие дополнительных ядер, которые по сути ничего не делают, кроме чтения и записи в основную память, создаст большую нагрузку на систему памяти.

Последний фактор, на который я должен обратить внимание, - это сам пул потоков, поскольку он может быть не таким эффективным, как вы думаете. Поскольку у вас есть потоки, которые крадут и генерируют работу из общей очереди, эта очередь требует методов синхронизации; и в зависимости от того, как они реализованы, они могут вызвать очень длинные последовательные секции в вашем коде.

0 голосов
/ 07 марта 2012

Я хотел бы указать вам на внешнюю сортировку [1], которая сталкивается с похожими проблемами.Обычно этот класс алгоритмов используется в основном для работы с большими объемами данных, но их главное в том, что они разбивают большие куски на более мелкие и не связанные с этим задачи, которые, следовательно, действительно хороши для параллельной работы.После этого вам «нужно только» сшить частичные результаты, которые не совсем параллельны (но относительно дешевы по сравнению с реальной сортировкой).

Внешняя сортировка слиянием также очень хорошо работает с неизвестнымпотоки.Вы просто произвольно разделяете рабочую нагрузку и передаете каждый кусок из n элементов в поток всякий раз, когда происходит один холостой ход, до тех пор, пока все ваши рабочие блоки не будут выполнены, и в этот момент вы можете начать их объединять.1] http://en.wikipedia.org/wiki/External_sorting

...