如何提高OpenMP代码的性能?

时间:2016-09-28 15:06:17

标签: c++ multithreading performance openmp

我目前正在尝试提高我的代码的并行性能,我仍然是OpenMP的新手。我必须迭代一个大容器,在每次迭代中读取多个条目并将结果写入单个条目。下面是我正在尝试做的非常简洁的代码示例。

data是一个指向数组的指针,其中存储了许多数据点。在并行区域之前,我创建了一个数组newData,因此可以将data设置为只读,将newData设置为只写,之后我将旧的data抛弃并使用newData进一步计算。 据我所知,datanewData在线程之间共享,并行区域内声明的所有内容都是私有的。 可以通过多个线程从data读取会导致性能问题吗?

我正在使用#criticalnewData元素分配新值以避免竞争条件。这是必要的,因为我只访问newData的每个元素一次,而不是多个线程吗?

我也不确定安排。我是否必须指定是否需要staticdynamic时间表?我可以使用nowait,因为所有线程都是彼此相关的吗?

array *newData = new array;

omp_set_num_threads (threads);

#pragma omp parallel
{
    #pragma omp for
    for (int i = 0;  i < range; i++)
    {
        double middle = (*data)[i];
        double previous = (*data)[i-1];
        double next = (*data)[i+1];

        double new_value = (previous + middle + next) / 3.0;
        #pragma omp critical(assignment)
        (*newData)[i] = new_value;
    }
}

delete data;
data = newData;

我知道在第一次和最后一次迭代previousnext都无法从data读取,在实际代码中这是照顾的但是对于这个最小的例子你从data获得多次阅读的想法。

3 个答案:

答案 0 :(得分:3)

首先,摆脱所有不必要的依赖。 #pragma omp critical(assignment)不是必需的,因为(*newData)的每个索引仅在每个循环中写入一次,因此没有竞争条件。

您的代码现在看起来像这样:

#pragma omp parallel for
for (int i = 0; i < range; i++)
   (*newData)[i] = ((*data)[i-1] + (*data)[i] + (*data)[i+1]) / 3.0;

现在我们正在寻找瓶颈。我想出的潜在候选人名单是:

  • 慢速分裂
  • 缓存抖动
  • ILP(指令级并行)
  • 内存带宽限制
  • 隐藏的依赖项

让我们进一步分析它们。

缓慢分裂: 它需要一些CPU永远计算双/双。要知道你的CPU有多长,有多少,你必须看看它的规格。也许将/3.0替换为*0.3333..可能有所帮助,但也许您的编译器已经这样做了。使用扩展指令集(如SSE / AVX),您可能会同时进行多次分割/乘法运算。

缓存抖动: 由于您的CPU必须一次加载/存储一个缓存行,因此可能存在冲突。想象一下,如果线程1尝试写入(* newdata)[1]并将线程2写入(* newdata)[2]并且它们位于同一缓存行上。现在他们中的一个必须等​​待另一个。您可以使用#pragma omp parallel for schedule(static, 64)解决此问题。

<强> ILP: 如果操作是独立的,CPU可以将多个操作安排到管道中。为此,您必须展开循环。这看起来像这样:

assert(range % 4 == 0);
#pragma omp parallel for
for (int i = 0; i < range/4; i++) {
   (*newData)[i*4+0] = ((*data)[i*4-1] + (*data)[i*4+0] + (*data)[i*4+1]) / 3.0;
   (*newData)[i*4+1] = ((*data)[i*4+0] + (*data)[i*4+1] + (*data)[i*4+2]) / 3.0;
   (*newData)[i*4+2] = ((*data)[i*4+1] + (*data)[i*4+2] + (*data)[i*4+3]) / 3.0;
   (*newData)[i*4+3] = ((*data)[i*4+2] + (*data)[i*4+3] + (*data)[i*4+4]) / 3.0;
}

内存带宽限制: 对于你非常简单的循环,请考虑一下。您需要加载多少内存以及CPU忙于处理它的时间。您正在加载大约1个缓存行并计算一些解除引用,一些指针添加,两个添加和一个除法。您达到的限制取决于您的CPU规格。 现在考虑缓存局部性。你能修改你的代码以更好地利用缓存吗?如果一个线程在一次循环迭代中得到i = 3,而在下一个迭代中得到i = 7,则必须重新加载3(* data)&#39; s。但是如果你从i = 3到i = 4,你可能不需要加载任何东西,因为(* data)[i + 1]在先前加载的高速缓存行中。你节省了一些RAM带宽。要使用它,请展开循环。使用float而不是double也会增加这个机会。

隐藏的依赖关系: 现在这部分我个人觉得非常棘手。有时候你的编译器并不能重复使用某些数据,因为它并不知道它没有改变。使用const可以帮助编译器。但有时您需要restrict来为编译器提供正确的提示。但是我不能很好地理解这一点来解释它。

所以我会尝试这样做:

const double ONETHIRD = 1.0 / 3.0;
assert(range % 4 == 0);
#pragma omp parallel for schedule(static, 1024)
for (int i = 0; i < range/4; i++) {
   (*newData)[i*4+0] = ((*data)[i*4-1] + (*data)[i*4+0] + (*data)[i*4+1]) * ONETHIRD;
   (*newData)[i*4+1] = ((*data)[i*4+0] + (*data)[i*4+1] + (*data)[i*4+2]) * ONETHIRD;
   (*newData)[i*4+2] = ((*data)[i*4+1] + (*data)[i*4+2] + (*data)[i*4+3]) * ONETHIRD;
   (*newData)[i*4+3] = ((*data)[i*4+2] + (*data)[i*4+3] + (*data)[i*4+4]) * ONETHIRD;
}

然后是基准。更多基准测试,并进行更多基准测试。只有基准测试才能显示哪些技巧有帮助。

PS:还有一件事需要考虑。如果你看到你的程序很难打到内存带。您可以考虑更改算法。也许将两个步骤合二为一。喜欢来自 b[i] := (a[i-1] + a[i] + a[i+1]) / 3.0d[i] := (n[i-1] + n[i] + n[i+1]) / 3.0 = (a[i-2] + 2.0 * a[i-1] + 3.0 * a[i] + 2.0 * a[i+1] + a[i+1]) / 3.0。我想你的原因就是你自己找到的。

享受乐趣优化; - )

答案 1 :(得分:2)

  1. 通过多个线程读取数组通常没有坏处。
  2. 如果多个线程处理完全相同的数据,您只需要一个关键部分,这里每个线程访问数组的不同部分,因此您不需要它。关键部分对性能非常不利,因此只有在绝对必要时才使用它们。通常它们可以被原子动作取代: openMP, atomic vs critical? 就像一个关键部分一样,如果每个线程访问不同的数据,它们就没有意义。
  3. 对于调度程序,最好每个测试它们并测量性能,因为对性能的预测通常是错误的。还可以尝试不同的块大小。
  4. 其他一些可能会有所帮助的事情:
    • 测量性能通常会被PC上的其他任务干扰,因此需要进行多次测量并采用最小值(除非每次输入不同,然后取平均值并进行更多测量)。
    • 你真的需要双精度吗?浮动速度要快得多。
  5. 编辑:nowait适用于多个独立的循环:https://msdn.microsoft.com/en-us/library/ek5st0e3.aspx

答案 2 :(得分:0)

我假设您正尝试使用1D阵列进行某种卷积或中值模糊。简短的回答是:坚持默认的时间表策略,并完全摆脱批评。

正如我所知,你是并行主义的新手,处理OpenMP指令有点混乱,比如nowait / private / reduction / critical / atomic / single等。我想你需要什么是一本精心编写的教科书,用于阐明各种概念。如果您有足够的知识,那么学习OpenMP一小时就足以应对大多数日常编程。