【问题标题】:why cache doesn't work as it supposed to be?为什么缓存不能正常工作?
【发布时间】:2013-07-28 16:43:16
【问题描述】:

根据http://igoro.com/archive/gallery-of-processor-cache-effects/,在尝试示例 2 时,时间应该会减少,直到偏移量等于缓存行 zie。
但是,在我的机器上,它不起作用。
代码如下。

#define SIZE 1024*1024*64

int main()
{
struct timeval start, end;
int k;
int i;

for(k = 1; k <= 1024; k *= 2)
{

    int *arr = (int*)malloc(SIZE * sizeof(int));
    gettimeofday(&start, NULL);
    for(i = 0; i < SIZE; i += k)
        arr[i] *= 3;
    gettimeofday(&end, NULL);

    printf("K = %d, time = %d\n", k,
            (end.tv_sec - start.tv_sec)*1000000 + (end.tv_usec - start.tv_usec));

    free(arr);
}
return 0;
}

结果如下:

K = 1,时间 = 410278
K = 2,时间 = 265313
K = 4,时间 = 201540
K = 8,时间 = 169800
K = 16,时间 = 155123
K = 32,时间 = 142496
K = 64,时间 = 137967
K = 128,时间 = 135818
K = 256,时间 = 135128
K = 512,时间 = 135167
K = 1024,时间 = 135462

【问题讨论】:

  • 尝试calloc() 而不是malloc()SIZE 有多大?
  • 另外,将malloc() 移出循环。
  • 我已经用完整的代码更新了编辑。 malloc 不包括在时间测量中。而且我认为如果它使用相同的内存,缓存可能会保留在内存中。那么测试将不准确。
  • 事实上,不管你信不信,即使malloc() 不在定时区域内,它也很重要。这是因为惰性分配。这就是为什么我要求您尝试使用calloc(),并将其移到两个循环之外。
  • 请记住,您将垃圾乘以 3。

标签: c linux performance caching cpu


【解决方案1】:

这取决于编译器(它的版本)、优化级别和CPU。显然,大部分时间都花在了malloc,所以我把它移出循环并增加了SIZE

我正在 i3770K 处理器和 16GB RAM 上尝试使用 GCC 4.8.1 的 Debian/Sid。

#include <stdio.h>
#include <stdlib.h> 
#include <sys/time.h>
#include <time.h>
#define SIZE 1024*1024*1024

int main ()
{
  struct timeval start, end;
  clock_t startcl, endcl;
  int k, i;

  int *arr = (int *) malloc (SIZE * sizeof (int));
  if (!arr) { perror("malloc"); exit(EXIT_FAILURE); };
  for (k = 1; k <= 1024; k *= 2)  {
      gettimeofday (&start, NULL);
      startcl = clock();
      for (i = 0; i < SIZE; i += k)
        arr[i] *= 3;
      gettimeofday (&end, NULL);
      endcl = clock();
      printf ("K = %d, time = %ld, cpu clock=%ld microsec\n", k,
              (end.tv_sec - start.tv_sec) * 1000000 
              + (end.tv_usec - start.tv_usec),
              (long) (endcl - startcl));
    }
  free (arr);
  return 0;
}   

并使用gcc -Wall -mtune=native -O3 ./wilsonwen.c -o ./wilsonwen-O3 编译然后运行它:

K = 1, time = 696074, cpu clock=680000 microsec
K = 2, time = 361173, cpu clock=360000 microsec
K = 4, time = 341920, cpu clock=340000 microsec
K = 8, time = 341767, cpu clock=340000 microsec
K = 16, time = 342065, cpu clock=340000 microsec
K = 32, time = 224502, cpu clock=230000 microsec
K = 64, time = 119544, cpu clock=120000 microsec
K = 128, time = 51089, cpu clock=50000 microsec
K = 256, time = 26447, cpu clock=20000 microsec
K = 512, time = 14104, cpu clock=20000 microsec
K = 1024, time = 8385, cpu clock=10000 microsec

这更符合你提到的博客。将malloc 移出k 的外循环非常重要(如果不这样做,则看不到缓存效果,显然是因为malloc 和底层的mmap 系统调用消耗了很多时间)。

我无法解释为什么 k=1 需要更多时间(也许是因为 malloc-ed 内存被页面错误占用到 RAM 中?)。即使通过在 for (k 循环之前添加 for (i=0; i&lt;SIZE/1024; i++) arr[i] = i; 循环来“预取页面”,k=1 的时间仍然几乎是 k=2 的两倍。我们确实看到了 Igor Ostrovsky 的博客中提到的 k=2k=16 的平稳期。用calloc 替换malloc 不是很重要。使用clang (3.2) 而不是gcc (4.8) 进行编译会得到非常相似的时序结果。

优化非常重要,通过尝试使用gcc -Wall -O0 ./wilsonwen.c -o ./wilsonwen-O0 并运行我没有看到任何平台(即使使用-O1,您也会看到)。众所周知,gcc 没有任何优化标志会吐出非常糟糕的机器代码。

基准测试的一般规则是启用编译器优化。

【讨论】:

  • k=1 花费的时间更长,因为这是第一次触及内存。所以惰性分配发生在k=1 期间。之后,一切都很好。
  • malloc 不在时间测量中。所以我认为移动 malloc 不会产生影响。
  • @Mystical:不,因为正如我所说,在测量之前添加for (i=0; i&lt;SIZE/1024; i++) arr[i] = i; 不会改变时间。
  • @wilsonwen 实际上确实如此。有很多关于内存分配的事情会“泄漏”到实际调用malloc() 的边界之外。我已经提到了惰性分配。那你不知道内存是从哪里来的,如果你最近使用了同一个区域,它可能已经在缓存中了……
  • @BasileStarynkevitch 我尝试了您的代码并添加了预取,结果与博客更加一致。非常感谢
【解决方案2】:

和我的一样。

有人在那篇文章下的讨论中得到了相同的结果。他说也许这只是 arr[i] *= 3 的真正 CPU 时间。

当 K = 1 时,需要运行 SIZE 次。但是当 K = 2 时,只需要运行 SIZE/2 次即可。

因此,无论 K 是什么,您都可以重写代码以运行相同的时间。然后检查是否相同。我只是在打字的时候有这个想法。我稍后会试试这个。如果您尝试过,请添加评论。

这是我的代码:

#include <stdio.h>
#include <time.h>
#include <stdint.h>
#define SIZE 64*1024*1024
int32_t arr [SIZE];
struct timespec ts;

int main(int argc, char *argv[])
{
long i,j= 0;
long start;
long start_sec;
int count = 1;
int k = 0;

// init the arr;
for (i = 0; i< 64*1024*1024;++i){
    arr[i] = 0;
}

for (j = 1; j< 1025;){
    clock_gettime(CLOCK_REALTIME, &ts);
    start = ts.tv_nsec;
    start_sec = ts.tv_sec;
    for (i = 0, k = 0; i< 64*1024*1024; i++, k+=j){
        k = k & (SIZE -1);
        arr[k] *=3;
        arr[k] =1;
    }
    clock_gettime(CLOCK_REALTIME, &ts);
    printf ("%d, %ld, %ld\n", count,(ts.tv_sec-start_sec)*1000000000+(ts.tv_nsec -start), j);
    count++;
    j *= 2;
}
return 0;
}

输出如下:

1, 352236657, 1
2, 356920027, 2
3, 375986006, 4
4, 494875602, 8
5, 957796009, 16
6, 1397285233, 32
7, 1784398514, 64
8, 1070586859, 128
9, 1130548756, 256
10, 1169113810, 512
11, 1312605482, 1024

如果我注释掉 arr-init 循环,当 K=1 时,它比 K=2 花费的时间更长。

我们可以看到,时间只是随着期望的增加而增加(在 K = 128 之前)。因为尽管有 K,我们总是有 64*1024*1024 次循环。K 越大,缓存行刷新的时间就越多。

嗯,但我无法解释从 K = 64 到 K = 128 的减少。

和@Mysticial 谈到了lazy-malloc,所以我也对文章中的原始代码做了一个实验,但是添加了arr-init循环来避免lazy-malloc的问题。 K = 1 的成本确实降低了,但仍然大于 K=2 的成本,并且比原始版本更接近 K=2 的成本 * 2。数据如下:

1, 212882204, 1
2, 111660951, 2
3, 67843457, 4
4, 62980310, 8
5, 62092973, 16
6, 42531407, 32
7, 27686909, 64
8, 9142755, 128
9, 4064936, 256
10, 2342842, 512
11, 1130305, 1024

所以我认为从 K = 1 减少到 K = 2 和 K=2 到 K=4 的原因是迭代次数从 SIZE 减少到 SIZE/2。

这是我的想法,但我不确定。

================================================ =======

我用 -Ox 编译了代码,减少的消失了(但必须添加 arr-init 循环)。感谢@Basile。 稍后我会检查 asm 代码中的差异。

这是asm代码的区别:

没有O1,

    movq    $0, -32(%rbp)
    jmp .L5
.L6:
    movq    -32(%rbp), %rax
    movl    arr(,%rax,4), %edx
    movl    %edx, %eax
    addl    %eax, %eax
    addl    %eax, %edx
    movq    -32(%rbp), %rax
    movl    %edx, arr(,%rax,4)
    movq    -24(%rbp), %rax
    addq    %rax, -32(%rbp)
.L5:
    cmpq    $67108863, -32(%rbp)
    jle .L6

还有 O1,

    movl    $0, %eax 
.L3:
    movl    arr(,%rax,4), %ecx # ecx = a[i]
    leal    (%rcx,%rcx,2), %edx # edx = 3* rcx
    movl    %edx, arr(,%rax,4) # a[i] = edx
    addq    %rbx, %rax # rax += rbx
    cmpq    $67108863, %rax 
    jle .L3

我把不带 O1 的 asm 代码改成这个,

    movq    $0, -32(%rbp) 
    movl    $0, %eax
    movq    -24(%rbp), %rbx
    jmp .L5
.L6:
    movl    arr(,%rax,4), %edx 
    movl    %edx, %ecx 
    addl    %ecx, %ecx 
    addl    %ecx, %edx 
    movl    %edx, arr(,%rax,4) 
    addq    %rbx, %rax
.L5:
    cmpq    $67108863, %rax 
    jle .L6

然后我得到结果:

1, 64119476, 1
2, 63417463, 2
3, 63732534, 4
4, 66703562, 8
5, 65740635, 16
6, 47743618, 32
7, 28402013, 64
8, 9444894, 128
9, 4544371, 256
10, 2991025, 512
11, 1242882, 1024

它几乎和 O1 一样。 似乎 movq -32(%rbp), %rax 的东西成本太高了。但我不知道为什么。

也许我最好问一个关于它的新问题。

【讨论】:

  • @BasileStarynkevitch 我使用不同的 -O 选项编译了代码。如果我从答案中的代码中添加 arr-init 循环,这种现象就会消失。谢谢。
  • @BasileStarynkevitch 我更新了我的答案。你能说出原因吗?在我的 O1 计算机上(Xubuntu 13.04,gcc 4.7.3),我没有看到从 K=1 到 K=2 的大幅下降,而您可以在您的计算机上看到它。
猜你喜欢
  • 2016-07-16
  • 2019-01-04
  • 2020-09-03
  • 2016-10-10
  • 2016-10-24
  • 2017-02-27
  • 2017-07-08
  • 2014-11-23
  • 2021-03-07
相关资源
最近更新 更多