Почему этот параллельный код в D так плохо?
Вот один эксперимент, который я провел, сравнивая parallelism в С++ и D. Я реализовал алгоритм (параллельную схему распространения меток для обнаружения сообществ в сетях) на обоих языках, используя тот же дизайн: параллельный итератор получает функцию дескриптора (обычно замыкание) и применяет его для каждого node на графике.
Вот итератор в D, реализованный с помощью taskPool
from std.parallelism
:
/**
* Iterate in parallel over all nodes of the graph and call handler (lambda closure).
*/
void parallelForNodes(F)(F handle) {
foreach (node v; taskPool.parallel(std.range.iota(z))) {
// call here
handle(v);
}
}
И это передаваемая функция handle:
auto propagateLabels = (node v){
if (active[v] && (G.degree(v) > 0)) {
integer[label] labelCounts;
G.forNeighborsOf(v, (node w) {
label lw = labels[w];
labelCounts[lw] += 1; // add weight of edge {v, w}
});
// get dominant label
label dominant;
integer lcmax = 0;
foreach (label l, integer lc; labelCounts) {
if (lc > lcmax) {
dominant = l;
lcmax = lc;
}
}
if (labels[v] != dominant) { // UPDATE
labels[v] = dominant;
nUpdated += 1; // TODO: atomic update?
G.forNeighborsOf(v, (node u) {
active[u] = 1;
});
} else {
active[v] = 0;
}
}
};
Реализация С++ 11 почти идентична, но использует OpenMP для распараллеливания. Итак, что показывают масштабные эксперименты?
![scaling]()
Здесь я рассматриваю слабое масштабирование, удваивая размер входного графа, а также удваивая количество потоков и измеряя время работы. Идеал был бы прямой линией, но, конечно, для parallelism есть некоторые накладные расходы. Я использую defaultPoolThreads(nThreads)
в своей основной функции для установки количества потоков для программы D. Кривая для С++ выглядит неплохо, но кривая для D выглядит удивительно плохо. Я что-то делаю неправильно w.r.t. D parallelism, или это плохо отражается на масштабируемости параллельных программ D?
p.s. флаги компилятора
для D: rdmd -release -O -inline -noboundscheck
для С++: -std=c++11 -fopenmp -O3 -DNDEBUG
ПФС. Что-то должно быть действительно неправильно, потому что реализация D медленнее параллельно, чем последовательно:
![enter image description here]()
ЧГП. Для любопытных, вот URL-адреса Mercurial для обеих реализаций:
Ответы
Ответ 1
Трудно сказать, потому что я не совсем понимаю, как должен работать ваш алгоритм, но похоже, что ваш код не является потокобезопасным, что заставляет алгоритм запускать больше итераций, чем необходимо.
Я добавил это в конец PLP.run
:
writeln(nIterations);
С помощью 1 потока nIterations = 19
С 10 потоками nIterations = 34
С 100 потоками nIterations = 90
Итак, как вы можете видеть, это занимает больше времени не из-за некоторых проблем с std.parallelism
, а просто потому, что он делает больше работы.
Почему ваш код не является потокобезопасным?
Параллельная функция propagateLabels
, которая имеет общий, несинхронизированный доступ к labels
, nUpdated
и active
. Кто знает, какое причудливое поведение это вызывает, но это не может быть хорошо.
Прежде чем начать профилирование, вам необходимо исправить алгоритм, чтобы он был потокобезопасным.
Ответ 2
Как указывает Питер Александр, ваш алгоритм выглядит небезопасным. Чтобы сделать его потокобезопасным, вам необходимо устранить все зависимости данных между событиями, которые могут возникать в разных потоках одновременно или в порядке undefined. Один из способов сделать это - реплицировать некоторое состояние в потоках с помощью WorkerLocalStorage
(предоставлено в std.parallelism) и, возможно, объединить результаты в относительно дешевом цикле в конце вашего алгоритма.
В некоторых случаях процесс репликации этого состояния может быть автоматизирован путем написания алгоритма как сокращения и использования std.parallelism.reduce
(возможно, в сочетании с std.algorithm.map
или std.parallelism.map
) для тяжелого подъема.