【问题标题】:Why does this parallel code in D scale so badly?为什么 D 中的这个并行代码的规模如此之大?
【发布时间】:2026-01-14 22:05:01
【问题描述】:

这是我在 C++ 和 D 中比较并行性进行的一个实验。我使用相同的设计在两种语言中实现了一种算法(一种用于网络社区检测的并行标签传播方案):并行迭代器获取句柄函数(通常一个闭包)并将其应用于图中的每个节点。

这是 D 中的迭代器,使用来自 std.parallelismtaskPool 实现:

/**
 * 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);
    }
}

这是传递的句柄函数:

    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;
        }

        }
    };

C++11 实现几乎相同,但使用 OpenMP 进行并行化。那么缩放实验表明了什么?

在这里,我检查了弱缩放,将输入图大小加倍,同时将线程数加倍并测量运行时间。理想情况下应该是一条直线,但并行性当然会有一些开销。我在 main 函数中使用defaultPoolThreads(nThreads) 来设置 D 程序的线程数。 C++ 的曲线看起来不错,但 D 的曲线看起来非常糟糕。我做错了什么吗? D 并行性,或者这是否严重影响了并行 D 程序的可扩展性?

附言编译器标志

对于 D:rdmd -release -O -inline -noboundscheck

对于 C++:-std=c++11 -fopenmp -O3 -DNDEBUG

pps。一定是真的有问题,因为并行的 D 实现比顺序的慢:

pps。出于好奇,以下是两种实现的 Mercurial 克隆网址:

【问题讨论】:

  • 不使用 openmp 时性能如何?
  • 通过检查它看起来不像 dmd 编译器当前支持 openmp。如果一个版本使用 openmp 而另一个版本不使用,这对我来说似乎不是苹果对苹果的比较。
  • 虚假分享,也许吧? en.wikipedia.org/wiki/False_sharing
  • 可以试试在GDC下编译D程序吗?
  • 我可以确认您的发现。我使用的线程越多,运行时间越长:平台 Win32。我知道您使用的是 64 位(因为我必须将一些 'long' 更改为 'size_t' 才能编译它)。你使用的是 Win 还是 Nix?

标签: c++ performance parallel-processing d


【解决方案1】:

很难说,因为我不完全理解您的算法应该如何工作,但看起来您的代码不是线程安全的,这导致算法运行的迭代次数超过了必要的次数。

我在PLP.run的末尾添加了这个:

writeln(nIterations);

有 1 个线程 nIterations = 19
10个线程nIterations = 34
100个线程nIterations = 90

如您所见,它需要更长的时间不是因为 std.parallelism 的一些问题,而仅仅是因为它正在做更多的工作。

为什么你的代码不是线程安全的?

您并行运行的函数是propagateLabels,它具有对labelsnUpdatedactive共享、非同步访问权限。谁知道这会导致什么奇怪的行为,但它不会是好的。

在开始分析之前,您需要将算法修复为线程安全的。

【讨论】:

  • 观察力不错。对我来说有趣的问题是:为什么 D 中的这种行为与几乎相同的 C++ 实现如此不同?我知道线程共享labelsactivenUpdated。这种情况对于 C++/OpenMP 实现也是一样的,这不是问题。
  • 不幸的是,我对 OpenMP 并不熟悉,但它分配作业的方式可能与 std.parallelism 不同,因此 OpenMP 解决方案可能“只适用于”您的运行方式。
【解决方案2】:

正如 Peter Alexander 所指出的,您的算法似乎是线程不安全的。为了使其线程安全,您需要消除可能同时发生在不同线程中或以未定义顺序发生的事件之间的所有数据依赖关系。一种方法是使用WorkerLocalStorage(在 std.parallelism 中提供)跨线程复制一些状态,并可能在算法结束时将结果合并到一个相对便宜的循环中。

在某些情况下,复制此状态的过程可以通过将算法编写为约简并使用 std.parallelism.reduce(可能与 std.algorithm.mapstd.parallelism.map 组合)来完成繁重的工作来自动化。

【讨论】: