【问题标题】:Why does transposing matrix before multiplication results in great speed-up为什么在乘法之前转置矩阵会导致很大的加速
【发布时间】:2013-10-25 03:11:01
【问题描述】:

听说由于缓存局部性,在乘法之前转置矩阵会大大加快运算速度。所以我写了一个简单的 C++ 程序来测试它的行优先顺序(编译需要 C++11 和 boost)。

结果令人惊讶:7.43 秒对 0.94 秒。但我不明白为什么它会加速。事实上,在第二个版本(第一个转置)中,乘法代码通过 stride-1 模式访问数据,并且比第一个版本具有更好的局部性。但是,要转置矩阵 B,也必须非顺序地访问数据,并且还会导致大量缓存未命中。分配内存和复制数据的开销也应该是不可忽略的。那么为什么第二个版本的代码速度会这么快呢?

#include <iostream>
#include <vector>
#include <boost/timer/timer.hpp>
#include <random>

std::vector<int> random_ints(size_t size)
{
    std::vector<int> result;
    result.reserve(size);
    std::random_device rd;
    std::mt19937 engine(rd());
    std::uniform_int_distribution<int> dist(0, 100);
    for (size_t i = 0; i < size; ++i)
        result.push_back(dist(engine));
    return result;
}

// matrix A: m x n; matrix B: n x p; matrix C: m x n;
std::vector<int> matrix_multiply1(const std::vector<int>& A, const std::vector<int>& B, size_t m, size_t n, size_t p)
{
    boost::timer::auto_cpu_timer t;
    std::vector<int> C(m * p);
    for (size_t i = 0; i < m; ++i)
    {
        for (size_t j = 0; j < p; ++j)
        {
            for (size_t k = 0; k < n; ++k)
            {
                C[i * m + j] += A[i * m + k] * B[k * n + j];
                // B is accessed non-sequentially
            }
        }
    }
    return C;
}

// matrix A: m x n; matrix B: n x p; matrix C: m x n;
std::vector<int> matrix_multiply2(const std::vector<int>& A, const std::vector<int>& B, size_t m, size_t n, size_t p)
{
    boost::timer::auto_cpu_timer t;
    std::vector<int> C(m * p), B_transpose(n * p);

    // transposing B
    for (size_t i = 0; i < n; ++i)
    {
        for (size_t j = 0; j < p; ++j)
        {
            B_transpose[i + j * p] = B[i * n + j];
            // B_transpose is accessed non-sequentially
        }
    }

    // multiplication
    for (size_t i = 0; i < m; ++i)
    {
        for (size_t j = 0; j < p; ++j)
        {
            for (size_t k = 0; k < n; ++k)
            {
                C[i * m + j] += A[i * m + k] * B_transpose[k + j * p];
                // all sequential access
            }
        }
    }
    return C;
}

int main()
{
    const size_t size = 1 << 10;
    auto A = random_ints(size * size);
    auto C = matrix_multiply1(A, A, size, size, size);
    std::cout << C.front() << ' ' << C.back() << std::endl; // output part of the result
    C = matrix_multiply2(A, A, size, size, size);
    std::cout << C.front() << ' ' << C.back() << std::endl; // compare with output of algorithm 1
    return 0;
}

【问题讨论】:

  • 我建议修改代码以交替尝试每种算法并重复三到四次。否则,加速可能是由于缓存预热、分支预测或其他与您的测试方式有关的因素,而不是代码优化。
  • @DavidSchwartz:我尝试了你的建议。没有区别。
  • 尝试分别计时转置和乘法。这可能会告诉你更多。

标签: c++ optimization matrix-multiplication


【解决方案1】:

乘法比转置涉及更多的访问,因此它支配了执行时间。

您可以通过查看 for 循环标头非常清楚地看到这一点:

// transpose
for (size_t i = 0; i < n; ++i)
    for (size_t j = 0; j < p; ++j)
        ...

// multiplication
for (size_t i = 0; i < m; ++i)
    for (size_t j = 0; j < p; ++j)
        for (size_t k = 0; k < n; ++k)
            ...

有了额外的嵌套,第二个显然工作量更大。

【讨论】:

  • 这是有道理的。但我期待的更像是 CPU 缓存的神奇属性,但我并没有完全理解。
猜你喜欢
  • 1970-01-01
  • 2012-11-13
  • 2020-10-01
  • 1970-01-01
  • 2018-04-11
相关资源
最近更新 更多