【问题标题】:OpenMP critical performance better than atomicOpenMP 关键性能优于原子
【发布时间】:2018-09-22 01:15:34
【问题描述】:

我正在尝试从 https://github.com/joeladams/patternlets/blob/master/patternlets/openMP/14.mutualExclusion-critical2/critical2.c 证明关键是更耗时,但我不断得到关键的执行时间比原子更快的结果。有谁知道这是怎么回事?

// simulate many deposits using atomic
startTime = omp_get_wtime();
#pragma omp parallel for 
for (i = 0; i < REPS; i++) {
    #pragma omp atomic
    balance += 1.0;
}
stopTime = omp_get_wtime();
atomicTime = stopTime - startTime;
print("atomic", REPS, balance, atomicTime, atomicTime/REPS);


// simulate the same number of deposits using critical
balance = 0.0;
startTime = omp_get_wtime();
#pragma omp parallel for 
for (i = 0; i < REPS; i++) {
     #pragma omp critical
     {
         balance += 1.0;
     }
}
stopTime = omp_get_wtime();
criticalTime = stopTime - startTime;
print("critical", REPS, balance, criticalTime, criticalTime/REPS);

我的结果是:

After 1000000 $1 deposits using 'atomic':
        - balance = 1000000.00,
        - total time = 0.421999931335,
        - average time per deposit = 0.000000422000

After 1000000 $1 deposits using 'critical':
        - balance = 0.00,
        - total time = 0.265000104904,
        - average time per deposit = 0.000000265000

谢谢!

【问题讨论】:

  • 执行第一次不定时并行计算,以便在两种情况下都准备好 OpenMP 线程。
  • 在我的例子中:wandbox.org/permlink/mxL9fAVWIycEHlVN,即使有线程创建开销,原子也更快。请注意,您的代码有问题,因为 balance 在第二次循环之后是 0.00,因此增量不适用。
  • 请始终将您的代码发布为minimal reproducible example

标签: c++ c openmp atomic critical-section


【解决方案1】:

发布答案,因为这种情况确实/确实存在。 (很大程度上取决于您使用的线程数)

考虑 OP 计算的三种情况(以 107 为增量更新 balance,每个增量为 1,从值 0 开始)与时间进行比较 - 一种没有任何形式的并行化(或不使用 openmp 指令),一种使用 omp critical 更新 balance,另一种使用 omp atomic 相同:

#include <omp.h>
#include <iostream>

int main()
{   const int REPS = 1e+7;
    double balance = 0.0;

    std::cout << "Running without any explicit parallelization from openmp:" << std::endl;
    auto startTime = omp_get_wtime();
    for (int i = 0; i < REPS; i++) {
        balance += 1.0;
    }
    auto stopTime = omp_get_wtime();
    std::cout << "Balance: " << balance << ", Total time taken: " << stopTime - startTime << std::endl;
    // Reset balance:
    balance = 0.0;

    std::cout << "Running with omp critical:" << std::endl;
    startTime = omp_get_wtime();
    #pragma omp parallel for
    for (int i = 0; i < REPS; i++) {
        #pragma omp critical
        balance += 1.0;
    }
    stopTime = omp_get_wtime();
    std::cout << "Balance: " << balance << ", Total time taken: " << stopTime - startTime << std::endl;
    // Reset balance:
    balance = 0.0;

    std::cout << "Running with omp atomic:" << std::endl;
    startTime = omp_get_wtime();
    #pragma omp parallel for
    for (int i = 0; i < REPS; i++) {
        #pragma omp atomic
        balance += 1.0;
    }
    stopTime = omp_get_wtime();
    std::cout << "Balance: " << balance << ", Total time taken: " << stopTime - startTime << std::endl;
}

这是我机器上的两次运行:

对于适用omp atomic 的单个计算,预计它会比omp critical 运行得更快,但是,可能相反,它有时会稍微耗时一些.例如,它不适用于我上面的第二次运行。我假设这可能是“应该更快的东西似乎有点慢”的一种情况,你没有指定线程数(罕见的情况)。对于更多数量的线程,或者只是按照现代计算机的默认值,每次(如提到的 OP)都不太可能获得相同的结果(原子时间>关键时间),因为它们有少数核心. (线程数通常由内核数给出,由于超线程,线程数可能是两倍或更多)
事实上,为了让我也复制这样的运行,我尝试了几次。 (我想展示两种情况都可能发生的两次运行,同时使用默认线程数,或者不指定线程数)

在我谈论这种行为每次都可能发生的情况之前,我想提一下,我合并第一种情况(没有 openmp 指令)的原因是为了表明它比omp criticalomp atomic 被使用,因为我们正在运行的代码段是连续的。通过使用critical/atomic,我们只是通过锁定和解锁引入了额外的开销。 (与在使用 pthread 时比较不带和不带互斥锁的情况时所期望的相同)

现在,如果我要复制 OP 面临的情况(原子所花费的时间大于关键的重复行为),我会将我的线程数设置为尽可能低,同时仍然牢记不要让它单线程:(即2)

omp_set_num_threads(2);

现在,可以观察到omp atomic 块花费的时间总是大于omp critical 块花费的时间。例如,使用此修改运行两次:(不是罕见的情况)

修复

为了使原子变体在这两种情况下总是比临界等价物运行得更快,reduction clause 可以与适当的参数一起使用:(即在我们正在执行加法的情况下使用“+”运算符,然后通过我们在这里聚合的变量)

#pragma omp parallel for reduction(+:balance)

进行此更改后运行两次(对于情况二和三),同时保持线程数为 2:

最后,omp atomic 现在的运行速度比 critical 等价物快得多,这与一般预期非常相似。

另一组两个运行此更改,但未设置线程数:

omp atomic 不仅性能优于 omp critical,而且现在(总是)比不使用 omp 指令的情况(第一)运行得更快。 (成功并行化)

【讨论】:

  • 请注意,如果您使用缩减,则根本不需要 omp atomicomp critical
  • @Laci 对,我将它们包括在内是因为 OP 的问题围绕着那些 omp 指令(原子指令与关键指令)。否则是的,最好只进行减少,因为这将是最快的选择(避免因原子/关键的锁定/开销而产生的成本)。
【解决方案2】:

我猜增加浮点数与增加整数不同。这取决于 CPU 架构。当我用整数测试时,没关系。

查看我的结果:atomic 比critical 快了一倍多,但与不使用 atomic 和 critical 相比,它仍然慢得多,甚至结果也不正确。

所以,如果可能的话,尽量避免锁、关键、原子。

测试结果:

没有原子,关键:6666667, 0.000113381

原子:10000000, 0.399095

关键:10000000, 0.999381

【讨论】:

  • 声明结果时始终显示代码。 “即使结果不正确”意味着您尝试的任何内容一开始都是不正确的。正确使用时,并行化没有理由影响代码输出(只是时间不同)。这同样适用于反面。与执行正常顺序运行时没有额外开销相比,这里使用 omp criticalomp atomic 会因为额外的开销(例如锁定和解锁)而变慢。 (即不使用原子/关键 omp 指令,或者由于并行化而无需迎合关键部分)
  • @Ben 我同意 x86-64 机器上整数的原子增加是单条指令 (lock add ...,0x1),但双倍的增加需要几条指令,包括可能的分支 (lock cmpxchg ... , jne ...) , 如果其他线程在该过程中更改相同的内存,则效率不高)。
猜你喜欢
  • 2014-03-02
  • 2014-06-08
  • 2012-01-31
  • 1970-01-01
  • 2011-11-07
  • 1970-01-01
  • 2021-08-13
  • 1970-01-01
  • 2012-06-11
相关资源
最近更新 更多