【问题标题】:Vector filling across OpenMP threads跨 OpenMP 线程的向量填充
【发布时间】:2016-03-31 14:51:14
【问题描述】:

我有一个算法,其目标是填充向量。出于性能考虑,算法的迭代分布在 OpenMP 线程中。我想知道哪种方式可以提供更好/更安全的填充向量的方式。

请注意,向量的顺序必须一致(即 vec1 的值 n 必须来自与 vec2 的值 n 相同的迭代。)

假设 1:

std::vector<BasicType> vec1;
std::vector<BasicType> vec2;
#pragma opm parallel for
for(...)
{
    // Do some intensive stuff to compute val1 and val2
    // ...

    #pragma omp critical
    {
        vec1.push_back(val1);
        vec2.push_back(val2);
    }
}
// Then go on to work with vec1 and vec2...

假设 2:

std::vector<BasicType> vec1;
std::vector<BasicType> vec2;
#pragma opm parallel
{
    std::vector<BasicType> vec1local;
    std::vector<BasicType> vec2local;
    #pragma omp for
    for(...)
    {
        // Do some intensive stuff to compute val1 and val2
        // ...

        vec1local.push_back(val1);
        vec2local.push_back(val2);
    }

    #pragma omp critical
    {
        // See note 1 below
        vec1.insert(vec1.end(), vec1local.begin(), vec1local.end());
        vec2.insert(vec2.end(), vec2local.begin(), vec2local.end());
    }
}
// Then go on to work with vec1 and vec2...

注 1: 这取自 Best way to append vector to vector

注 2: 似乎 val1 和 val2 可以组合在某个对象中以保持一致性并仅使用一个向量,但目前对于算法的其余部分来说似乎不切实际。

注 3: 给出一个数量级,for 循环包含大约 100 次迭代,分布在 4 个线程中。除了极少数例外,每次迭代都应具有相同的工作负载(这会带来关键部分几乎同时发生的问题。)

注 4: 仅作记录,整个事情处理图像稳定,并在 Tegra K1 架构上运行。使用的编译器是 gcc 4.8.4。

【问题讨论】:

  • 如果你像这样合并你的向量,你会想要使用移动迭代器来节省复制这么多。
  • 对的填充顺序重要吗?我的意思是你在串行中填写 (val1,val2)_0, 然后 ),(val1,val2)_1, ...(val1,val2)_n-1。如果它必须像这样,那么你的假设都不会成立。
  • 请参阅thisthis,了解有关填充向量和并行合并的更多信息。
  • 此外,如果您提前知道向量的元素数量,则没有理由使用向量。只需使用数组或像Zulan's answer 中的数组一样访问向量。
  • @Zboson 不,顺序无关紧要,因为在此过程的后期会进行一些排序和过滤。后者是为什么我更喜欢使用向量而不是数组。

标签: c++ vector openmp std


【解决方案1】:

根据您的两个建议,我更喜欢第一个。它更简单,不需要额外的内存。但是,我建议没有critical 部分的替代方案:

std::vector<BasicType> vec1(size);
std::vector<BasicType> vec2(size);
#pragma opm parallel for
for(...)
{
    // Do some intensive stuff to compute val1 and val2
    // ...

    vec1[i] = val1;
    vec2[i] = val2;
}

请注意,由于缓存行窃取,这可能会出现一些性能问题。但是,除非事实证明这是通过实际性能分析验证的实际问题,否则我不会担心这一点。一个解决方案可能是:

  • 采用第二个解决方案。 (这会消耗内存和额外的后处理)
  • Align your vector 并为循环使用适当的块。 (这样每个线程都有本地缓存​​行)
  • 使用内部包含局部向量的数据结构,但外部提供必要的全局向量操作。 (这可能是总体上最好的解决方案。)

【讨论】:

  • 使用静态 OpenMP 调度,缓存争用对性能的影响应该很小或没有影响。
  • 谢谢,我认为这应该以最简单的方式解决问题。
  • 至少现在......但是我担心我可能会想出一些迭代可能对向量填充没有贡献的情况,在这种情况下我必须处理 unset元素。
  • 我不确定我的回答是否正确。它在我所做的测试中有效,但我认为可能存在一些可能会产生错误结果的同步问题。我的回答基于您的回答here,
  • @VincentCOISY:在“稀疏”的情况下,只需使用您的解决方案 1,并且只有在性能分析表明这成为一个问题时才重新访问。 [次要免责声明:假设BasicType 易于移动或复制,因此假设push_back 相当便宜。]
【解决方案2】:

我将假设您需要使用向量并且不能使用数组(否则您的问题不是很有趣)。使用t = omp_get_num_threads(),您可以并行填充向量,然后将它们合并到log2(t) 操作中,而不是像这样的t操作(就像您现在所做的那样)

void reduce(std::vector<BasicType> *v1, std::vector<BasicType> *v2, int begin, int end) {
    if(end - begin == 1) return;
    int pivot = (begin+end)/2;
    #pragma omp task
    reduce(v, begin, pivot);
    #pragma omp task
    reduce(v, pivot, end);
    #pragma omp taskwait
    v1[begin].insert(v1[begin].end(), v1[pivot].begin(), v1[pivot].end());
    v2[begin].insert(v2[begin].end(), v2[pivot].begin(), v2[pivot].end());
}

std::vector<BasicType> v1, v2;
std::vector<BasicType> *v1p, *v2p;
#pragma omp parallel
{
    #pragma omp single
    {
        v1p = new std::vector<BasicType>[omp_get_num_threads()];
        v2p = new std::vector<BasicType>[omp_get_num_threads()];
    }
    #pragma omp for
    for(...) {
        // Do some intensive stuff to compute val1 and val2
        // ...
       v1p[omp_get_thread_num()].push_back(val1);
       v2p[omp_get_thread_num()].push_back(val2);
    }
    #pragma omp single
    reduce(v1p, v2p, 0, omp_get_num_threads());
}
v1 = v1p[0], v2 = v2p[0];
delete[] v1p;
delete[] v2p;

例如有 8 个线程,这将加入线程的向量

(0,1) (2,3) (4,5) (6,7)
(0,2) (4,6)
(0,4)

有关并行填充向量的更多信息,请参阅this。有关在log2(t) 操作中合并线程的更多信息,请参阅this question 的答案。

【讨论】:

  • 是的,向量是最有必要的(请参阅我对 Zulan 的回答的评论。)顺便说一句,感谢您提供参考链接。
  • @VincentCOISY,顺便说一句,我不确定我的回答是否 100% 正确。例如,如果我有八个线程和 thread0 进程 (0,1) 并在 thread1 完成 (2,3) 之前完成,它将在 (2,3) 完成之前开始处理 (0,2)。我在做一些测试时得到了正确的结果,但我可能错过了正确的单元测试。
  • @VincentCOISY,我认为可能没问题。该文档说“TASKWAIT 构造指定了等待自当前任务开始以来生成的子任务的完成。”所以只要它等待每个子任务我认为没问题(我仍在了解@987654331 @ 指令)。
  • 我认为您的回答很好,但对我来说这似乎是过早的优化。请注意,您并没有减少操作的数量,而是通过并行化它们来减少关键路径。
  • @Zulan,感谢您的验证。这是一个过早的优化,因为我希望这个问题比它更有趣。我可能不应该给出答案,而是将此答案附加到this 答案。我所说的操作是指插入操作的数量,而不是所有操作。
猜你喜欢
  • 2016-06-03
  • 2018-10-10
  • 2021-12-09
  • 1970-01-01
  • 2017-01-07
  • 1970-01-01
  • 2018-07-10
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多