我对缓存性能的推理是否正确?

Am I correctly reasoning about cache performance?

我试图巩固我对数据争用的理解,并提出了以下最小测试程序。它运行一个执行一些数据处理的线程,并在原子 bool 上自旋锁,直到线程完成。

#include <iostream>
#include <atomic>
#include <thread>
#include <array>

class SideProcessor
{
public:
    SideProcessor()
    : dataArr{}
    {
    };

    void start() {
        processingRequested = true;
    };

    bool isStarted() {
        return processingRequested;
    };

    bool isDone() {
        return !processingRequested;
    };

    void finish() {
        processingRequested = false;
    }

    void run(std::atomic<bool>* exitRequested) {
        while(!(*exitRequested)) {
            // Spin on the flag.
            while(!(isStarted()) && !(*exitRequested)) {
            }
            if (*exitRequested) {
                // If we were asked to spin down, break out of the loop.
                break;
            }

            // Process
            processData();

            // Flag that we're done.
            finish();
        }
    };

private:
    std::atomic<bool> processingRequested;

#ifdef PAD_ALIGNMENT
    std::array<bool, 64> padding;
#endif

    std::array<int, 100> dataArr;

    void processData() {
        // Add 1 to every element a bunch of times.
        std::cout << "Processing data." << std::endl;
        for (unsigned int i = 0; i < 10000000; ++i) {
            for (unsigned int j = 0; j < 100; ++j) {
                dataArr[j] += 1;
            }
        }
        std::cout << "Done processing." << std::endl;
    };
};

int main() {
    std::atomic<bool> exitRequested;
    exitRequested = false;

    SideProcessor sideProcessor;
    std::thread processThreadObj = std::thread(&SideProcessor::run,
                                       &sideProcessor, &exitRequested);

    // Spinlock while the thread is doing work.
    std::cout << "Starting processing." << std::endl;
    sideProcessor.start();
    while (!(sideProcessor.isDone())) {
    }

    // Spin the thread down.
    std::cout << "Waiting for thread to spin down." << std::endl;
    exitRequested = true;
    processThreadObj.join();

    std::cout << "Done." << std::endl;

    return 0;
}

当我使用 -O3 构建且未定义 PAD_ALIGNMENT 时,我从 Linux perf 得到以下结果:

142,511,066      cache-references                                              (29.15%)
    142,364      cache-misses              #    0.100 % of all cache refs      (39.33%)
 33,580,965      L1-dcache-load-misses     #    3.40% of all L1-dcache hits    (39.90%)
988,605,337      L1-dcache-loads                                               (40.46%)
279,446,259      L1-dcache-store                                               (40.71%)
    227,713      L1-icache-load-misses                                         (40.71%)
 10,040,733      LLC-loads                                                     (40.71%)
      5,834      LLC-load-misses           #    0.06% of all LL-cache hits     (40.32%)
 40,070,067      LLC-stores                                                    (19.39%)
         94      LLC-store-misses                                              (19.22%)
                                                                                       
0.708704757 seconds time elapsed                                                       

当我使用 PAD_ALIGNMENT 构建时,我得到以下结果:

    450,713      cache-references                                              (27.83%)
    124,281      cache-misses              #   27.574 % of all cache refs      (39.29%)
    104,857      L1-dcache-load-misses     #    0.01% of all L1-dcache hits    (42.16%)
714,361,767      L1-dcache-loads                                               (45.02%)
281,140,925      L1-dcache-store                                               (45.83%)
     90,839      L1-icache-load-misses                                         (43.52%)
     11,225      LLC-loads                                                     (40.66%)
      3,685      LLC-load-misses           #   32.83% of all LL-cache hits     (37.80%)
      1,798      LLC-stores                                                    (17.18%)
         76      LLC-store-misses                                              (17.18%)
                                                                                       
0.140602005 seconds time elapsed                                                       

我有两个问题:

  1. 我说大量增加的缓存引用来自缺少 L1 并且必须转到 LLC 以获取其他核心无效的缓存行,这样说对吗? (如果不准确,请更正我的术语)
  2. 我想我理解增加的 LLC 负载,但为什么当数据在同一个缓存行时有这么多的 LLC 存储?

(编辑)请求的附加信息:

g++ version:  (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0 
CPU model: Intel(R) Core(TM) i7-6700HQ CPU @ 2.60GHz 
Kernel version: 4.15.0-106-generic   

使用 g++ -std=c++14 -pthread -g -O3 -c -fverbose-asm -Wa,-adhln 编译时的 processData() 循环:

没有PAD_ALIGNMENT:

57:main.cpp      ****     void processData() {
58:main.cpp      ****         // Add 1 to every element a bunch of times.
59:main.cpp      ****         std::cout << "Processing data." << std::endl;
60:main.cpp      ****         for (unsigned int i = 0; i < 10000000; ++i) {
61:main.cpp      ****             for (unsigned int j = 0; j < 100; ++j) {
62:main.cpp      ****                 dataArr[j] += 1;
409                     .loc 4 62 0
410 0109 83450401       addl    , 4(%rbp) #, MEM[(value_type &)this_8(D) + 4]
411                 .LVL32:
412 010d 4183FD01       cmpl    , %r13d   #, prolog_loop_niters.140
413 0111 0F841901       je  .L29    #,
413      0000
414 0117 83450801       addl    , 8(%rbp) #, MEM[(value_type &)this_8(D) + 4]
415                 .LVL33:
416 011b 4183FD02       cmpl    , %r13d   #, prolog_loop_niters.140
417 011f 0F842801       je  .L30    #,
417      0000
418 0125 83450C01       addl    , 12(%rbp)    #, MEM[(value_type &)this_8(D) + 4]
419                 .LVL34:
420 0129 BF610000       movl    , %edi   #, ivtmp_12
420      00
421                 # main.cpp:61:             for (unsigned int j = 0; j < 100; ++j) {
61:main.cpp      ****                 dataArr[j] += 1;
422                     .loc 4 61 0
423 012e 41BA0300       movl    , %r10d   #, j
423      0000
424                 .LVL35:
425                 .L18:
426 0134 31C0           xorl    %eax, %eax  # ivtmp.156
427 0136 31D2           xorl    %edx, %edx  # ivtmp.153
428 0138 0F1F8400       .p2align 4,,10
428      00000000 
429                     .p2align 3
430                 .L20:
431 0140 83C201         addl    , %edx    #, ivtmp.153
432                 # main.cpp:62:                 dataArr[j] += 1;
433                     .loc 4 62 0
434 0143 66410F6F       movdqa  (%r14,%rax), %xmm0  # MEM[base: vectp_this.148_118, index: ivtmp.156_48, offset: 0], vect__2
434      0406
435 0149 660FFEC1       paddd   %xmm1, %xmm0    # tmp231, vect__25.150
436 014d 410F2904       movaps  %xmm0, (%r14,%rax)  # vect__25.150, MEM[base: vectp_this.148_118, index: ivtmp.156_48, offse
436      06
437 0152 4883C010       addq    , %rax   #, ivtmp.156
438 0156 4439FA         cmpl    %r15d, %edx # bnd.143, ivtmp.153
439 0159 72E5           jb  .L20    #,
440 015b 4429E7         subl    %r12d, %edi # niters_vector_mult_vf.144, ivtmp_12
441 015e 4439E3         cmpl    %r12d, %ebx # niters_vector_mult_vf.144, niters.142
442 0161 438D0422       leal    (%r10,%r12), %eax   #, tmp.145
443 0165 89FA           movl    %edi, %edx  # ivtmp_12, tmp.146
444 0167 7421           je  .L21    #,
445                 .LVL36:
446 0169 89C7           movl    %eax, %edi  # tmp.145, tmp.145
447 016b 8344BD04       addl    , 4(%rbp,%rdi,4)  #, MEM[(value_type &)_129 + 4]
447      01
448                 # main.cpp:61:             for (unsigned int j = 0; j < 100; ++j) {
61:main.cpp      ****                 dataArr[j] += 1;
449                     .loc 4 61 0
450 0170 83FA01         cmpl    , %edx    #, tmp.146
451 0173 8D7801         leal    1(%rax), %edi   #,
452                 .LVL37:
453 0176 7412           je  .L21    #,
454                 # main.cpp:62:                 dataArr[j] += 1;
455                     .loc 4 62 0
456 0178 8344BD04       addl    , 4(%rbp,%rdi,4)  #, MEM[(value_type &)_122 + 4]
456      01
457                 # main.cpp:61:             for (unsigned int j = 0; j < 100; ++j) {
61:main.cpp      ****                 dataArr[j] += 1;
458                     .loc 4 61 0
459 017d 83C002         addl    , %eax    #,
460                 .LVL38:
461 0180 83FA02         cmpl    , %edx    #, tmp.146
462 0183 7405           je  .L21    #,
463                 # main.cpp:62:                 dataArr[j] += 1;
464                     .loc 4 62 0
465 0185 83448504       addl    , 4(%rbp,%rax,4)  #, MEM[(value_type &)_160 + 4]
465      01
466                 .LVL39:
467                 .L21:
468                 .LBE930:
469                 # main.cpp:60:         for (unsigned int i = 0; i < 10000000; ++i) {
60:main.cpp      ****             for (unsigned int j = 0; j < 100; ++j) {
470                     .loc 4 60 0
471 018a 83EE01         subl    , %esi    #, ivtmp_3
472 018d 0F856DFF       jne .L22    #,
472      FFFF

与PAD_ALIGNMENT:

57:main.cpp      ****     void processData() {
58:main.cpp      ****         // Add 1 to every element a bunch of times.
59:main.cpp      ****         std::cout << "Processing data." << std::endl;
60:main.cpp      ****         for (unsigned int i = 0; i < 10000000; ++i) {
61:main.cpp      ****             for (unsigned int j = 0; j < 100; ++j) {
62:main.cpp      ****                 dataArr[j] += 1;
410                     .loc 4 62 0
411 0109 83454401       addl    , 68(%rbp)    #, MEM[(value_type &)this_8(D) + 68]
412                 .LVL32:
413 010d 4183FD01       cmpl    , %r13d   #, prolog_loop_niters.140
414 0111 0F841901       je  .L29    #,
414      0000
415 0117 83454801       addl    , 72(%rbp)    #, MEM[(value_type &)this_8(D) + 68]
416                 .LVL33:
417 011b 4183FD02       cmpl    , %r13d   #, prolog_loop_niters.140
418 011f 0F842801       je  .L30    #,
418      0000
419 0125 83454C01       addl    , 76(%rbp)    #, MEM[(value_type &)this_8(D) + 68]
420                 .LVL34:
421 0129 BF610000       movl    , %edi   #, ivtmp_12
421      00
422                 # main.cpp:61:             for (unsigned int j = 0; j < 100; ++j) {
61:main.cpp      ****                 dataArr[j] += 1;
423                     .loc 4 61 0
424 012e 41BA0300       movl    , %r10d   #, j
424      0000
425                 .LVL35:
426                 .L18:
427 0134 31C0           xorl    %eax, %eax  # ivtmp.156
428 0136 31D2           xorl    %edx, %edx  # ivtmp.153
429 0138 0F1F8400       .p2align 4,,10
429      00000000 
430                     .p2align 3
431                 .L20:
432 0140 83C201         addl    , %edx    #, ivtmp.153
433                 # main.cpp:62:                 dataArr[j] += 1;
434                     .loc 4 62 0
435 0143 66410F6F       movdqa  (%r14,%rax), %xmm0  # MEM[base: vectp_this.148_118, index: ivtmp.156_48, offset: 0], vect__2
435      0406
436 0149 660FFEC1       paddd   %xmm1, %xmm0    # tmp231, vect__25.150
437 014d 410F2904       movaps  %xmm0, (%r14,%rax)  # vect__25.150, MEM[base: vectp_this.148_118, index: ivtmp.156_48, offse
437      06
438 0152 4883C010       addq    , %rax   #, ivtmp.156
439 0156 4439FA         cmpl    %r15d, %edx # bnd.143, ivtmp.153
440 0159 72E5           jb  .L20    #,
441 015b 4429E7         subl    %r12d, %edi # niters_vector_mult_vf.144, ivtmp_12
442 015e 4439E3         cmpl    %r12d, %ebx # niters_vector_mult_vf.144, niters.142
443 0161 438D0422       leal    (%r10,%r12), %eax   #, tmp.145
444 0165 89FA           movl    %edi, %edx  # ivtmp_12, tmp.146
445 0167 7421           je  .L21    #,
446                 .LVL36:
447 0169 89C7           movl    %eax, %edi  # tmp.145, tmp.145
448 016b 8344BD44       addl    , 68(%rbp,%rdi,4) #, MEM[(value_type &)_129 + 68]
448      01
449                 # main.cpp:61:             for (unsigned int j = 0; j < 100; ++j) {
61:main.cpp      ****                 dataArr[j] += 1;
450                     .loc 4 61 0
451 0170 83FA01         cmpl    , %edx    #, tmp.146
452 0173 8D7801         leal    1(%rax), %edi   #,
453                 .LVL37:
454 0176 7412           je  .L21    #,
455                 # main.cpp:62:                 dataArr[j] += 1;
456                     .loc 4 62 0
457 0178 8344BD44       addl    , 68(%rbp,%rdi,4) #, MEM[(value_type &)_122 + 68]
457      01
458                 # main.cpp:61:             for (unsigned int j = 0; j < 100; ++j) {
61:main.cpp      ****                 dataArr[j] += 1;
459                     .loc 4 61 0
460 017d 83C002         addl    , %eax    #,
461                 .LVL38:
462 0180 83FA02         cmpl    , %edx    #, tmp.146
463 0183 7405           je  .L21    #,
464                 # main.cpp:62:                 dataArr[j] += 1;
465                     .loc 4 62 0
466 0185 83448544       addl    , 68(%rbp,%rax,4) #, MEM[(value_type &)_160 + 68]
466      01
467                 .LVL39:
468                 .L21:
469                 .LBE930:
470                 # main.cpp:60:         for (unsigned int i = 0; i < 10000000; ++i) {
60:main.cpp      ****             for (unsigned int j = 0; j < 100; ++j) {
471                     .loc 4 60 0
472 018a 83EE01         subl    , %esi    #, ivtmp_3
473 018d 0F856DFF       jne .L22    #,
473      FFFF

类型 SideProcessor 的实例具有以下字段:

    std::atomic<bool> processingRequested;
#ifdef PAD_ALIGNMENT
    std::array<bool, 64> padding;
#endif
    std::array<int, 100> dataArr;

processingRequested的大小大概是一个字节。如果没有 PAD_ALIGNMENT,编译器可能会安排字段,使 dataArr 的前几个元素与 processingRequested 在相同的 64 字节缓存行中。但是,对于 PAD_ALIGNMENT,两个字段之间会有 64 字节的间隔,因此数组的第一个元素和 processingRequested 将在不同的缓存行中。

单独考虑 processData 中的循环,人们会期望 dataArr 的所有 100 个元素都能轻松放入 L1D 缓存,因此绝大多数访问都应该命中L1D。但是,主线程在 while (!(sideProcessor.isDone())) { } 中读取 processingRequested,而处理线程在 processData 中执行循环。如果没有 PAD_ALIGNMENT,主线程希望从处理线程希望读取和写入的同一缓存行读取。这会导致错误共享情况,其中共享缓存行在线程所在的两个内核的私有缓存之间反复跳转 运行。

在同一 LLC 共享域中的两个内核之间进行虚假共享时,LLC 的未命中次数可以忽略不计(它可以阻止请求,因此它们不会转到 DRAM),但会有来自两个内核的大量读取和 RFO 请求。这就是 LLC 未命中事件计数很小的原因。

在我看来,编译器已将 processData 中的循环展开四次,并使用 16 字节 SSE 指令将其矢量化。这可以解释为什么商店数量接近十亿分之一。没有的话,负载的数量大约是十亿,其中大约四分之一来自处理线程,其余大部分来自主线程。 while (!(sideProcessor.isDone())) { }中执行的加载次数取决于完成processData执行所需的时间。所以在没有false sharing的情况下(withPAD_ALIGNMENT),load的数量要小很多是有道理的。

在没有PAD_ALIGNMENT的情况下,大部分L1-dcache-load-missesLLC-loads事件来自主线程产生的请求,而大部分LLC-stores事件来自处理线程生成的请求。所有这些请求都是针对包含 processingRequested 的行。 LLC-storesLLC-loads 大得多是有道理的,因为主线程比处理线程更快速地访问该行,因此 RFO 更有可能在处理时错过核心的私有缓存线程是 运行。我认为大多数 L1-dcache-load-misses 事件也代表从主线程到共享缓存行的负载。看起来这些加载中只有三分之一未命中私有 L2 缓存,这表明该行已被预取到 L2 中。这可以通过禁用 L2 预取器并检查 L1-dcache-load-misses 是否大约等于 LLC-loads.

来验证