4

我写了一些 Naiive GEMM 代码,我想知道为什么它比等效的单线程 GEMM 代码慢得多。

使用 200x200 矩阵,单线程:7 毫秒,多线程:108 毫秒,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);
    }
4

3 回答 3

4

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

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

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

项目 2. 和 3. 在您的示例中可能不起作用:您在 12 个(超线程)内核上使用 12 个线程,并且您的算法不涉及锁。

但是,1. 可能与您的情况相关:您正在创建总共 40000 个线程,每个线程相乘和相加 200 个值。我建议尝试不那么细粒度的线程,也许只在第一个循环后拆分。最好不要将问题分解成比必要的更小的部分。

4.也很可能对您的情况很重要。虽然在将结果写入数组时不会遇到竞争条件(因为每个线程都在写入自己的索引位置),但您很可能会引发大量缓存同步开销。

“为什么?” 您可能会想,因为您正在写入内存中的不同位置。这是因为典型的 CPU 缓存是按缓存线组织的,在当前的 Intel 和 AMD CPU 型号上,缓存线的长度为 64 字节。这是更改某些内容时可用于从缓存传输到缓存的最小大小。现在所有 CPU 内核都在读取和写入相邻的内存字,这会导致在所有内核之间同步 64 字节,只要您只写入 4 个字节(或 8 个,取决于您使用的数据类型的大小)。

如果内存不是问题,您可以简单地用“虚拟”数据“填充”每个输出数组元素,以便每个高速缓存行只有一个输出元素。如果您使用 4 字节数据类型,这意味着每 1 个实际数据元素跳过 15 个数组元素。当您减少线程的细粒度时,缓存问题也会得到改善,因为每个线程实际上都会访问其自己的内存中连续区域,而不会干扰其他线程的内存。

编辑:Herb Sutter(C++ 大师之一)更详细的描述可以在这里找到:http ://www.drdobbs.com/parallel/maximize-locality-minimize-contention/208200273

Edit2:顺便说一句,建议std::move在 return 语句中避免,因为这可能会妨碍返回值优化和复制省略规则,而标准现在要求这些规则自动发生。请参阅在多个返回语句的情况下使用 `std::move` 返回是否合理?

于 2013-02-11T12:56:02.863 回答
3

多线程意味着始终同步、上下文切换、函数调用。这一切都加起来并消耗 CPU 周期,您可以将其花费在主要任务本身上。

如果您只有第三个嵌套循环,您可以保存所有这些步骤并且可以内联而不是子例程进行计算,您必须在其中设置堆栈,调用,切换到不同的线程,返回结果并切换回主程序线。

仅当这些成本与主要任务相比较小时,多线程才有用。我想,当矩阵大于 200x200 时,您会看到多线程的更好结果。

于 2013-02-11T12:17:46.367 回答
1

一般来说,多线程非常适用于需要大量时间的任务,最有利的是因为复杂性而不是设备访问。您向我们展示的循环需要很短的执行时间才能有效地并行化。

您必须记住,线程创建有很多开销。同步也有一些(但明显更少)的开销。

于 2013-02-11T13:23:37.300 回答