【问题标题】:Memory performance/cache puzzle内存性能/缓存难题
【发布时间】:2013-11-16 18:01:45
【问题描述】:

我有一个内存性能难题。我正在尝试测试从主内存中获取一个字节需要多长时间,以及各种 BIOS 设置和内存硬件参数如何影响它。我为 Windows 编写了以下代码,它在循环中通过读取另一个缓冲区来刷新缓存,然后以不同的步幅一次读取一个字节的目标缓冲区。我认为一旦步幅是缓存行大小,这就是我要测量的数量,因为每次读取都会进入主内存。这是基准代码(注意缓冲区的大小是步幅 x 1MB,并且我将线程固定到核心 1):

#include <stdio.h>
#include <memory.h>

#define NREAD       (1024*1024)
#define CACHE_SIZE  (50*1024*1024)

char readTest(int stride) {
    LARGE_INTEGER frequency;
    LARGE_INTEGER start;
    LARGE_INTEGER end;
    int rep, i,ofs;
    double time, min_time=1e100, max_time=0.0, mean_time=0.0;
    char *buf = (char *)malloc(NREAD*stride);
    char *flusher = (char *)malloc(CACHE_SIZE); 
    char jnk=0;
    for(rep=0; rep<255; rep++) {
        // read the flusher to flush the cache
        for(ofs = 0; ofs<CACHE_SIZE; ofs+=64) jnk+=flusher[ofs];
        if (QueryPerformanceFrequency(&frequency) == FALSE) exit(-1);
        if (QueryPerformanceCounter(&start) == FALSE) exit(-2);

        // here's the timed loop
        for(ofs=0; ofs<NREAD*stride; ofs+=stride) jnk += buf[ofs];

        if (QueryPerformanceCounter(&end) == FALSE) exit(-3);
        time = (double)(end.QuadPart - start.QuadPart) / (double)frequency.QuadPart*1e6;
        max_time = time > max_time ? time : max_time;
        min_time = time < min_time ? time : min_time;
        mean_time += time;
    }
    mean_time /= 255;
    printf("Stride = %4i, Max: %6.0f us, Min: %6.0f us, Mean: %6.0f us, B/W: %4.0f MB/s\n", stride, max_time, min_time, mean_time, NREAD/min_time);
    free(buf);
    free(flusher);
    return jnk;
}

int main(int argc, char* argv[]) {
    SetThreadAffinityMask(GetCurrentThread(), 1);  // pin to core 1 to avoid weirdness
    // run the tests
    readTest(1);    readTest(2);    readTest(4);    readTest(6);    readTest(8);
    readTest(12);   readTest(16);   readTest(24);   readTest(32);   readTest(48);
    readTest(64);   readTest(96);   readTest(128);  readTest(192);  readTest(256);
    readTest(384);  readTest(512);  readTest(768);  readTest(1024); readTest(1536);
    return 0;
}

定时内循环组装为:

        // here's the timed loop
        for(ofs=0; ofs<NREAD*stride; ofs+=stride) jnk += buf[ofs];
00F410AF  xor         eax,eax  
00F410B1  test        edi,edi  
00F410B3  jle         readTest+0C2h (0F410C2h)  
00F410B5  mov         edx,dword ptr [buf]  
00F410B8  add         bl,byte ptr [eax+edx]  
00F410BB  add         eax,dword ptr [stride]  
00F410BE  cmp         eax,edi  
00F410C0  jl          readTest+0B5h (0F410B5h)  

我在双处理器 E5-2609 机器上运行此程序,结果如下:

Stride =    1, Max:   2362 us, Min:    937 us, Mean:    950 us, B/W: 1119 MB/s
Stride =    2, Max:   1389 us, Min:    968 us, Mean:    978 us, B/W: 1083 MB/s
Stride =    4, Max:   1694 us, Min:   1026 us, Mean:   1037 us, B/W: 1022 MB/s
Stride =    6, Max:   2418 us, Min:   1098 us, Mean:   1124 us, B/W:  955 MB/s
Stride =    8, Max:   2835 us, Min:   1234 us, Mean:   1252 us, B/W:  850 MB/s
Stride =   12, Max:   4203 us, Min:   1527 us, Mean:   1559 us, B/W:  687 MB/s
Stride =   16, Max:   5130 us, Min:   1816 us, Mean:   1849 us, B/W:  577 MB/s
Stride =   24, Max:   7370 us, Min:   2408 us, Mean:   2449 us, B/W:  435 MB/s
Stride =   32, Max:  10039 us, Min:   2901 us, Mean:   3014 us, B/W:  361 MB/s
Stride =   48, Max:  14248 us, Min:   4652 us, Mean:   4731 us, B/W:  225 MB/s
Stride =   64, Max:  19149 us, Min:   6340 us, Mean:   6447 us, B/W:  165 MB/s
Stride =   96, Max:  28848 us, Min:   8475 us, Mean:   8615 us, B/W:  124 MB/s
Stride =  128, Max:  37449 us, Min:   9900 us, Mean:  10160 us, B/W:  106 MB/s
Stride =  192, Max:  51718 us, Min:  11282 us, Mean:  11563 us, B/W:   93 MB/s
Stride =  256, Max:  62193 us, Min:  11558 us, Mean:  11924 us, B/W:   91 MB/s
Stride =  384, Max:  86943 us, Min:  11829 us, Mean:  12260 us, B/W:   89 MB/s
Stride =  512, Max: 108661 us, Min:  11847 us, Mean:  12401 us, B/W:   89 MB/s
Stride =  768, Max: 167951 us, Min:  11797 us, Mean:  12946 us, B/W:   89 MB/s
Stride = 1024, Max: 211700 us, Min:  12893 us, Mean:  13979 us, B/W:   81 MB/s
Stride = 1536, Max: 332214 us, Min:  12967 us, Mean:  15077 us, B/W:   81 MB/s

这是我的问题:

  • 为什么在步幅大于缓存行大小(Sandy Bridge 为 64 字节)后性能继续下降?我会假设一旦步幅足够大以至于每次读取都需要缓存行传输,那么最差的性能就会发生,但即使在那之后,时间也会增加两倍......我错过了什么?
  • 为什么最大时间(发生在循环的第一次迭代中)比最小时间长 2-4 倍?我每次迭代都会刷新缓存...

【问题讨论】:

  • 你能发布所有的计时结果吗?我很想见到他们。
  • 您可以通过大幅增加被测缓冲区的大小(至数百 MB)来消除刷新。这消除了复杂性和错误的原因。您还可以使用大页面来减少 TLB 的使用。您还可以将线程优先级提高到 HIGH。

标签: c performance cpu cpu-cache


【解决方案1】:

缓存线不是跟踪内存的唯一粒度。从虚拟地址到物理地址的转换发生在 page 粒度。您的系统几乎可以肯定使用 4k 页面。

步幅为 64 时,每页有 64 个条目,因此您有 16384 页。 L2 TLB 只能跟踪其中的 512 个页面,因此您在每个新页面(每 64 次访问)上都会丢失 L2 TLB。

步幅为 1024 时,每页有 4 个条目,因此您有 262144 页。现在,您在每 4 次访问时都会丢失一次 L2 TLB。

tl;dr:TLB 失误正在杀死你。您可以使用性能计数器直接观察这一点,而不是让 Stack Overflow 为您读取茶叶。您还可以让您的系统使用一个或多个“超级页面”来分配缓冲区以扩展您的 TLB 范围(尽管不同的系统对此功能的支持程度不同)。

【讨论】:

  • 好的,谢谢你的提示……我得再读一读。后续问题:楼层应该是每次访问的 TLB 未命中,还是 4K 的步幅?性能不断下降,最大跨度为 2MB。
  • @Andrew,4k 或以上的步幅意味着如果您的页面为 4k,则每次访问都会错过 TLB。可以为 2M 页面构建页面地图,这可能会导致那里的降级(尽管降级仍然会像 Stephen 指出的那样是渐进的,因为随着步幅的扩大,每次翻译的重用率会降低)。
【解决方案2】:
  1. 由于预取,在一个行大小之后降级将继续 - 只要您以稳定的流(甚至跨步)跨越缓存行,您就会享受到硬件预取器带来的好处几行。 L2 流媒体特别有用,因为它比您的访问流运行得更快。
    但是,一旦您的步幅超过 128 字节,您应该开始在流预取器之前运行,并且每次访问都会导致完全延迟。
    为确保确实如此 - 禁用预取(希望您的系统在 BIOS 中允许这样做) 编辑:斯蒂芬也提出了一个很好的观点,即访问 TLB 查找的比率——这将说明大的进步。如果您绘制每步长的时间,我敢打赌,由于 TLB 未命中率,您会看到一个强劲的趋势,并且在 64 和 128 字节步长之间跳跃。

  2. 我相信您的第一次迭代由于 TLB 太冷而需要更长的时间。您可以通过尝试刷新它(硬..)或通过运行预热迭代并仅从第二个迭代进行测量来测试它。

【讨论】:

    猜你喜欢
    • 2014-07-24
    • 1970-01-01
    • 2015-09-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-02-16
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多