矩阵向量乘法优化 - 缓存大小

时间:2016-04-13 11:33:43

标签: c++ performance optimization matrix

这个问题是关于C ++优化技术的。我有一个大尺寸的矩阵向量乘法,并希望减少运行时间。我知道有专门的线性代数库,但我实际上想了解一下底层处理器的特性。到目前为止,我正在使用\ O2(Microsoft)进行编译,并且我得到了编译器以确认乘法的内部循环是向量化的。

示例代码为:

#include <stdio.h>
#include <ctime>
#include <iostream>

#define VEC_LENGTH 64
#define ITERATIONS 4000000

void gen_vector_matrix_multiplication(double *vec_result, double *vec_a, double *matrix_B, unsigned int cols_B, unsigned int rows_B)
{
    // initialise result vector
    for (unsigned int i = 0; i < rows_B; i++)
    {
        vec_result[i] = 0;
    }
    // perform multiplication
    for (unsigned int j = 0; j < cols_B; j++)
    {
        const double entry = vec_a[j];
        const int col = j*rows_B;

        for (unsigned int i = 0; i < rows_B; i++)
        {
            vec_result[i] += entry * matrix_B[i + col];
        }
    }
}

int main()
{
    double *vec_a = new double[VEC_LENGTH];
    double *vec_result = new double[VEC_LENGTH];
    double *matrix_B = new double[VEC_LENGTH*VEC_LENGTH];

    // start clock
    clock_t begin = clock();

    // this outer loop is just for test purposes so that the timing becomes meaningful
    for (unsigned int i = 0; i < ITERATIONS; i++)
    {
        gen_vector_matrix_multiplication(vec_result, vec_a, matrix_B, VEC_LENGTH, VEC_LENGTH);
    }

    // stop clock
    double elapsed_time = static_cast<double>(clock() - begin) / CLOCKS_PER_SEC;
    std::cout << elapsed_time/(VEC_LENGTH*VEC_LENGTH) << std::endl;

    delete[] vec_a;
    delete[] vec_result;
    delete[] matrix_B;

    return 1;
}

乘法完成几次以获得运行时的可靠估计。我已经测量了许多不同向量长度的运行时间(在这个例子中,只有一个元素N,它是向量的长度,同时定义了矩阵的大小{{1 })并将测量的运行时标准化为元素数。

enter image description here

您可以看到,对于足够小的NxN,每个操作的运行时间是不变的。但是,在N以上,运行时会跳起来。蓝色和红色数据点之间的差异是处理器上的负载。如果示例程序几乎单独运行,则运行时由蓝点给出,当其他核忙时,时间由红点表示。

我现在有几个与此相关的问题。

  • 我是否正确假设N=512N=512之间的跳转与我的处理器(Ivy Bridge i5-3570)的L3缓存大小有关,该缓存应该是6MB? N=1024大约等于2MB,512*512*8byte大约为8MB。因此矩阵不再适合缓存,因此从RAM中获取数据是执行时间更长的原因吗?
  • 运行时间稳定增加超过此阈值的原因是什么?
  • 有关繁忙和空闲处理器曲线在阈值之上如此不同的原因的任何想法?
  • 使用1024*1024*8byte
  • 优化此乘法例程以进行操作时,合理的后续步骤是什么?

我很想听听你的想法。谢谢!

2 个答案:

答案 0 :(得分:2)

优化此类代码的一个重要方面是处理别名和矢量化,您的帖子表明您已经处理了后者。编译器通常需要一些帮助。在GCC 5.3.0上,使用下面的循环可以大大减少运行时间。 __restrict__限定符告诉编译器没有可能的别名,#pragma GCC ivdep告诉GCC编译器可以向量化代码。此外,编译器标志也非常重要。我使用g++ -O3 -march=native -mtune=native matrix_example.cxx编译了代码。

void gen_vector_matrix_multiplication(double* const __restrict__ vec_result,
                                      const double* const __restrict__ vec_a,
                                      const double* const __restrict__ matrix_B,
                                      const int cols_B,
                                      const int rows_B)
{
    // initialise result vector
#pragma GCC ivdep
    for (int i = 0; i < rows_B; i++)
        vec_result[i] = 0;

    // perform multiplication
    for (int j = 0; j < cols_B; j++)
    {
        const double entry = vec_a[j];
        const int col = j*rows_B;

#pragma GCC ivdep
        for (int i = 0; i < rows_B; i++)
        {
            vec_result[i] += entry * matrix_B[i + col];
        }
    }
}

答案 1 :(得分:1)

为了规范化,我选择了

elapsed_time/(VEC_LENGTH*VEC_LENGTH*ITERATIONS)

并以6纳秒开始,从N = 64到N = 8192以7纳秒结束

ITERATIONS=20

并且只有缓存的所有情况都是“vec_a”,因此只有矩阵元素才能从内存中读取大型矩阵。

内存带宽约为20 GB / s,这意味着每秒超过2 G双倍。 核心频率为3.7 GHz,因此最大值为3.7 G乘法

核心每秒可发出3.7 G双打,但内存每秒输入2 G含义。

当然这只适用于64位fp操作。还有

 i + col

必须在乘法之前完成,所以这是一个串行执行。 3.7 GHz的2条指令有效地接近1.8 G /秒。接近2。 即使缓存完成其工作,cpu核心也缺乏此串行代码的计算能力。

当循环展开4时发生同样的事情。这减少了一半的时间!现在每个操作的时间为3.4纳秒,但对于所有N个值,因为在cpu必须执行的1个单位的内存带宽之后仍有2个指令(1个整数和1个浮点)。

编辑:使用所有核心将超过内存带宽,并使L3缓存的效果更加明显。