我写了一些Naiive 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);
}
答案 0 :(得分:4)
我没有GEMM的经验,但您的问题似乎与所有类型的多线程场景中出现的问题有关。
使用多线程时,会引入一些潜在的开销,其中最常见的开销通常是
项目2和3.可能在您的示例中不起作用:您在12个(超线程)核心上使用12个线程,并且您的算法不涉及锁定。
但是,1。在您的情况下可能是相关的:您正在创建总共40000个线程,每个线程相乘并添加200个值。我建议尝试一个不太细粒度的线程,可能只在第一个循环后分裂。不要将问题分成小于必要的部分,这总是一个好主意。
同样4.在你的情况下很可能很重要。虽然在将结果写入数组时没有遇到竞争条件(因为每个线程都写入自己的索引位置),但很可能会引发缓存同步的大量开销。
“为什么?”你可能会想,因为你在写作记忆的不同地方。这是因为典型的CPU缓存是在缓存行中组织的,当前的Intel和AMD CPU型号都是64字节长。当更改某些内容时,这是可用于从缓存传输到缓存的最小大小。既然所有CPU内核都在读取和写入相邻的存储器字,那么只要你只写4个字节(或8个,取决于你正在使用的数据类型的大小),这就会导致所有内核之间的64字节同步。
如果内存不是问题,您只需使用“虚拟”数据“填充”每个输出数组元素,这样每个缓存行只有一个输出元素。如果您使用4字节数据类型,则意味着为每个1个实数数据元素跳过15个数组元素。当你使线程不那么精细化时,缓存问题也会得到改善,因为每个线程都会在内存中访问自己的连续区域,而不会干扰其他线程的内存。
编辑:Herb Sutter(C ++大师之一)的更详细描述可以在这里找到:http://www.drdobbs.com/parallel/maximize-locality-minimize-contention/208200273
Edit2:BTW,建议在return语句中避免std::move
,因为这可能会妨碍返回值优化和复制省略规则,标准现在要求自动发生这种规则。见Is returning with `std::move` sensible in the case of multiple return statements?
答案 1 :(得分:3)
多线程意味着始终同步,上下文切换,函数调用。这一切都会增加CPU成本,并且可以花费在主要任务本身上。
如果您只有第三个嵌套循环,则保存所有这些步骤,并且可以进行内联计算而不是子例程,您必须在其中设置堆栈,调用,切换到其他线程,返回结果并切换回到主线程。
如果这些成本与主要任务相比较小,则多线程仅有用。我想,当矩阵大于200x200时,你会看到更好的多线程结果。
答案 2 :(得分:1)
通常,多线程很适用于花费大量时间的任务,最有利的是因为复杂性而不是设备访问。你向我们展示的循环需要缩短执行时间才能有效地并行化。
你必须记住线程创建有很多开销。同步还有一些(但显着减少)开销。