使用 openmp 并行化矩阵以避免错误共享

Posted

技术标签:

【中文标题】使用 openmp 并行化矩阵以避免错误共享【英文标题】:Parallelize a matrix with openmp to avoid false sharing 【发布时间】:2019-09-13 10:32:46 【问题描述】:

我正在尝试优化并行算法:

 double update(double *src, int x, int y)
     return ((*src)[x][y - 1] + (*src)[x][y + 1] +
            (*src)[x - 1][y] + (*src)[x + 1][y])+(*src)[x][y];
 

 void func()
     int h = 300;
     int w = 300;
     int t = 0;
     double e = 90.0;
     double data[300][300];
     double data2[300][300];
     while (d >= e) 
            d = 0.0;
            #pragma omp parallel for collapse(2) reduction(+:d) schedule(static,5624)
            for (y = 1; y < h - 1; y++) 
                for (x = 1; x < w - 1; x++) 

                    if(t % 2 == 0)
                        double o = data2[x][y];
                        double n = update(data2, x, y);

                        data[x][y] = n;
                        d += fabs(o - n);
                    else
                        double o = data[x][y];
                        double n = update(data, x, y);
                        data2[x][y] = n;
                        d += fabs(o - n);
                    

                
            

            
            t += 1;
        

此代码并行工作,但当线程数设置为相当高的 16(我正在运行 8 核 i9 CPU)时,程序运行速度比仅使用 8 或 4 个线程时要慢。我假设这可能与错误共享有关,所以我尝试设置一些调度,我的逻辑是这样的:

300*300 = 90k,有 16 个线程 90k/16 = 5625,这是我假设的默认静态调度?

但是我正在写入一个 8 字节的嵌套数据数组,假设我的缓存线是 64 字节,这意味着我可以在缓存线上有 8 个元素,这些元素不应该在内核之间共享。由于 5625%8 不等于 0 是否意味着缓存行是共享的?所以为了解决这个问题,我只是让它减去 1 得到 5624。

我猜这意味着一个线程将多次运行一个循环,但我认为最好不要共享缓存行?

无论如何,最后的结果是共享缓存线 (5625) 的时间是 3.8464 秒,不共享缓存线 (5624) 的时间是 3.8028。差异几乎不惊人,所以我完全误解了这一切是如何运作的?

【问题讨论】:

【参考方案1】:

虚假共享很可能不是这里的问题。缓存行只能在不同线程的索引范围相遇的地方重叠,因此在线程 n 将执行的最后几个索引和线程 n+1 的第一个索引.这不仅代表了索引的一小部分,而且不太可能同时运行,您可以想象所有线程都以相似的速度进行,只要范围不太小,这永远不会发生(即线程 n 应该在线程 n+1 到达其范围的末尾之前处理第一个索引)。

这确实给我们带来了问题之一。您的问题不是特别大,单个内核上的 5600 次迭代将花费很短的时间(考虑到与循环的每次迭代相关的操作很少)并且并行化的开销可能比您获得的更大。人们希望编译器优化问题,以便在 while 循环之前创建线程,然后简单地重用它们,但这将是一种相当激进的优化,我不能说我知道它是否知道它应该这样做那个(这也可能取决于您没有共享的编译选项,这样做可能很有用)。您可以尝试自己通过移动 #pragma omp parallel 指令来做到这一点,然后只在里面添加 #pragma omp for

您可能会看到 16 个线程时性能下降的另一个原因是超线程。超线程只会在您的线程必须等待经常发生的事情(例如缓存未命中)时提高性能。在您的情况下,这似乎不太可能,并且完全有可能单个线程已经完全利用了核心。在这种情况下,超线程会损害性能,因为操作系统会尝试在核心上给每个线程一些时间,而这些开关不会免费。

最后,由于您的操作非常琐碎,在更大的情况下,您可能会遇到内存带宽的瓶颈。但是,对于小到足以仅将其作为一个整体存储在缓存中的矩阵,情况不应该如此。

但是,确定瓶颈在哪里的最佳方法是进行测试。您可以使用诸如 Intel VTune Amplifier 之类的工具来检查您的代码如何使用可用的 CPU 资源,这样您就可以轻松地确定您的代码大部分时间都花在了哪些方面。

【讨论】:

正如您所说,您希望优化算法,从优化循环嵌套和允许 simd 优化等基础知识开始似乎很重要。如果您有错误共享,那么纠正循环嵌套似乎会有所帮助。

以上是关于使用 openmp 并行化矩阵以避免错误共享的主要内容,如果未能解决你的问题,请参考以下文章

C++ Armadillo 和 OpenMp:外积求和的并行化 - 定义 Armadillo 矩阵的约简

OpenMp实现并行化

如何并行化使用 boost?

如何使用带有串行内循环的openMP并行化外循环以进行数组添加

使用 OpenMP 在 C、C++ 中并行化嵌套 for 循环的几种方法之间的区别

如何使用 OpenMP 通过 C++ std::list 并行化 for 循环?