缓存未命中对矩阵乘法时间的影响

时间:2017-01-03 21:36:46

标签: c++ caching matrix

我正在尝试进行矩阵乘法,从一个小的矩阵大小开始并逐渐增加它,希望观察一旦矩阵不再适合缓存,时间会突然改变。但令我失望的是,我总是得到一个非常流畅的图表,显然是相同的功能。我试着从像4x4一样小的矩阵开始,逐渐增加直到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;
}

以下是我获得的数据示例图: enter image description here

详细数据可在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;
    }

}

结果如下:

enter image description here

更新2:在交换ij循环后: enter image description here

2 个答案:

答案 0 :(得分:3)

嗯,你肯定会观察时间的三次曲线。假设您使用两个N * N平方矩阵,那么矩阵乘法在2 * N ^ 3)处具有复杂性或浮点运算量(FLOP)。随着N的增加,FLOP的增加主导着时间增长,并且您不会轻易地观察到延迟问题。

如果你想调查潜伏期的绝对影响,你应该&#34;规范化&#34;按FLOP数量计算您的时间:

measured time / (2 * N ^ 3)

或者:

(2 * N ^ 3) / measured time

前者是每个FLOP花费的平均时间,而后者为您提供 FLOP /秒,通常在文献中称为 FLOP 。 FLOP是性能的主要指标(至少对于科学计算而言)。预计,当N增加时,前一个指标将出现向上跳跃(延迟增加),而后一个指标将出现向下跳跃(性能下降)。

很抱歉,我没有编写C ++,因此无法修改您的代码(但这很简单,因为您只需要按2 * N ^ 3进行额外划分)。我曾经使用C代码进行过相同的实验,这是我在英特尔酷睿2双核处理器上的结果。注意,我正在报告MFLOP或10 ^ 6 FLOP。该图实际上是在R软件中生成的。

enter image description here

我的上述观察真的假设你让其他一切正确。但实际上,它似乎并非如此。

首先,矩阵乘法是:

C[i + j*matrix_size] += A[i + k*matrix_size] * B[k + j*matrix_size];

请注意+=而不是=

其次,你的循环嵌套设计得很糟糕。您正在进行矩阵乘法C = A * B,其中所有矩阵都存储在列主要顺序中,因此您应注意循环嵌套顺序,以确保始终在最内层循环中具有stride-1访问权限。众所周知,j-k-i循环嵌套在这种情况下是最佳的。因此,请考虑以下因素:

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时,你基本上会进行一次乘法。

当然,不要忘记你需要重复划分测量时间。

当然还有其他地方可以优化代码,例如使用常量寄存器和消除地址计算中的整数乘法,但它们不那么重要,因此没有涵盖。数组的初始化也被跳过,因为它已经在弗兰克的答案中提出。

答案 1 :(得分:1)

您没有初始化数组内部的数据,因此系统可能会分配一页写入时复制内存并将所有数组映射到该数据库。

简而言之,A和B总是占用4096字节的硬件内存。由于缓存是基于硬件地址(而不是虚拟)完成的,因此您实际上始终处于缓存中。

使用随机数据初始化A和B将强制分配您想要的实际硬件内存。