这个问题是关于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 })并将测量的运行时标准化为元素数。
您可以看到,对于足够小的NxN
,每个操作的运行时间是不变的。但是,在N
以上,运行时会跳起来。蓝色和红色数据点之间的差异是处理器上的负载。如果示例程序几乎单独运行,则运行时由蓝点给出,当其他核忙时,时间由红点表示。
我现在有几个与此相关的问题。
N=512
和N=512
之间的跳转与我的处理器(Ivy Bridge i5-3570)的L3缓存大小有关,该缓存应该是6MB? N=1024
大约等于2MB,512*512*8byte
大约为8MB。因此矩阵不再适合缓存,因此从RAM中获取数据是执行时间更长的原因吗?1024*1024*8byte
我很想听听你的想法。谢谢!
答案 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缓存的效果更加明显。