Почему этот параллельный код в 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) для тяжелого подъема.