为什么我的8M L3缓存不能为大于1M的阵列提供任何好处?

我受到这个问题的启发,编写了一个简单的程序来测试我的机器在每个缓存级别的内存带宽:

为什么矢量化循环没有提高性能

我的代码使用memset反复写入缓冲区(或缓冲区)并测量速度。 它还保存最后打印的每个缓冲区的地址。 列表如下:

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

#define SIZE_KB {8, 16, 24, 28, 32, 36, 40, 48, 64, 128, 256, 384, 512, 768, 1024, 1025, 2048, 4096, 8192, 16384, 200000}
#define TESTMEM 10000000000 // Approximate, in bytes
#define BUFFERS 1

double timer(void)
{
    struct timeval ts;
    double ans;

    gettimeofday(&ts, NULL);
    ans = ts.tv_sec + ts.tv_usec*1.0e-6;

    return ans;
}

int main(int argc, char **argv)
{
    double *x[BUFFERS];
    double t1, t2;
    int kbsizes[] = SIZE_KB;
    double bandwidth[sizeof(kbsizes)/sizeof(int)];
    int iterations[sizeof(kbsizes)/sizeof(int)];
    double *address[sizeof(kbsizes)/sizeof(int)][BUFFERS];
    int i, j, k;

    for (k = 0; k < sizeof(kbsizes)/sizeof(int); k++)
        iterations[k] = TESTMEM/(kbsizes[k]*1024);

    for (k = 0; k < sizeof(kbsizes)/sizeof(int); k++)
    {
        // Allocate
        for (j = 0; j < BUFFERS; j++)
        {
            x[j] = (double *) malloc(kbsizes[k]*1024);
            address[k][j] = x[j];
            memset(x[j], 0, kbsizes[k]*1024);
        }

        // Measure
        t1 = timer();
        for (i = 0; i < iterations[k]; i++)
        {
            for (j = 0; j < BUFFERS; j++)
                memset(x[j], 0xff, kbsizes[k]*1024);
        }
        t2 = timer();
        bandwidth[k] = (BUFFERS*kbsizes[k]*iterations[k])/1024.0/1024.0/(t2-t1);

        // Free
        for (j = 0; j < BUFFERS; j++)
            free(x[j]);
    }

    printf("TESTMEM = %ldn", TESTMEM);
    printf("BUFFERS = %dn", BUFFERS);
    printf("Size (kB)tBandwidth (GB/s)tIterationstAddressesn");
    for (k = 0; k < sizeof(kbsizes)/sizeof(int); k++)
    {
        printf("%7dtt%.2fttt%dtt%x", kbsizes[k], bandwidth[k], iterations[k], address[k][0]);
        for (j = 1; j < BUFFERS; j++)
            printf(", %x", address[k][j]);
        printf("n");
    }

    return 0;
}

结果(BUFFERS = 1):

TESTMEM = 10000000000
BUFFERS = 1
Size (kB)   Bandwidth (GB/s)    Iterations  Addresses
      8     52.79               1220703     90b010
     16     56.48               610351      90b010
     24     57.01               406901      90b010
     28     57.13               348772      90b010
     32     45.40               305175      90b010
     36     38.11               271267      90b010
     40     38.02               244140      90b010
     48     38.12               203450      90b010
     64     37.51               152587      90b010
    128     36.89               76293       90b010
    256     35.58               38146       d760f010
    384     31.01               25431       d75ef010
    512     26.79               19073       d75cf010
    768     26.20               12715       d758f010
   1024     26.20               9536        d754f010
   1025     18.30               9527        90b010
   2048     18.29               4768        d744f010
   4096     18.29               2384        d724f010
   8192     18.31               1192        d6e4f010
  16384     18.31               596         d664f010
 200000     18.32               48          cb2ff010

我可以很容易地看到32K L1缓存和256K L2缓存的效果。 我不明白为什么在memset缓冲区的大小超过1M之后性能突然下降。 我的三级缓存应该是8M。 它的发生也是如此突然,而不是像L1和L2缓存大小超过时那样逐渐减少。

我的处理器是Intel i7 3700.来自/ sys / devices / system / cpu / cpu0 / cache的L3缓存的详细信息是:

level = 3
coherency_line_size = 64
number_of_sets = 8192
physical_line_partition = 1
shared_cpu_list = 0-7
shared_cpu_map = ff
size = 8192K
type = Unified
ways_of_associativity = 16

我想我会尝试使用多个缓冲区 - 在每个1M缓冲区的2个缓冲区上调用memset并查看性能是否会下降。 用BUFFERS = 2,我得到:

TESTMEM = 10000000000
BUFFERS = 2
Size (kB)   Bandwidth (GB/s)    Iterations  Addresses
      8     54.15               1220703     e59010, e5b020
     16     51.52               610351      e59010, e5d020
     24     38.94               406901      e59010, e5f020
     28     38.53               348772      e59010, e60020
     32     38.31               305175      e59010, e61020
     36     38.29               271267      e59010, e62020
     40     38.29               244140      e59010, e63020
     48     37.46               203450      e59010, e65020
     64     36.93               152587      e59010, e69020
    128     35.67               76293       e59010, 63769010
    256     27.21               38146       63724010, 636e3010
    384     26.26               25431       63704010, 636a3010
    512     26.19               19073       636e4010, 63663010
    768     26.20               12715       636a4010, 635e3010
   1024     26.16               9536        63664010, 63563010
   1025     18.29               9527        e59010, f59420
   2048     18.23               4768        63564010, 63363010
   4096     18.27               2384        63364010, 62f63010
   8192     18.29               1192        62f64010, 62763010
  16384     18.31               596         62764010, 61763010
 200000     18.31               48          57414010, 4b0c3010

似乎两个1M缓冲区都保留在L3缓存中。 但试着稍微增加任一缓冲区的大小,性能就会下降。

我一直在编译-O3。 它没有太大区别(除了可能在BUFFERS上展开循环)。 我尝试了-O0,除了L1速度之外,它是一样的。 gcc版本是4.9.1。

总而言之,我有一个由两部分组成的问题:

  • 为什么我的8 MB三级缓存不能为大于1M的内存块提供任何好处?
  • 为什么表现如此突然下降?

  • 编辑:

    正如加布里埃尔南方的建议,我跑我的代码perf使用缓冲区= 1在同一时间只有一个缓冲区的大小。 这是完整的命令:

    perf stat -e dTLB-loads,dTLB-load-misses,dTLB-stores,dTLB-store-misses -r 100 ./a.out 2> perfout.txt
    

    -r表示perf会执行100次并返回平均统计数据。

    perf的输出,使用#define SIZE_KB {1024}

     Performance counter stats for './a.out' (100 runs):
    
             1,508,798 dTLB-loads                                                    ( +-  0.02% )
                     0 dTLB-load-misses          #    0.00% of all dTLB cache hits 
           625,967,550 dTLB-stores                                                   ( +-  0.00% )
                 1,503 dTLB-store-misses                                             ( +-  0.79% )
    
           0.360471583 seconds time elapsed                                          ( +-  0.79% )
    

    #define SIZE_KB {1025}

     Performance counter stats for './a.out' (100 runs):
    
             1,670,402 dTLB-loads                                                    ( +-  0.09% )
                     0 dTLB-load-misses          #    0.00% of all dTLB cache hits 
           626,099,850 dTLB-stores                                                   ( +-  0.00% )
                 2,115 dTLB-store-misses                                             ( +-  2.19% )
    
           0.503913416 seconds time elapsed                                          ( +-  0.06% )
    

    所以似乎有更多的TLB与1025K缓冲区未命中。 但是,对于这个大小的缓冲区,该程序会执行大约9500个memset调用,因此每个memset调用仍然少于1次。


    简答:

    初始化大于1 MB的内存区域时,您的memset版本开始使用非临时存储。 因此,即使您的L3缓存大于1 MB,CPU也不会将这些行存储在缓存中。 因此,对于大于1 MB的缓冲区值,性能受系统中可用内存带宽的限制。

    细节:

    背景:

    我测试了你在几个不同系统上提供的代码,并且最初专注于调查TLB,因为我认为可能会在第二级TLB中出现颠簸。 但是,我收集的数据都没有证实这一假设。

    我测试过的一些系统使用Arch Linux,它具有最新版本的glibc,而其他一些系统则使用Ubuntu 10.04,它使用的是旧版本的eglibc。 当使用多种不同的CPU架构进行测试时,我能够重现在使用静态链接二进制文件时问题中描述的行为。 我关注的行为在SIZE_KB10241025时在运行时间上存在显着差异。 性能差异是由缓慢版本和快速版本中执行代码的更改来解释的。

    汇编代码

    我使用perf recordperf annotate来收集正在执行的汇编代码的踪迹,以查看热代码路径是什么。 代码使用以下格式显示在下面:

    percentage time executing instruction | address | instruction percentage time executing instruction | address | instruction

    我从较短版本复制了热循环,省略了大部分地址,并且有一条连接循环后端和循环头的行。

    对于在Arch Linux上编译的版本,热循环是(对于1024和1025大小):

      2.35 │a0:┌─+movdqa %xmm8,(%rcx)
     54.90 │   │  movdqa %xmm8,0x10(%rcx)
     32.85 │   │  movdqa %xmm8,0x20(%rcx)
      1.73 │   │  movdqa %xmm8,0x30(%rcx)
      8.11 │   │  add    $0x40,%rcx      
      0.03 │   │  cmp    %rcx,%rdx       
           │   └──jne    a0
    

    对于Ubuntu 10.04二进制文件,运行时大小为1024的热循环为:

           │a00:┌─+lea    -0x80(%r8),%r8
      0.01 │    │  cmp    $0x80,%r8     
      5.33 │    │  movdqa %xmm0,(%rdi)  
      4.67 │    │  movdqa %xmm0,0x10(%rdi)
      6.69 │    │  movdqa %xmm0,0x20(%rdi)
     31.23 │    │  movdqa %xmm0,0x30(%rdi)
     18.35 │    │  movdqa %xmm0,0x40(%rdi)
      0.27 │    │  movdqa %xmm0,0x50(%rdi)
      3.24 │    │  movdqa %xmm0,0x60(%rdi)
     16.36 │    │  movdqa %xmm0,0x70(%rdi)
     13.76 │    │  lea    0x80(%rdi),%rdi 
           │    └──jge    a00    
    

    对于运行缓冲区大小为1025的Ubuntu 10.04版本,热循环为:

           │a60:┌─+lea    -0x80(%r8),%r8  
      0.15 │    │  cmp    $0x80,%r8       
      1.36 │    │  movntd %xmm0,(%rdi)    
      0.24 │    │  movntd %xmm0,0x10(%rdi)
      1.49 │    │  movntd %xmm0,0x20(%rdi)
     44.89 │    │  movntd %xmm0,0x30(%rdi)
      5.46 │    │  movntd %xmm0,0x40(%rdi)
      0.02 │    │  movntd %xmm0,0x50(%rdi)
      0.74 │    │  movntd %xmm0,0x60(%rdi)
     40.14 │    │  movntd %xmm0,0x70(%rdi)
      5.50 │    │  lea    0x80(%rdi),%rdi 
           │    └──jge    a60
    

    这里的关键区别在于较慢版本使用movntd指令,而较快版本使用movdqa指令。 “英特尔软件开发人员手册”对非暂时性商店进行了如下说明:

    对于WC内存类型来说,处理器似乎永远不会将数据读入缓存层次结构中。 相反,非临时提示可以通过加载具有等同于对齐的高速缓存行的临时内部缓冲区而不填充该数据到高速缓存来实现。

    因此,这似乎解释了使用大于1 MB的值的memset不适合缓存的行为。 下一个问题是为什么Ubuntu 10.04系统和Arch Linux系统之间存在差异,为什么选择1 MB作为截止点。 为了调查这个问题,我查看了glibc源代码:

    memset源代码

    sysdeps/x86_64/memset.S glibc git repo,我发现有趣的第一个提交是b2b671b677d92429a3d41bf451668f476aa267ed

    提交描述是:

    在x64上更快的memset

    这个实现以几种方式加速memset。 首先是避免昂贵的计算跳跃。 其次是使用memset的参数大部分时间与8字节对齐的事实。

    基准测试结果:kam.mff.cuni.cz/~ondra/benchmark_string/memset_profile_result27_04_13.tar.bz2

    所引用的网站有一些有趣的分析数据。

    提交的差异表明memset的代码被简化了很多,并且非临时存储被删除。 这与Arch Linux展示的代码相匹配。

    看着前面The largest cache size代码,我发现是否使用非临时存储的选择似乎利用了一个值, The largest cache size值被描述为The largest cache size

    L(byte32sse2_pre):
    
        mov    __x86_shared_cache_size(%rip),%r9d  # The largest cache size
        cmp    %r9,%r8
        ja     L(sse2_nt_move_pre)
    

    用于计算的代码位于:sysdeps / x86_64 / cacheinfo.c

    虽然看起来有计算实际共享缓存大小的代码,但默认值也是1 MB:

    long int __x86_64_shared_cache_size attribute_hidden = 1024 * 1024;
    

    所以我怀疑是使用了默认值,但可能有其他原因,代码选择1MB作为截止点。

    在任何一种情况下,对问题的总体回答似乎是,在设置大于1 MB的内存区域时,系统上的memset版本正在使用非临时存储。


    鉴于Gabriel对生成的汇编代码的反汇编,我认为这确实是问题[编辑:他的答案已被编辑,现在看来是根本原因,所以我们达成了一致意见]:

    请注意, movnt是一个流式存储,它可能具有(取决于具体的微架构实现)几种影响:

  • 具有弱排序语义(这使得它更快)。
  • 如果覆盖整行(无需提取以前的数据并合并),则延迟时间得到改进。
  • 有一个非暂时的暗示,使其无法缓存。
  • #1和#2可能会改善这些操作的延迟和带宽,如果它们是内存绑定的,但#3基本上会强制它们成为内存绑定,即使它们可以适应某些缓存级别。 这可能会超过这些好处,因为内存延迟/体重大大降低。

    所以,你的memset库实现可能会使用错误的门槛来切换到流式存储版本(我想它并不会检查你的LLC大小,但假设1M是内存驻留很奇怪)。 我建议尝试替代库,或者禁用编译器生成它们的能力(如果支持的话)。


    您的基准测试只能写入内存,从不读取,使用memset可能是巧妙设计的,不会从缓存中读取任何内容。 很可能,使用此代码的情况下,只使用高速缓存的一半功能,与原始内存相比,性能并没有提高。 写入原始记忆非常接近L2速度的事实可能是一个暗示。 如果L2以26 GB /秒的速度运行,主存以18 GB /秒的速度运行,那么您对L3缓存的期望是什么?

    您正在测量吞吐量,而不是延迟。 我会尝试一个基准测试,你真正使用L3缓存的强度,提供比主内存更低的延迟。

    链接地址: http://www.djcxy.com/p/85961.html

    上一篇: Why does my 8M L3 cache not provide any benefit for arrays larger than 1M?

    下一篇: Speed of memcpy() greatly influenced by different ways of malloc()