向量的并行求和

Posted

技术标签:

【中文标题】向量的并行求和【英文标题】:Parallel Sum for Vectors 【发布时间】:2011-09-08 15:14:50 【问题描述】:

有人可以就如何通过多线程减少以下 for 循环的运行时间提供一些建议吗?假设我还有两个向量,分别称为“a”和“b”。

for (int j = 0; j < 8000; j++)
    // Perform an operation and store in the vector 'a'
    // Add 'a' to 'b' coefficient wise

这个 for 循环在我的程序中执行了很多次。上面 for 循环中的两个操作已经优化过了,但是它们只运行在一个内核上。但是,我有 16 个内核可用,并希望使用它们。

我尝试如下修改循环。我没有向量“a”,而是有 16 个向量,并假设第 i 个向量称为 a[i]。我的 for 循环现在看起来像

for (int j = 0; j < 500; j++)
    for (int i = 0; i < 16; i++)
        // Perform an operation and store in the vector 'a[i]'
    
    for (int i = 0; i < 16; i++)
        // Add 'a[i]' to 'b' coefficient wise
    


我通过在每个内部循环之前添加“#pragma omp parallel for”来对内部的每个 for 循环使用 OpenMp。我所有的处理器都在使用,但我的运行时间只会显着增加。有人对如何减少此循环的运行时间有任何建议吗?先感谢您。

【问题讨论】:

您是否分析过您的代码以查看瓶颈在哪里? 这可能是因为也许在优化后你的代码不能被分解成更小的片段,如果你原来的 for 只是做类似a[i] += b[i] 的事情,那么你可以在 for 之前添加那个 pragma 标签。它会随心所欲地提升你的表现。 如果你的循环体真的那么微不足道,那么你可能会受到内存带宽的限制,更多的核心也无济于事(因为内存带宽已经饱和)。在更高的级别上重新安排以在循环内找到更多工作要做,或者让机器具有更快的 RAM。 @GWW,是的,我已经分析了我的代码,它报告说调用最多的函数是 for 循环中的第一个函数(即对向量执行操作)。但是,正如我已经说过的,这个功能已经被优化了。 @Gajet,我的原始代码使用向量数学库,因此每个操作都是在整个向量上执行的,例如。 b += a。 再想一想。为这种琐碎的操作分叉线程不太可能有帮助。确保您的编译器正确矢量化您的循环。 (如果是 GCC,使用 -O3。) 【参考方案1】:

omp 为您的程序创建线程,无论您在何处插入 pragma 标记,它都会为内部标记创建线程,但问题是创建了 16 个线程,每个线程执行 1 次操作,然后使用您的方法将它们全部销毁。创建和销毁线程需要很多时间,因此您使用的方法会增加进程的总时间,尽管它使用了所有 16 个内核。您不必创建内部 fors 只需在 8000 循环之前放置 #pragma omp parallel for 标记,这取决于 omp 来分隔踏板之间的值,因此您为创建第二个循环所做的工作是 omp 的工作。这样 omp 只创建一次线程,然后使用每个线程处理 500 个数字,然后结束所有这些数字(使用 499 少线程创建和销毁)

【讨论】:

这是我第一次使用 OpenMp,但我在 main 方法中执行了 'omp_set_num_threads(16)'。它不充当线程池,不破坏任何线程吗? 线程就像函数一样,你不能在它们的列表中添加或删除任何操作。只需将线程创建函数视为将函数指针作为输入的东西。 omp_set_num_threads 顺便说一句是可选的,如果你不把它放在那里 omp 自己会选择它应该创建多少线程来优化你的代码,设置只会强制 omp 使用你指定的使用量。 wiki page for omp 包含创建简单 omp 程序所需的几乎所有数据,您只需在进行任何调试之前阅读它即可。【参考方案2】:

其实我是要把这些cmets放在一个答案里。

为琐碎的操作分叉线程只会增加开销。

首先,确保您的编译器使用向量指令来实现您的循环。 (如果它不知道怎么做,你可能不得不自己编写向量指令;尝试搜索“SSE instrinsics”。但是对于这种简单的向量添加,自动向量化应该是可能的。)

假设你的编译器是一个相当现代的 GCC,调用它:

gcc -O3 -march=native ...

添加 -ftree-vectorizer-verbose=2 以了解它是否自动矢量化了您的循环以及原因。

如果您已经在使用向量指令,那么您的内存带宽可能已经饱和。现代 CPU 内核非常快......如果是这样,您需要在更高级别进行重组,以便在循环的每次迭代中获得更多操作,找到对适合 L1 缓存的块执行大量操作的方法。

【讨论】:

感谢您的评论。我确实启用了 SSE2,并且正在使用 Microsoft 的编译器和 Visual Studio 2010 IDE。我会考虑到这一点,看看我是否可以重组其他任何东西。 我建议运行 -S 来查看程序集并确保它使用的是 SSE2 指令。【参考方案3】:

Does anyone have any suggestions on how I can decrease the runtime of this loop?

for (int j = 0; j < 500; j++)  // outer loop
  for (int i = 0; i < 16; i++)  // inner loop

始终尝试使外循环迭代次数少于内循环。这将使您免于多次内循环初始化。在上面的代码中,内部循环i = 0; 被初始化500 次。现在,

for (int i = 0; j < 16; i++)  // outer loop
  for (int j = 0; j < 500; j++)  // inner loop

现在,内部循环 j = 0; 只初始化了 16 次! 如果有任何影响,请尝试相应地修改您的代码。

【讨论】:

在现代 CPU 上,这个建议完全是倒退的。 small 内部循环序列更有可能对 L1 缓存中的数据进行操作,这比初始化循环计数器的开销重要 100-1000 倍。 @Nemo,谢谢,我不知道。但是,在这里您可能指的是 smaller 循环而不是 inner 循环,对吗?这也取决于缓存大小。如果缓存大小真的能够容纳更大的循环那么,关于逻辑仍然成立。 是的。关键是缓存友好,现在典型的 L1 缓存大约是 32K。所以你要确保你的内部循环接触的数据比那个少......

以上是关于向量的并行求和的主要内容,如果未能解决你的问题,请参考以下文章

并行减少(例如求和)hpx::futures<double> 的向量

Python 中的多处理:Numpy + 向量求和 -> 大幅减速

机器学习中的数学:向量求和符号累乘符号

创建一个求和函数以仅对向量的一部分求和

多线程向量求和的可扩展性

LLVM IR:有效地对向量求和