gpt4 book ai didi

c++ - 多线程 GEMM 比单线程 GEMM 慢?

转载 作者:塔克拉玛干 更新时间:2023-11-03 01:24:11 29 4
gpt4 key购买 nike

我写了一些 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);
}

最佳答案

我没有使用 GEMM 的经验,但你的问题似乎与各种多线程场景中出现的问题有关。

使用多线程时,您会引入一些潜在的开销,其中最常见的通常是

  1. 创建/清理开始/结束线程
  2. 当(线程数)>(CPU 核心数)时上下文切换
  3. 锁定资源,等待获取锁
  4. 缓存同步问题

第 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:顺便说一句,建议避免在 return 语句中使用 std::move,因为这可能会妨碍返回值优化和复制省略规则,这些规则现在是标准要求自动发生。参见 Is returning with `std::move` sensible in the case of multiple return statements?

关于c++ - 多线程 GEMM 比单线程 GEMM 慢?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/14811301/

29 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com