【问题标题】:x86 MESI invalidate cache line latency issuex86 MESI 无效缓存线延迟问题
【发布时间】:2019-06-10 01:35:07
【问题描述】:

我有以下进程,我尝试让 ProcessB 的延迟非常低,所以我一直使用紧密循环并隔离 cpu core 2。

共享内存中的全局变量:

int bDOIT ;
typedef struct XYZ_ {
    int field1 ;
    int field2 ;
    .....
    int field20;
}  XYZ;
XYZ glbXYZ ; 

static void escape(void* p) {
    asm volatile("" : : "g"(p) : "memory");
} 

ProcessA(在核心 1 中)

while(1){
    nonblocking_recv(fd,&iret);
    if( errno == EAGAIN)
        continue ; 
    if( iret == 1 )
        bDOIT = 1 ;
    else
        bDOIT = 0 ;
 } // while

ProcessB(在核心 2 中)

while(1){
    escape(&bDOIT) ;
    if( bDOIT ){
        memcpy(localxyz,glbXYZ) ; // ignore lock issue 
        doSomething(localxyz) ;
    }
} //while 

ProcessC(在核心 3 中)

while(1){
     usleep(1000) ;
     glbXYZ.field1 = xx ;
     glbXYZ.field2 = xxx ;
     ....
     glbXYZ.field20 = xxxx ;  
} //while

在这些简单的伪代码进程中,而 ProcessesA 将 bDOIT 修改为 1 ,它将使缓存行无效 核心 2 ,然后在 ProcessB 获得 bDOIT=1 然后 ProcessB 会做 memcpy(localxyz,glbXYZ) 。

由于每 1000 微秒,ProcessC 将使 glbXYZ 在 Core2 ,我想这会影响延迟,而 ProcessB 尝试做 memcpy(localxyz,glbXYZ) ,因为虽然 ProcessB 扫描 bDOIT 到 1 ,glbXYZ 被无效 ProcessC已经,

glbXYZ 的新值仍在核心 3 L1$ 或 L2$ 之后 ProcessB 实际上得到 bDOIT=1 ,此时 core2 就知道了 它的 glbXYZ 无效,因此它询问 glbXYZ 的新值 此时,ProcessB 的延迟受到等待 glbXYZ 的新值的影响。

我的问题:

如果我有一个 processD(在核心 4 中),它会:

while(1){
    usleep(10);
    memcpy(nouseXYZ,glbXYZ);
 } //while 

这个 ProcessD 是否会让 glbXYZ 更早地刷新到 L3$ 所以 当核心 2 中的 ProcessB 知道它的 glbXYZ 无效时,它会询问 glbXYZ 的新值, 这个 ProcessD 将帮助 PrcoessB 更早地获得 glbXYZ ?! 由于 ProcessD 一直在帮助将 glbXYZ 转换为 L3$。

【问题讨论】:

    标签: performance x86 shared-memory cpu-cache mesi


    【解决方案1】:

    有趣的想法,是的,这可能应该让保存你的结构的缓存线进入 L3 缓存中的状态,其中 core#2 可以直接获得 L3 命中,而不必等待 MESI 读取在核心#2 的 L1d 中线路仍处于 M 状态时请求。

    或者,如果 ProcessD 运行在与 ProcessB 相同物理内核的另一个逻辑内核上,则数据将被提取到正确的 L1d 中。如果它大部分时间都处于休眠状态(并且很少醒来),ProcessB 通常仍将拥有整个 CPU,以单线程模式运行,而不会对 ROB 和存储缓冲区进行分区。

    不是让虚拟访问线程在usleep(10) 上旋转,您可以让它等待一个条件变量或 ProcessC 在写入 glbXYZ 后触发的信号量

    使用计数信号量(如 POSIX C 信号量sem_wait/sem_post),写入glbXYZ 的线程可以增加信号量,触发操作系统唤醒在sem_down 中阻塞的 ProcessD。如果由于某种原因 ProcessD 错过了唤醒,它将在再次阻塞之前执行 2 次迭代,但这很好。 (嗯,所以实际上我们不需要计数信号量,但我认为我们确实需要操作系统辅助的睡眠/唤醒,这是一种简单的方法,除非我们需要避免在 processC 之后的系统调用开销编写结构体。)或者 ProcessC 中的 raise() 系统调用可以发送信号来触发 ProcessD 的唤醒。

    借助 Spectre+Meltdown 缓解措施,任何系统调用,即使是像 Linux futex 这样的高效系统调用,对于创建它的线程来说都是相当昂贵的。不过,这个成本并不是您试图缩短的关键路径的一部分,而且它仍然比您在两次提取之间考虑的 10 微秒睡眠时间要少得多。

    void ProcessD(void) {
        while(1){
            sem_wait(something);          // allows one iteration to run per sem_post
            __builtin_prefetch (&glbXYZ, 0, 1);  // PREFETCHT2 into L2 and L3 cache
        }
    }
    

    (根据Intel's optimization manual section 7.3.2,当前 CPU 上的 PREFETCHT2 与 PREFETCHT1 相同,并获取 L2 缓存(以及沿途的 L3。我没有检查 AMD。 What level of the cache does PREFETCHT2 fetch into?)。

    我还没有测试过 PREFETCHT2 在 Intel 或 AMD CPU 上是否真的有用。您可能想要使用虚拟的volatile 访问,例如*(volatile char*)&glbXYZ;*(volatile int*)&glbXYZ.field1。特别是如果您的 ProcessD 与 ProcessB 在同一物理内核上运行。

    如果prefetchT2 有效,您可以在写入bDOIT (ProcessA) 的线程中执行此操作,因此它可以在 ProcessB 需要它之前触发行迁移到 L3。

    如果您发现该行在使用前被驱逐,也许您确实想要一个线程在获取该缓存行时旋转。

    在未来的 Intel CPU 上,您可以在写入后使用 cldemote instruction (_cldemote(const void*)) 来触发脏缓存行迁移到 L3。它在不支持它的 CPU 上作为 NOP 运行,但到目前为止它仅适用于 Tremont (Atom)。 (与umonitor/umwait 一起,当另一个内核在用户空间的受监控范围内写入时唤醒,这对于低延迟的内核间内容可能也非常有用。)


    由于 ProcessA 不写入结构,您可能应该确保bDOIT 与结构位于不同的缓存行中。您可以将alignas(64) 放在XYZ 的第一个成员上,这样结构就从缓存行的开头开始。 alignas(64) atomic<int> bDOIT; 会确保它也在一行的开头,所以他们不能共享一个缓存行。或者将其设为alignas(64) atomic<bool>atomic_flag

    另见Understanding std::hardware_destructive_interference_size and std::hardware_constructive_interference_size1:通常 128 是您希望避免由于邻线预取器而导致错误共享的值,但如果 ProcessB 触发 L2 邻线预取器,这实际上并不是一件坏事在核心#2 上,当它在 bDOIT 上旋转时,推测性地将 glbXYZ 拉入其 L2 缓存。因此,如果您使用的是 Intel CPU,您可能希望将它们组合成一个 128 字节对齐的结构。

    并且/或者如果bDOIT 为假,您甚至可以在进程B 中使用软件预取。预取不会阻塞等待数据,但如果读取请求在中间到达ProcessC 的写作glbXYZ 那么它会花费更长的时间。所以也许只有每 16 或 64 次的 SW 预取 bDOIT 是假的?


    并且不要忘记在你的旋转循环中使用_mm_pause(),以避免当你旋转的分支转到另一个方向时内存顺序错误推测管道核弹。 (通常这是自旋等待循环中的循环退出分支,但这无关紧要。您的分支逻辑等效于包含自旋等待循环的外部无限循环,然后进行一些工作,即使这不是您编写的方式.)

    或者可能使用lock cmpxchg 而不是纯负载来读取旧值。完全障碍已经阻止了障碍之后的投机负载,因此请防止错误推测。 (您可以在 C11 中使用 atomic_compare_exchange_weak 和 expected = desired 执行此操作。它通过引用获取 expected,并在比较失败时更新它。)但是使用 lock cmpxchg 敲击缓存行可能对 ProcessA 没有帮助能够快速将其存储提交到 L1d。

    检查machine_clears.memory_ordering 性能计数器,看看在没有_mm_pause 的情况下是否会发生这种情况。 如果是,请先尝试_mm_pause,然后再尝试使用atomic_compare_exchange_weak 作为负载.或者atomic_fetch_add(&bDOIT, 0),因为lock xadd 是等价的。


    // GNU C11.  The typedef in your question looks like C, redundant in C++, so I assumed C.
    
    #include <immintrin.h>
    #include <stdatomic.h>
    #include <stdalign.h>
    
    alignas(64) atomic_bool bDOIT;
    typedef struct { int a,b,c,d;       // 16 bytes
                     int e,f,g,h;       // another 16
    } XYZ;
    alignas(64) XYZ glbXYZ;
    
    extern void doSomething(XYZ);
    
    // just one object (of arbitrary type) that might be modified
    // maybe cheaper than a "memory" clobber (compile-time memory barrier)
    #define MAYBE_MODIFIED(x) asm volatile("": "+g"(x))
    
    // suggested ProcessB
    void ProcessB(void) {
        int prefetch_counter = 32;  // local that doesn't escape
        while(1){
            if (atomic_load_explicit(&bDOIT, memory_order_acquire)){
                MAYBE_MODIFIED(glbXYZ);
                XYZ localxyz = glbXYZ;    // or maybe a seqlock_read
      //        MAYBE_MODIFIED(glbXYZ);  // worse code from clang, but still good with gcc, unlike a "memory" clobber which can make gcc store localxyz separately from writing it to the stack as a function arg
    
      //          asm("":::"memory");   // make sure it finishes reading glbXYZ instead of optimizing away the copy and doing it during doSomething
                // localxyz hasn't escaped the function, so it shouldn't be spilled because of the memory barrier
                // but if it's too big to be passed in RDI+RSI, code-gen is in practice worse
                doSomething(localxyz);
            } else {
    
                if (0 == --prefetch_counter) {
                    // not too often: don't want to slow down writes
                    __builtin_prefetch(&glbXYZ, 0, 3);  // PREFETCHT0 into L1d cache
                    prefetch_counter = 32;
                }
    
                _mm_pause();       // avoids memory order mis-speculation on bDOIT
                                   // probably worth it for latency and throughput
                                   // even though it pauses for ~100 cycles on Skylake and newer, up from ~5 on earlier Intel.
            }
    
        }
    }
    

    This compiles nicely on Godbolt 非常漂亮的 asm。如果bDOIT 保持为真,那么这是一个紧密的循环,调用周围没有开销。 clang7.0 甚至使用 SSE 加载/存储将结构作为函数 arg 一次复制 16 个字节到堆栈。


    很明显,这个问题是一堆未定义的行为,你应该用_Atomic (C11) 或std::atomic (C++11) 和memory_order_relaxed 来解决。或mo_release / mo_acquire 在写入bDOIT 的函数中没有任何内存屏障,因此它可以将其从循环中删除。将其设为 atomic 并放松内存顺序对 asm 的质量几乎为零。

    大概您正在使用 SeqLock 或其他东西来保护 glbXYZ 不被撕裂。是的,asm("":::"memory") 应该通过强制编译器假设它已被异步修改来完成这项工作。 "g"(glbXYZ) 输入的 asm 语句虽然没用。它是全局的,所以 "memory" 屏障已经适用于它(因为 asm 语句已经可以引用它)。如果您想告诉编译器只是它可能已经改变,请使用 asm volatile("" : "+g"(glbXYZ)); 而不使用 "memory" clobber。

    或者在 C(不是 C++)中,只需将其设为 volatile 并进行结构赋值,让编译器选择如何复制它,而不使用障碍。在 C++ 中,foo x = y;volatile foo y; 失败,其中 foo 是一个类似结构的聚合类型。 volatile struct = struct not possible, why?。当您想使用volatile 告诉编译器数据可能作为在 C++ 中实现 SeqLock 的一部分异步更改时,这很烦人,但是您仍然希望让编译器以任意顺序尽可能有效地复制它,而不是一个狭窄的一个成员。


    脚注 1:C++17 指定 std::hardware_destructive_interference_size 作为硬编码 64 或使您自己的 CLSIZE 常量的替代方案,但 gcc 和 clang 尚未实现它,因为它已成为一部分如果在结构中的 alignas() 中使用 ABI,则实际上不能根据实际 L1d 行大小进行更改。

    【讨论】:

    • 感谢您的热心信息,让我先消化一下内容,非常丰富。
    • @barfatchen:我很想知道prefetchT2 是否对使缓存行进入可共享状态有用。或者总的来说,这其中的哪些部分是有用的。
    • 我的生产应用有抖动,我还在猜测原因。
    • 我有一个关于 _mm_pause 的问题,首先,我不会离开旋转循环,所以 _mm_pause 就没有必要了,对吗?!第二:在自旋循环内,_mm_pause 会导致延迟增加,我看不到 _mm_pause 在编译/cpu 内存顺序屏障中可以做的任何帮助。
    • @barfatchen:您的分支相当于一个自旋等待循环,它会进入一些实际工作,并围绕它进行无限循环。更新了我的答案以更清楚地说明这一点。这是pause 在检查之间延迟与您的 CPU 在发现bDOIT 已更改并且有真正的工作要做的那一刻可能因内存顺序错误推测而停滞许多周期之间的权衡。检查machine_clears.memory_ordering 的性能计数器,看看在没有pause 的情况下这是否是一个真正的问题。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-02-17
    • 2019-05-17
    • 2012-08-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多