【问题标题】:Atomic increment is flushing other cache lines (code and results provided)?原子增量正在刷新其他缓存行(提供的代码和结果)?
【发布时间】:2014-06-08 03:49:52
【问题描述】:

设置:Intel Ivy Bridge Core i7,在 64 位模式下编译,MSVC(2012) 和 Win 7 64 位。

我试图了解原子增量是否会导致缓存未命中。

我设置了一个测试,其中一个原子变量和另一个变量在同一个缓存行中,而不是在同一个缓存行中,然后比较缓存未命中。代码和结果如下。

结果

不同的缓存行:

  • 原子增量没有 L1 缓存未命中
  • d.a 的两个增量都有 40-50% 的 L1 缓存未命中。

相同的缓存行

  • 递增d.a 没有缓存未命中
  • 递增原子遇到100% L1 缓存未命中

谁能解释一下?!我期待当原子与 d.a 位于同一缓存行时,d.a 将遭受 100% 的缓存未命中,而当它们位于不同的缓存行时 d.a 不会受到影响。

#include <atomic>
#include <iostream>
#include <iomanip>
#include <vector>

//Structure to ensure not in same cache line
__declspec(align(64)) struct S{
  volatile double a,b,d,c,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t;
  volatile std::atomic<short> atom;
};

//Structure to ensure same cache line
/*__declspec(align(64)) struct S{
   volatile std::atomic<short> atom;
   volatile short a;
};*/


int main(){

    volatile S d;

    for(long long i=0; i<1000000000; i++){
        d.a++;
        d.atom++;
        d.a++;
    }
}

这里更新了一些asm:

    /* _Atomic_fetch_add_2, _Atomic_fetch_sub_2 */
inline _Uint2_t _Fetch_add_seq_cst_2(volatile _Uint2_t *_Tgt, _Uint2_t _Value)
    {   /* add _Value to *_Tgt atomically with
 mov         word ptr [_Tgt],dx  
 mov         qword ptr [rsp+8],rcx  
 push        rdi  
            sequentially consistent memory order */

    return (_INTRIN_SEQ_CST(_InterlockedExchangeAdd16)((volatile short *)_Tgt, _Value));
 movzx       eax,word ptr [_Value]  
 mov         rcx,qword ptr [_Tgt]  
 lock xadd   word ptr [rcx],ax  
    }
 pop         rdi  

【问题讨论】:

  • 如果您比较原子/互锁 API 及其在 Linux、FreeBSD 和 Windows 以及可能在其他操作系统上的行为,您会发现您提供的代码没有提及您运行代码的位置可能会成功不可能给你的问题一个好的答案。另外,我想知道您如何测量缓存未命中。也许,这将有助于使用低级操作系统特定 API 重现代码。可以选择如何使用原子增量。例如,在 Windows 上,存在 NUMA 特定的变体。主要是 InterlockedIncrement/Acquire/Release 等。#include &lt;atomic&gt; 并没有显示所有这些。
  • 已添加更多详细信息 - 使用 Win 7 和 MSVC 11 (2012)。使用英特尔 VTune 测量缓存未命中。
  • 深入内部直到找到“牛肉”,产生原子运算符 ++ 使用 _InterlockedExchangeAdd16 。阅读MSDN: InterlockedExchangeAdd() 并希望 16 位版本能够相应地工作,您选择了 Windows 互锁 API 的“完全内存屏障”版本。此评论旨在为其他人节省一些时间来了解他们实际在看什么。
  • 为了排除您对 L1 缓存行大小的假设确实与您的结构内容相匹配的可能性,您可以使用 How to find out cache line size 在代码中添加一些运行时检查。跨度>

标签: c++ multithreading performance caching atomic


【解决方案1】:

看这个序列:

for(long long i=0; i<1000000000; i++){
    d.a++;
    d.atom++;
    d.a++;
}

我们可以将其重写为(大致):

for(long long i=0; i<1000000000 / 4; i+=4){
    d.a++;
    d.atom++;
    d.a++;
    d.a++;
    d.atom++;
    d.a++;
    d.a++;
    d.atom++;
    d.a++;
    d.a++;
    d.atom++;
    d.a++;
}

我可以继续并进一步扩展循环,但很明显在d.atom++ 之后连续有两个d.a++

换句话说,您应该预计 d.a++ 上大约 50% 的缓存未命中,这是基于循环结束时的 d.a++ 为循环的下一次迭代获取 d.a++ 的数据。与此的任何差异都将是测量错误(我认为缓存未命中是根据统计数据测量的,而不是在精确行上的精确步骤)。

在 d.a 和 d.atom 位于不同缓存行的情况下,显然d.atom++ 获取了该特定地址上的所有缓存未命中,解释了这种情况下的 100% 数字。

我不能 100% 确定锁定操作的定义(“锁定操作”= x86 领域中的原子)需要缓存刷新,但它肯定需要“独占访问”,这意味着所有其他 CPU( cores) 将需要被告知“您现在必须从缓存中刷新此数据的任何副本”。从您的测试中可以看出,至少在这种处理器型号上,这等同于“刷新该行的所有缓存”,包括当前保存数据的缓存。

【讨论】:

  • 感谢您的回答。两个问题:为什么d.a的双连续增量意味着缓存未命中?当然,如果 d.a 刚刚增加,那么该值仍在寄存器中并且不需要缓存加载?
  • 有什么办法可以调整我的测试,为原子增量刷新缓存行提供更强有力的案例?
  • 由于d.avolatile,编译器不允许将其加载到寄存器中并在写回内存之前进行多次更新,因此每个d.a++ 将产生一个单独的对该缓存行的内存访问。为什么你需要一个更强有力的案例,我会说你的结果是非常确凿的。
  • 但这一切都是在一个核心上完成的,所以我不明白为什么缓存会被刷新? :s
  • 您能否详细说明这一点:“在 d.a 和 d.atom 位于不同缓存行的情况下,显然 d.atom++ 获取了该特定地址上的所有缓存未命中,解释了在那种情况下是 100% 的数字。”我不明白为什么 d.atom 会得到这么多缓存未命中?除非我们明确声明 CPU 在进行原子更新时会刷新缓存?
【解决方案2】:

请参阅 SO 讨论 What is a memory fence?。这就是现代 Intel CPU 实现锁定指令的方式。内存屏障强制将数据完全写入内存,然后在下次访问时重新读取。在 [ref to A] [barrier] [ref to A] 的情况下,CPU 无法对第二个参考进行任何巧妙的预取。所有屏障指令都以相同的方式运行,但有些指令允许您明确限制为左或右栅栏模式。

执行屏障指令会导致性能下降。试图了解这种命中的特征会因 CPU 架构而异,而在硬件中执行此操作的算法的内部结构并不是英特尔将详细发布的内容。

【讨论】:

  • 也许说:“现代 CPU 在考虑内存屏障的情况下实现锁定指令”而不是“锁定指令被实现为内存屏障”会更合适。这就是为什么在 WIN32 API 集中提供替代方案的原因,例如 InterlockedIncrement()/InterlockedIncremeentAcquire()/... 至于 OP 代码,使用编译器内在 _InterlockedExchangeAdd16,我们只能理论化此内在适用的围栏。
  • 我考虑的是底层的英特尔机器代码,而不是 GCC 或 VSC + WinAPI 等编译器如何在源代码级别公开它们。事实上,旧的锁定前缀现在已被弃用,取而代之的是 *fence 指令。有关更多讨论,请参阅this thread 等。
  • 这确实是一个令人困惑的话题。防护业务实际上有 2 个利益相关者:CPU(使用锁定前缀、LFENCE、MFENCE、SFENCE)和编译器也需要利用这些重新排序约束。例如,如果您查看 InterlockedIncrement() - MSDN 文档状态,那就是创建一个完整的围栏。但是,如果您查看生成的汇编代码,则看不到 MFENCE。只是一把锁; xadd 指令(在 x86 上...)。
猜你喜欢
  • 2021-06-30
  • 2014-09-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-10-10
相关资源
最近更新 更多