[英]Effect of cache misses on time of matrix multiplication
我正在尝试从较小的矩阵大小开始逐步进行矩阵乘法,以期观察一旦矩阵不再适合高速缓存,时间将突然改变。 但是令我失望的是,我总是得到一个看似相同的函数的非常平滑的图形。 我尝试从小到4x4
的矩阵开始,并逐渐3400x3400
其增加到3400x3400
(等于11MB
整数值),但是时间函数没有变化。 可能是我在这里缺少一些关键点。 任何帮助将不胜感激。
这是我的C ++代码:
long long clock_time() {
struct timespec tp;
clock_gettime(CLOCK_REALTIME, &tp);
return (long long)(tp.tv_nsec + (long long)tp.tv_sec * 1000000000ll);
}
int main()
{
for(int matrix_size = 100; matrix_size < 3500; matrix_size += 100)
{
int *A = new int[matrix_size*matrix_size];
int *B = new int[matrix_size*matrix_size];
int *C = new int[matrix_size*matrix_size];
long long start = clock_time();
for(int i = 0; i < matrix_size; ++i)
for(int j = 0; j < matrix_size; ++j)
for(int k = 0; k < matrix_size; ++k)
{
C[i + j*matrix_size] = A[i + k*matrix_size] * B[k + j*matrix_size];
}
long long end = clock_time();
long long totalTime = (end - start);
std::cout << matrix_size << "," << totalTime << std::endl;
delete[] A;
delete[] B;
delete[] C;
}
std::cout << "done" ;
return 0;
}
有关详细数据, 请访问https://docs.google.com/spreadsheets/d/1Xtri8w2sLZLQE0566Raducg7G2L4GLqNYIvP4nrp2t8/edit?usp=sharing
更新 :根据浙源和弗兰克的建议,我没有用值i+j
初始化我的矩阵并将时间除以2*N^3
for(int i = 0; i < matrix_size; i++)
{
for(int j = 0; j < matrix_size; j++)
{
A[i + j * matrix_size] = i+j;
B[i + j * matrix_size] = i+j;
B[i + j * matrix_size] = i+j;
}
}
结果如下:
好吧,您一定会观察到定时的三次曲线。 假设您使用两个N * N
平方矩阵,则矩阵乘法的复杂度或浮点运算(FLOP)的数量为2 * N ^ 3)
。 随着N
增加,FLOP的增加将主导时间的增加,您将不会轻易观察到延迟问题。
如果要调查延迟的纯粹影响,则应按FLOP量“标准化”时序:
measured time / (2 * N ^ 3)
或者:
(2 * N ^ 3) / measured time
前者是每FLOP平均花费的时间,而后者给你每秒翻牌 ,通常被称为FLOPS文学。 FLOP是性能的主要指标(至少对于科学计算而言)。 可以预期的是,随着N
增加,前一个指标将出现上行跳跃(增加的延迟),而后一个指标将出现下行跳跃(降低的性能)。
抱歉,我没有编写C ++,因此无法修改您的代码(但这很简单,因为您只需要进行2 * N ^ 3
除法即可)。 我曾经用C代码做过相同的实验,这是我在Intel Core 2 Duo上的结果。 注意,我报告的是MFLOP,即10 ^ 6
FLOP。 该图实际上是在R软件中生成的。
我的上述观察确实假设您正确地理解了所有其他内容。 但是实际上,事实并非如此。
首先 ,矩阵乘法是:
C[i + j*matrix_size] += A[i + k*matrix_size] * B[k + j*matrix_size];
注意+=
而不是=
。
其次 ,您的循环嵌套设计不当。 您正在执行矩阵乘法C = A * B
,其中所有矩阵均以列主顺序存储,因此您应提防循环嵌套顺序,以确保最内层循环始终具有stride-1访问权限。 众所周知,在这种情况下, jki
循环嵌套是最佳的。 因此,请考虑以下事项:
for(int j = 0; j < matrix_size; ++j)
for(int k = 0; k < matrix_size; ++k)
for(int i = 0; i < matrix_size; ++i)
{
C[i + j*matrix_size] += A[i + k*matrix_size] * B[k + j*matrix_size];
}
第三 ,从矩阵大小100 * 100
,该大小已经在L1缓存之外,大部分为64KB。 我建议您从N = 24
开始。 一些文献表明, N = 60
大约是此类缓存的边界值。
第四 ,您需要多次重复乘法以消除测量偏差。 目前,对于每个试验N
(或代码中的matrix_size
),您只需进行一次乘法并测量时间。 这是不准确的。 对于小N
您会得到虚假的计时。 重复(1000 / N + 1) ^ 3
次如何?
N
很小时,您会重复很多次; N
越来越接近1000
,您重复的次数越少; N > 1000
,您基本上只做一次乘法。 当然,不要忘记您需要将测量的时间除以重复的时间。
当然,还有其他一些地方可以优化代码,例如在地址计算中使用常量寄存器和消除整数乘法,但是它们的重要性不大,因此未涉及。 数组的初始化也被跳过了,因为Frank的答案已经提到了它。
您没有在数组内部初始化数据,因此您的系统可能分配了一页写时复制内存,并将所有数组映射到该页面。
简而言之,A和B始终占用总共4096字节的硬件内存。 而且由于缓存是基于硬件地址(而不是虚拟地址)完成的,因此您实际上始终处于缓存中。
使用随机数据初始化A和B将按照您的要求强制分配实际的硬件内存。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.