【问题标题】:Matrix-Multiplication: Why non-blocked outperforms blocked?矩阵乘法:为什么非阻塞优于阻塞?
【发布时间】:2016-11-06 11:57:18
【问题描述】:

我正在尝试通过阻塞循环来提高缓存性能来加速矩阵乘法算法,但非阻塞版本无论矩阵大小、块大小如何都保持显着更快(我尝试了很多介于 2 和200、2 等的势能)和优化级别。

非阻塞版本:

  for(size_t i = 0; i < n; ++i)
  {
    for(size_t k = 0; k < n; ++k)
    {
      int r = a[i][k];
      for(size_t j = 0; j < n; ++j)
      {
        c[i][j] += r * b[k][j];
      }
    }
  }

屏蔽版本:

  for(size_t kk = 0; kk < n; kk += BLOCK)
  {
    for(size_t jj = 0; jj < n; jj += BLOCK)
    {
      for(size_t i = 0; i < n; ++i)
      {
        for(size_t k = kk; k < kk + BLOCK; ++k)
        {
          int r = a[i][k];
          for(size_t j = jj; j < jj + BLOCK; ++j)
          {
            c[i][j] += r * b[k][j];
          }
        }
      }
    }
  }

我还有一个 bijk 版本和一个 6-loops bikj 版本,但它们的性能都优于非阻塞版本,我不明白为什么会发生这种情况。我遇到的每篇论文和教程似乎都表明被阻止的版本应该更快。如果这很重要,我会在 Core i5 上运行它。

【问题讨论】:

  • 什么编译器?有哪些优化设置?还有太多变数。

标签: c caching matrix-multiplication cpu-architecture


【解决方案1】:

尝试仅在一个维度上阻止,而不是在两个维度上。

矩阵乘法详尽地处理两个矩阵中的元素。左边矩阵的每一行向量都被重复处理,取到右边矩阵的连续列中。

如果矩阵不能同时放入缓存,某些数据最终会被多次加载。

我们可以做的是分解操作,以便我们一次处理大约缓存大小的数据量。我们希望缓存左操作数的行向量,因为它被重复应用于多个列。但是我们应该只取足够的列(一次)以保持在缓存的限制范围内。例如,如果我们只能取 25% 的列,这意味着我们将不得不四次传递行向量。我们最终从内存中加载了四次左矩阵,而右矩阵只加载了一次。

(如果要多次加载任何内容,则应该是左侧的行向量,因为它们在内存中是平坦的,这得益于突发加载。许多缓存架构可以执行从内存到相邻缓存的突发加载行比随机访问加载快。如果正确的矩阵以列优先顺序存储,那就更好了:然后我们在平面数组之间进行交叉乘积,可以很好地预取到内存中。)

我们也不要忘记输出矩阵。输出矩阵也占用缓存空间。

我怀疑 2D 块方法的一个缺陷是输出矩阵的每个元素都依赖于两个输入:左侧矩阵中的整行和右侧矩阵中的整列。如果矩阵被分块访问,这意味着每个目标元素被多次访问以累积部分结果。

如果我们做一个完整的行列点积,我们不必多次访问c[i][j];一旦我们将列j 放入行i,我们就完成了c[i][j]

【讨论】:

  • 我只在一个维度上尝试了阻塞。 kk-ikj 循环获得了最佳性能,并成功地以 2 倍击败了非阻塞版本。选择 k 循环作为要阻塞的循环实际上是有道理的,因为这意味着将遭受最多缓存未命中的矩阵是输出一个,但写入永远不会使处理器停止,因此它不会影响吞吐量。
  • @terratinkah:指令仍然必须按顺序退出,因此缓存未命中存储最终会阻止新的微指令进入 ROB。 Re-Order Buffer 约为 200 uop,这不足以完全隐藏必须等待主内存的缓存未命中的延迟。 (经过良好调整的代码每个时钟将执行超过 2 微指令,每个时钟最多可以执行 4 个融合域微指令)。
猜你喜欢
  • 2018-11-15
  • 2012-10-31
  • 1970-01-01
  • 2017-04-26
  • 2016-07-06
  • 1970-01-01
  • 2012-06-13
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多