【问题标题】:analysis of cpu cache access timecpu缓存访问时间分析
【发布时间】:2013-06-14 04:40:45
【问题描述】:

我有以下程序,我在 stackoverflow 上的其他人的帮助下编写了以了解缓存线和 CPU 缓存。我在下面发布了计算结果。

    1     450.0  440.0
    2     420.0  230.0
    4     400.0  110.0
    8     390.0   60.0
   16     380.0   30.0
   32     320.0   10.0
   64     180.0   10.0
  128      60.0    0.0
  256      40.0   10.0
  512      10.0    0.0
 1024      10.0    0.0

我使用 gnuplot 绘制了一张图表,发布在下面。

我有以下问题。

  1. 我以毫秒为单位的计时计算是否正确? 440ms 似乎 有很多时间吗?

  2. 从图 cache_access_1(红线)我们可以得出结论, 缓存行的大小是 32 位(而不是 64 位?)

  3. 在代码中的 for 循环之间清除 缓存?如果是,我该如何以编程方式做到这一点?

  4. 如您所见,我在上面的结果中有一些 0.0 值。? 这说明什么?也是测量的粒度 粗?

请回复。

#include <stdio.h>
#include <sys/time.h>
#include <time.h>
#include <unistd.h>
#include <stdlib.h>

#define MAX_SIZE (512*1024*1024)

int main()
{
    clock_t start, end;
    double cpu_time;
    int i = 0;
    int k = 0;
    int count = 0;

    /*
     * MAX_SIZE array is too big for stack.This is an unfortunate rough edge of the way the stack works. 
     * It lives in a fixed-size buffer, set by the program executable's configuration according to the 
     * operating system, but its actual size is seldom checked against the available space.
     */

    /*int arr[MAX_SIZE];*/

    int *arr = (int*)malloc(MAX_SIZE * sizeof(int));

    /*cpu clock ticks count start*/

    for(k = 0; k < 3; k++)
    {
        start = clock();
        count = 0;

        for (i = 0; i < MAX_SIZE; i++)
        { 
            arr[i] += 3;
            /*count++;*/
        }

        /*cpu clock ticks count stop*/
        end = clock();

        cpu_time = ((double) (end - start)) / CLOCKS_PER_SEC;

        printf("cpu time for loop 1 (k : %4d) %.1f ms.\n",k,(cpu_time*1000));
    }

    printf("\n");
    for (k = 1 ; k <= 1024 ; k <<= 1)
    {
        /*cpu clock ticks count start*/
        start = clock();
        count = 0;

        for (i = 0; i < MAX_SIZE; i += k) 
        {
            /*count++;*/
            arr[i] += 3;
        }

        /*cpu clock ticks count stop*/
        end = clock();

        cpu_time = ((double) (end - start)) / CLOCKS_PER_SEC;

        printf("cpu time for loop 2 (k : %4d) %.1f ms.\n",k,(cpu_time*1000));
    }
    printf("\n");

    /* Third loop, performing the same operations as loop 2, 
       but only touching 16KB of memory
     */
    for (k = 1 ; k <= 1024 ; k <<= 1)
    {
        /*cpu clock ticks count start*/
        start = clock();
        count = 0;

        for (i = 0; i < MAX_SIZE; i += k) 
        {
            count++;
            arr[i & 0xfff] += 3;
        }

        /*cpu clock ticks count stop*/
        end = clock();

        cpu_time = ((double) (end - start)) / CLOCKS_PER_SEC;

        printf("cpu time for loop 3 (k : %4d) %.1f ms.\n",k,(cpu_time*1000));
    }

    return 0;
}

【问题讨论】:

  • 我假设您使用的是clock(),因为您使用的是linux。我只知道osx和windows都有更好的高精度性能计数器。但是如果你跑得足够长,任何计时器最终都应该这样做:) 现在我注意到的一件事是你做了两步的力量。要测量你需要做随机模式。许多缓存是用非常简单的哈希实现的,它只占用低位。通过以两个步幅的精确功率访问数据来触发缓存别名实际上是某些 CPU 的性能缺陷。
  • 我在那个问题上做了和你一样的事情:stackoverflow.com/questions/16249694/….
  • 没关系,但不需要强制转换 malloc 结果。该行应为int *arr = malloc(MAX_SIZE * sizeof *arr);
  • @starmole: "windows ha[s] better high accuracy performance counters" - 在 可怕的。
  • Tony D: msdn.microsoft.com/en-us/library/windows/desktop/… QueryPerformance 计数器通常以时钟速率运行。 osx 也一样:developer.apple.com/library/mac/#qa/qa1398/_index.html。我确信linux有一个等价物,但我只是不知道那里的名字。是的,如果你在其他内核上有很多负载等,每个时间都会关闭。我只是读了一下时钟,这似乎是正确的选择。只是听到了奇怪的声音,对此感到抱歉。

标签: c performance optimization cpu cpu-architecture


【解决方案1】:

由于您使用的是 Linux,我将从这个角度回答。我还将在编写时考虑到 Intel(即 x86-64)架构。

  1. 440 毫秒可能是准确的。查看结果的更好方法是每个元素或访问的时间。请注意,增加 k 会减少访问的元素数量。现在,缓存访问 2 显示出相当稳定的结果,即 0.9ns / 访问。这个时间大致相当于每次访问 1 - 3 个周期(取决于 CPU 的时钟速率)。所以尺寸 1 - 16(也许 32)是准确的。
  2. 否(尽管我首先假设您的意思是 32 字节与 64 字节)。您应该问自己,“缓存行大小”是什么样的?如果您访问小于缓存行,那么您将错过并随后命中一次或多次。如果大于或等于缓存行大小,则每次访问都会丢失。在 k=32 及以上时,访问 1 的访问时间相对恒定,每次访问为 20ns。在 k=1-16 时,总访问时间是恒定的,这表明缓存未命中的数量大致相同。所以我会得出结论,缓存行大小是 64 字节。
  3. 是的,至少对于仅存储约 16KB 的最后一个循环。如何?要么接触很多其他数据,比如另一个 GB 数组。或者调用像x86的WBINVD这样的指令,写入内存,然后使所有缓存内容失效;但是,它要求您处于内核模式。
  4. 正如您所指出的,在大小 32 之外,时间徘徊在 10 毫秒左右,这显示了您的时间粒度。您需要增加所需的时间(以便 10 毫秒的粒度就足够了)或切换到不同的计时机制,这是 cmets 正在争论的问题。我喜欢使用指令 rdtsc(读取时间戳计数器(即循环计数)),但这可能比上面的建议更有问题。将代码切换到 rdtsc 基本上需要切换时钟、clock_t 和 CLOCKS_PER_SEC。但是,如果您的线程迁移,您仍然可能会面临时钟漂移,但这是一个有趣的测试,所以我不会担心这个问题。

更多注意事项:一致的步幅(如 2 的幂)的问题在于处理器喜欢通过预取来隐藏缓存未命中惩罚。您可以在 BIOS 中禁用许多机器上的预取器(请参阅"Changing the Prefetcher for Intel Processors")。

页面错误也可能会影响您的结果。您正在分配 500M 个整数或大约 2GB 的存储空间。循环 1 尝试访问内存以便操作系统分配页面,但如果您没有这么多可用内存(不仅仅是总内存,因为操作系统等占用了一些空间),那么您的结果将会出现偏差。此外,操作系统可能会开始回收一些空间,因此您在某些访问时总是会出现页面错误。

与之前相关,TLB 也会对结果产生一些影响。硬件在转换后备缓冲区 (TLB) 中保留一小部分从虚拟地址到物理地址的映射缓存。每页内存(Intel 上为 4KB)都需要一个 TLB 条目。因此,您的实验将需要 2GB / 4KB => ~500,000 个条目。大多数 TLB 的条目少于 1000 个,因此测量值也因未命中而出现偏差。幸运的是,每 4KB 或 1024 个整数只有一次。 malloc 可能正在为您分配“大”或“巨大”页面,以获取更多详细信息 - Huge Pages in Linux

另一个实验是重复第三个循环,但更改您正在使用的掩码,以便您可以观察每个缓存级别的大小(L1、L2,可能是 L3,很少是 L4)。您可能还会发现不同的缓存级别使用不同的缓存线大小。

【讨论】:

    猜你喜欢
    • 2016-04-11
    • 1970-01-01
    • 1970-01-01
    • 2012-04-28
    • 1970-01-01
    • 2011-08-17
    • 1970-01-01
    • 1970-01-01
    • 2019-08-28
    相关资源
    最近更新 更多