我目前正在尝试提高我的代码的并行性能,我仍然是OpenMP的新手。我必须迭代一个大容器,在每次迭代中读取多个条目并将结果写入单个条目。下面是我正在尝试做的非常简洁的代码示例。
data
是一个指向数组的指针,其中存储了许多数据点。在并行区域之前,我创建了一个数组newData
,因此可以将data
设置为只读,将newData
设置为只写,之后我将旧的data
抛弃并使用newData
进一步计算。
据我所知,data
和newData
在线程之间共享,并行区域内声明的所有内容都是私有的。
可以通过多个线程从data
读取会导致性能问题吗?
我正在使用#critical
为newData
元素分配新值以避免竞争条件。这是必要的,因为我只访问newData
的每个元素一次,而不是多个线程吗?
我也不确定安排。我是否必须指定是否需要static
或dynamic
时间表?我可以使用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;
我知道在第一次和最后一次迭代previous
和next
都无法从data
读取,在实际代码中这是照顾的但是对于这个最小的例子你从data
获得多次阅读的想法。
答案 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;
现在我们正在寻找瓶颈。我想出的潜在候选人名单是:
让我们进一步分析它们。
缓慢分裂:
它需要一些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.0
至
d[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)
答案 2 :(得分:0)
我假设您正尝试使用1D阵列进行某种卷积或中值模糊。简短的回答是:坚持默认的时间表策略,并完全摆脱批评。
正如我所知,你是并行主义的新手,处理OpenMP指令有点混乱,比如nowait / private / reduction / critical / atomic / single等。我想你需要什么是一本精心编写的教科书,用于阐明各种概念。如果您有足够的知识,那么学习OpenMP一小时就足以应对大多数日常编程。