97

原子操作的成本是多少(比较和交换或原子加/减)?它消耗多少周期?它会暂停 SMP 或 NUMA 上的其他处理器,还是会阻止内存访问?它会在乱序 CPU 中刷新重新排序缓冲区吗?

对缓存会有什么影响?

我对现代流行的 CPU 很感兴趣:x86、x86_64、PowerPC、SPARC、Itanium。

4

3 回答 3

62

这几天我一直在寻找实际数据,但一无所获。但是,我做了一些研究,比较了原子操作的成本和缓存未命中的成本。

lock cmpxchg在 PentiumPro 之前(如文档中所述)的 x86 LOCK 前缀(包括原子 CAS)的成本是内存访问(如缓存未命中),+ 停止其他处理器的内存操作,+ 与其他处理器的任何争用试图锁定总线。但是,由于 PentiumPro,对于普通的 Writeback 可缓存内存(应用程序处理的所有内存,除非您直接与硬件交谈),而不是阻止所有内存操作,而是仅阻止相关的缓存行(基于@osgx 答案中的链接) .

即核心延迟对线路的MESI 共享和RFO 请求的响应,直到实际locked 操作的存储部分之后。这称为“高速缓存锁”,并且只影响那一个高速缓存行。其他核心可以同时加载/存储甚至 CASing 其他线路。


实际上,CAS 案例可能更复杂,如本页所述,没有时间安排,但值得信赖的工程师有深刻的描述。(至少对于在实际 CAS 之前进行纯加载的正常用例。)

在讨论太多细节之前,我会说一个 LOCKed 操作会花费一次缓存未命中 + 可能与同一缓存行上的其他处理器争用,而 CAS + 前面的负载(这几乎总是需要的,除了互斥锁,你总是CAS 0 和 1) 可能会导致两次缓存未命中。

他解释说,单个位置上的加载 + CAS 实际上会导致两次缓存未命中,例如 Load-Linked/Store-Conditional(后者见那里)。他的解释依赖于MESI 缓存一致性协议的知识。它对缓存线使用 4 种状态:M(odified)、E(xclusive)、S(hared)、I(nvalid)(因此称为 MESI),下面将在需要时进行解释。解释的场景如下:

  • LOAD 导致缓存未命中 - 相关的缓存线在共享状态下从内存中加载(即其他处理器仍被允许将该缓存线保留在内存中;在此状态下不允许更改)。如果该位置在内存中,则跳过此缓存未命中。可能的成本:1 次缓存未命中。(如果cacheline 处于Shared、Exclusive 或Modified 状态则跳过,即数据在此CPU 的L1 高速缓存中)。
  • 程序计算要存储的新值,
  • 它运行一个原子 CAS 指令。
    • 它必须避免并发修改,因此它必须从其他 CPU 的缓存中删除缓存线的副本,以将缓存线移动到独占状态。可能的成本:1 次缓存未命中。如果它已经是独占拥有的,即处于独占或修改状态,则不需要这样做。在这两种状态下,没有其他 CPU 持有缓存线,但在独占状态下它还没有被修改(还)。
    • 在此通信之后,变量在我们 CPU 的本地缓存中被修改,此时它对所有其他 CPU 全局可见(因为它们的缓存与我们的一致)。它最终将根据通常的算法写入主存储器。
    • 试图读取或修改该变量的其他处理器将首先必须以共享或独占模式获取该缓存线,并且这样做将联系该处理器并接收缓存线的更新版本。相反,LOCKed 操作只能花费缓存未命中(因为缓存行将直接在 Exclusive 状态下请求)。

在所有情况下,缓存线请求都可能被其他已经修改数据的处理器停止。

于 2010-05-06T19:50:50.610 回答
39

我使用以下设置进行了一些分析:启动了测试机器(AMD Athlon64 x2 3800+),切换到长模式(禁用中断),感兴趣的指令在循环中执行,展开 100 次迭代和 1,000 次循环周期。循环体对齐到 16 个字节。时间是在循环前后使用 rdtsc 指令测量的。此外,执行了一个没有任何指令的虚拟循环(每次循环迭代测量 2 个周期,其余 14 个周期),并且从指令分析时间的结果中减去结果。

测量了以下指令:

  • " lock cmpxchg [rsp - 8], rdx" (有比较匹配和不匹配),
  • " lock xadd [rsp - 8], rdx",
  • " lock bts qword ptr [rsp - 8], 1"

在所有情况下,测量的时间约为 310 个周期,误差约为 +/- 8 个周期

这是在同一(缓存)内存上重复执行的值。随着额外的缓存未命中,时间要长得多。这也是在两个核心中只有一个处于活动状态的情况下完成的,因此缓存是专有的,不需要缓存同步。

为了评估缓存未命中时锁定指令的成本,我wbinvld在锁定指令之前添加了一条指令,并将wbinvld加号add [rsp - 8], rax放入比较循环中。在这两种情况下,每个指令对的成本约为 80,000 个周期!在锁定 bts 的情况下,时间差约为每条指令 180 个周期。

请注意,这是倒数吞吐量,但由于锁定操作是序列化操作,因此延迟可能没有区别。

结论:锁定操作很重,但缓存未命中可能要重得多。另外:锁定操作不会导致缓存未命中。当缓存线不是独占时,它只会导致缓存同步流量。

为了启动机器,我使用了来自 ReactOS 项目的 x64 版本的 FreeLdr。这是asm源代码:

#define LOOP_COUNT 1000
#define UNROLLED_COUNT 100

PUBLIC ProfileDummy
ProfileDummy:

    cli

    // Get current TSC value into r8
    rdtsc
    mov r8, rdx
    shl r8, 32
    or r8, rax

    mov rcx, LOOP_COUNT
    jmp looper1

.align 16
looper1:

REPEAT UNROLLED_COUNT
    // nothing, or add something to compare against
ENDR

    dec rcx
    jnz looper1

    // Put new TSC minus old TSC into rax
    rdtsc
    shl rdx, 32
    or rax, rdx
    sub rax, r8

    ret

PUBLIC ProfileFunction
ProfileFunction:

    cli

    rdtsc
    mov r8, rdx
    shl r8, 32
    or r8, rax
    mov rcx, LOOP_COUNT

    jmp looper2

.align 16
looper2:

REPEAT UNROLLED_COUNT
    // Put here the code you want to profile
    // make sure it doesn't mess up non-volatiles or r8
    lock bts qword ptr [rsp - 8], 1
ENDR

    dec rcx
    jnz looper2

    rdtsc
    shl rdx, 32
    or rax, rdx
    sub rax, r8

    ret
于 2013-04-21T15:04:17.657 回答
4

在基于总线的 SMP 上,原子前缀LOCK确实断言(打开)总线信号LOCK#。它将禁止总线上的其他 CPU/设备使用它。

Ppro & P2 书http://books.google.com/books?id=3gDmyIYvFH4C&pg=PA245&dq=lock+instruction+pentium&lr=&ei=_E61S5ehLI78zQSzrqwI&cd=1#v=onepage&q=lock%20instruction%20pentium&f=false第 244-246 页

锁定指令是序列化、同步操作.... /about Out-of-order/locked RMW/read-modify-write = atomic itself/指令确保处理器在执行锁定指令之前执行所有指令。/关于尚未刷新的写入/它强制在执行下一条指令之前将处理器内的所有已发布写入刷新到外部存储器。

/about SMP/ 信号量在 S 状态的缓存中...针对 0 字节日期发出读取和无效事务(这是相邻 CPU 中缓存行的共享副本的终止/)

于 2010-04-02T02:41:05.500 回答