And*_*And 3 c++ multithreading atomic openmp histogram
我的完整代码中有一段代码:
\nconst unsigned int GL=8000000;\nconst int cuba=8;\nconst int cubn=cuba+cuba;\nconst int cub3=cubn*cubn*cubn;\nint Length[cub3];\nint Begin[cub3];\nint Counter[cub3];\nint MIndex[GL];\nstruct Particle{\n int ix,jy,kz;\n int ip;\n};\nParticle particles[GL];\nint GetIndex(const Particle & p){return (p.ix+cuba+cubn*(p.jy+cuba+cubn*(p.kz+cuba)));} \n...\n#pragma omp parallel for\nfor(int i=0; i<cub3; ++i) Length[i]=Counter[i]=0;\n#pragma omp parallel for\nfor(int i=0; i<N; ++i)\n{\n int ic=GetIndex(particles[i]);\n #pragma omp atomic update\n Length[ic]++;\n}\nBegin[0]=0;\n#pragma omp single\nfor(int i=1; i<cub3; ++i) Begin[i]=Begin[i-1]+Length[i-1];\n#pragma omp parallel for\nfor(int i=0; i<N; ++i)\n{\n if(particles[i].ip==3)\n {\n int ic=GetIndex(particles[i]);\n if(ic>cub3 || ic<0) printf("ic=%d out of range!\\n",ic);\n int cnt=0;\n #pragma omp atomic capture\n cnt=Counter[ic]++;\n MIndex[Begin[ic]+cnt]=i;\n }\n}\nRun Code Online (Sandbox Code Playgroud)\n如果要删除
\n#pragma omp parallel for\nRun Code Online (Sandbox Code Playgroud)\n代码工作正常,输出结果始终相同。\n但是使用此编译指示,代码中存在一些未定义的行为/竞争条件,因为每次它都会给出不同的输出结果。\n如何解决此问题?
\n更新:任务如下。有很多带有一些随机坐标的粒子。需要将粒子的数组粒子在坐标系的每个单元(笛卡尔立方体,例如1\xc3\x971\xc3\x971 cm)中的索引输出到数组MIndex中。因此,在MIndex的开头,应该在坐标系的第一个单元格中的粒子的数组粒子中存在索引,然后 - 在第二个单元格中,然后 - 在第三个单元格中,依此类推。区域MIndex中给定单元内的索引顺序并不重要,可以是任意的。如果可能的话,需要并行进行,可以使用原子操作。
\n有一种直接的方法:并行遍历所有坐标单元,并在每个单元中检查所有粒子的坐标。但对于大量细胞和粒子来说,这似乎很慢。有更快的方法吗?是否可以并行遍历粒子数组一次并使用原子操作填充MIndex数组,就像上面的代码片段中所写的那样?
\n如果您想要一个可以高效工作的算法(不需要共享计数器上的原子 RMW,这将是一场灾难,请参见下文),您可能无法让编译器为您自动并行化标量代码。但您也许可以使用 OpenMP 作为启动线程和获取线程 ID 的方式。
(更新:这可能行不通:我if(particles[i].ip==3)之前没有注意到源代码中的 。我假设它Count[ic]会像Length[ic]串行版本中一样高。如果情况不是这样,这个策略可能会留下空白或其他东西。但正如 Laci 所说指出,也许您希望在计算长度时首先进行检查,那么就可以了。)
手动对第一个直方图进行多线程处理(进入Length[]),每个线程处理已知的i值范围。保留这些每个线程的长度,即使您对它们求和并通过前缀求和来构建 Begin[]。
Length[thread][ic]该立方体中的粒子数量也是如此,超出了i该线程处理的值范围。(并且将在第二个循环中再次循环:关键是我们以相同的方式将线程之间的粒子划分两次。理想情况下,同一线程在同一范围内工作,因此 L1d 缓存中的事物可能仍然很热。)
将其预处理到每个线程Begin[][]数组中,以便每个线程知道 MIndex 中的位置将每个存储桶中的数据放入其中。
// pseudo-code, fairly close to actual C
for(ic < cub3) {
// perhaps do this "vertical" sum into a temporary array
// or prefix-sum within Length before combining across threads?
int pos = sum(Length[0..nthreads-1][ic-1]) + Begin[0][ic-1];
Begin[0][ic] = pos;
for (int t = 1 ; t<nthreads ; t++) {
pos += Length[t][ic]; // prefix-sum across threads for this cube bucket
Begin[t][ic] = pos;
}
}
Run Code Online (Sandbox Code Playgroud)
这有一个非常糟糕的缓存访问模式,特别是在cuba=8makeLength[t][0]和Length[t+1][0]4096 字节彼此分开的情况下。(因此 4k 别名是一个可能的问题,缓存冲突未命中也是如此)。
也许每个线程都可以将自己的 Length 切片前缀求和到 Begin 切片中,1. 用于缓存访问模式(以及局部性,因为它刚刚写入了这些长度),2. 为该工作获得一些并行性。
然后在最后一个循环中MIndex,每个线程都可以int pos = --Length[t][ic]从长度中派生出唯一的 ID。(就像您对 所做的那样Count[],但没有将另一个每线程数组引入为零。)
Length 的每个元素都将返回零,因为同一个线程正在查看它刚刚计算的相同点。Begin[t][ic]如果位置计算正确,MIndex[...] = i商店就不会发生冲突。 错误共享仍然是可能的,但它是一个足够大的数组,点往往会分散在周围。
不要过度使用线程数,特别是当cuba线程数大于 8 时。长度/开始预处理工作量随线程数而变化,因此最好为不相关的线程或任务留出一些空闲 CPU完成一些吞吐量。OTOH,这cuba=8意味着每个线程数组只有 4096 字节(太小而无法并行化归零,顺便说一句),实际上并没有那么多。
(编辑之前的先前回答更清楚地说明了发生了什么。)
这基本上是一个直方图吗?如果每个线程都有自己的计数数组,您可以在最后将它们相加(您可能需要手动执行此操作,而不是让 OpenMP 为您执行此操作)。但似乎您还需要这个计数在每个体素中都是唯一的,以便正确更新 MIndex?这可能是一个令人震惊的事情,就像要求调整每个 MIndex 条目(如果可能的话)。
更新后,您将在 Length[] 中绘制直方图,以便可以加快该部分的速度。
共享计数器的原子增量会更慢,并且在 x86 上可能会严重破坏内存级并行性。在 x86 上,每个原子 RMW 也是一个完整的内存屏障,在发生之前耗尽存储缓冲区,并阻止稍后的加载从开始到发生之后。
与使用非原子访问的单个线程相反,单个线程可能会缓存未命中多个元素Counter,Begin并且元素未完成。MIndex(由于无序 exec,下一次迭代的 load / inc / store forCounter[ic]++可以执行加载,同时 forBegin[ic]和/或Mindex[]store 存在未完成的缓存未命中。)
允许宽松原子增量的 ISA 可能能够有效地做到这一点,例如 AArch64。(同样,OpenMP 可能无法为您做到这一点。)
即使在 x86 上,拥有足够的(逻辑)核心,您仍可能获得一定的加速,特别是如果Counter访问足够分散,核心不会不断争夺相同的缓存线。不过,您仍然会得到大量缓存行在内核之间跳跃,而不是在 L1d 或 L2 中保持热状态。(虚假分享是一个问题,
也许软件预取可以提供帮助,例如prefetchw(写入预取)计数器 5 或 10i次迭代后。
不过,即使有memory_order_seq_cst增量,哪个点按哪个顺序进行也不是确定性的。无论哪一个线程首先递增,Counter[ic]都是将 thatcnt与 that关联起来的线程i。
也许让每个线程扫描所有点,但只处理其中的一个子集,且子集不相交。Counter[]因此,任何给定线程接触的元素 集仅由该线程接触,因此增量可以是非原子的。
按范围过滤p.kz可能最有意义,因为这是索引中最大的乘数,因此每个线程“拥有”一个连续的Counter[].
但如果你的观点分布不均匀,你就需要知道如何分解事情以大致平均地分配工作。而且您不能将其划分得更细(如 OMP 动态调度),因为每个线程都会扫描所有点:这会增加过滤工作量。
也许几个固定分区是一个很好的权衡,可以在不引入大量额外工作的情况下获得一些并行性。
你已经循环遍历整个点数组了吗Length[ic]++;?使用 再次执行相同的直方图工作似乎是多余的Counter[ic]++;,但如何避免它并不明显。
计数数组很小,但如果完成后不需要两者,您可能只需递减 Length 即可为体素中的每个点分配唯一索引。至少第一个直方图可以受益于每个线程的不同计数数组的并行化,以及在最后垂直相加。由于计数数组对于 L1d 缓存来说足够小,因此应该可以与线程完美地扩展。
顺便说一句,for() Length[i]=Counter[i]=0;太小了,不值得并行化。对于cuba=8,它8*8*16 * sizeof(int)= 4096 字节,只是一页,所以它只是两个小的内存集。
(当然,如果每个线程都有自己单独的 Length 数组,则每个线程都需要将其归零)。如果一长串点都在同一个存储桶中,那么它足够小,甚至可以考虑展开每个线程可能有 2 个计数数组,以隐藏存储/重新加载串行依赖关系。最后组合计数数组是一项工作,#pragma omp simd或者只是正常的自动向量化,gcc -O3 -march=native因为它是整数工作。
对于最后一个循环,您可以将点数组分成两半(将一半分配给每个线程),并让一个线程通过从 向下计数来获取唯一 ID --Length[i],另一个线程从 中的 0 开始向上计数Counter[i]++。通过不同的线程查看不同的点,这可以使您的速度提高 2 倍。(MIndex 存储的模争用。)
要做的不仅仅是向上和向下计数,您还需要从整个 Length 数组中获得的信息……但您暂时拥有这些信息。请参阅顶部部分
| 归档时间: |
|
| 查看次数: |
214 次 |
| 最近记录: |