我写了一些简单的GEMM代码,我想知道为什么它比同等的单线程GEMM代码慢得多。
200x200矩阵,单线程: 7ms,多线程: 108ms,CPU: 3930k,线程池中有12个线程。
template <unsigned M, unsigned N, unsigned P, typename T>
static Matrix<M, P, T> multiply( const Matrix<M, N, T> &lhs, const Matrix<N, P, T> &rhs, ThreadPool & pool )
{
Matrix<M, P, T> result = {0};
Task<void> task(pool);
for (auto i=0u; i<M; ++i)
for (auto j=0u; j<P; j++)
task.async([&result, &lhs, &rhs, i, j](){
T sum = 0;
for (auto k=0u; k < N; ++k)
sum += lhs[i * N + k] * rhs[k * P + j];
result[i * M + j] = sum;
});
task.wait();
return std::move(result);
}发布于 2013-02-11 20:56:03
我没有使用GEMM的经验,但您的问题似乎与所有类型的多线程场景中出现的问题有关。
在使用多线程时,您会引入一些潜在的开销,其中最常见的通常是
启动/结束threads
条目2.和3.可能在您的示例中不起作用:您在12个(超线程)核心上使用12个线程,并且您的算法不涉及锁。
然而,1.可能与您的情况相关:您总共创建了40000个线程,其中每个线程都会相乘和相加200值。我建议尝试一种不那么细粒度的线程,也许只在第一次循环后拆分。不要把问题分成不必要的小块总是一个好主意。
4.在你的情况下很可能是很重要的。虽然在将结果写入数组时不会遇到竞争条件(因为每个线程都在写入自己的索引位置),但很可能会引起大量的缓存同步开销。
“为什么?”你可能会这样想,因为你在写内存中的不同地方。这是因为典型的CPU缓存是在缓存线中组织的,在当前的Intel和AMD CPU型号上,缓存线是64字节长。当某些内容发生更改时,这是可用于传入和传出缓存的最小大小。现在所有CPU核心都在读写相邻的内存字,这导致当您只写入4个字节(或8个字节,取决于您使用的数据类型的大小)时,所有核心之间将同步64个字节。
如果内存不是问题,您可以简单地用“虚拟”数据“填充”每个输出数组元素,这样每个缓存线只有一个输出元素。如果你使用4byte数据类型,这意味着每1个实数数据元素跳过15个数组元素。当你减少线程的细粒度时,缓存问题也会得到改善,因为每个线程实际上都会访问自己在内存中的连续区域,而不会干扰其他线程的内存。
编辑: Herb Sutter (C++大师之一)的更详细的描述可以在这里找到:http://www.drdobbs.com/parallel/maximize-locality-minimize-contention/208200273
Edit2:顺便说一句,建议在返回语句中避免std::move,因为这可能会妨碍返回值优化和复制省略规则,而标准现在要求这些规则自动发生。请参阅Is returning with std::move sensible in the case of multiple return statements?
发布于 2013-02-11 20:17:46
多线程意味着总是同步、上下文切换、函数调用。所有这些加起来会消耗CPU周期,你可以花在主要任务本身上。
如果只有第三个嵌套循环,则可以保存所有这些步骤,并可以内联执行计算,而不是子例程,在子例程中,必须设置堆栈,调用,切换到不同的线程,返回结果,然后切换回主线程。
只有在这些开销与主任务相比较小的情况下,多线程才有用。我猜,当矩阵大于200x200的时候,你会看到多线程的效果更好。
发布于 2013-02-11 21:23:37
一般来说,多线程非常适用于耗费大量时间的任务,最有利的原因是复杂性,而不是设备访问。你向我们展示的循环需要很短的时间来执行,这样它才能有效地并行化。
您必须记住,创建线程有很多开销。同步也有一些(但明显更少)的开销。
https://stackoverflow.com/questions/14811301
复制相似问题