原子操作成本

Posted

技术标签:

【中文标题】原子操作成本【英文标题】:atomic operation cost 【发布时间】:2011-02-02 01:02:45 【问题描述】:

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

会对缓存产生什么影响?

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

【问题讨论】:

@Jason S,任何。 cas 和 atomic inc/dec 之间的差异可以忽略不计。 x86 上的原子操作会随着内存地址的争用而变慢。我相信通常它们比非锁定操作慢一个数量级,但显然这将取决于所使用的操作、争用和内存屏障。 嗯。 writes 在 x86 上似乎是原子的。 '了解 Linux 内核'->spin_unlock 32 位写入在 Java 中是原子的,即它是可移植的原子(但没有内存屏障语义,因此这对于指针来说通常是不够的)。添加 1 通常不是原子的,除非您添加 LOCK 前缀。关于Linux内核,不用看spin_unlock。请参阅,在当前版本中,arch/x86/include/asm/atomic_32.h(它曾经是 include/asm-i386/atomic.h)。 @Blaisorblade,JAva 不在这里。 LOCKed 操作的成本是多少? 【参考方案1】:

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

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

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


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

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

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

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

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

【讨论】:

为什么改变其他 CPU 上的状态会花费 1 次缓存未命中? 因为它是在 CPU 之外的通信,因此比访问缓存慢。无论如何,缓存未命中必须从其他 CPU 传递。实际上,如果在最新的 Xeon 处理器上使用直接互连,例如 AMD Hypertransport(很久以前)或 Intel 的 Intel QuickPath Interconnect,与另一个 CPU 通信可能比与内存通信更快。基于尼哈勒姆。否则,与其他 CPU 的通信将在与内存相同的 FSB 上进行。在 Wikipedia 上搜索 HyperTransport 和 Front Side Bus 以获取更多信息。 哇,没想到他的成本这么高 - 缓存未命中可能是几千个周期。 真的吗?我习惯的数字是:缓存未命中一百个周期,上下文/权限切换(包括系统调用)数千个周期。 缓存未命中不是几千个周期!它大约 100ns,通常是 300-350 个 CPU 周期....【参考方案2】:

我使用以下设置进行了一些分析:测试机器 (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 个周期

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

为了评估锁定指令在缓存未命中时的成本,我在锁定指令之前添加了一条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

【讨论】:

谢谢!你能发布你的测试代码或自己测试Core2/Core i3/i5/i7吗?您的测试设置中是否已初始化所有内核? 我添加了源代码。只有一个核心被初始化。希望看到其他机器的结果。 CLFLUSH 应该是一种比整个缓存的 WBINVD 更轻松的方式来刷新缓存行。 WBINVD 也会刷新指令缓存,导致额外的缓存未命中。 也许有趣的是测试高速缓存行在共享状态下很热的情况。您可以通过让另一个线程以纯负载读取它来实现这一点。【参考方案3】:

在基于总线的 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本身/指令确保处理器在执行锁定指令之前执行所有指令。 /关于尚未刷新的写入/它强制在执行下一条指令之前将处理器内的所有已发布写入刷新到外部存储器。

/about SMP/semaphore is in cache in S state...针对0字节日期发出读取和无效事务(这是相邻CPU中高速缓存行的kill/of共享副本/)

【讨论】:

自 1995 年的 P6/Pentium Pro 架构 (source) 以来,就不再使用基于总线的 SMP。现在LOCK 不会每次都做总线锁,除非数据在缓存行上没有对齐,或者存在缓存争用。查看rigtorp.se/split-locks 获取最新号码。

以上是关于原子操作成本的主要内容,如果未能解决你的问题,请参考以下文章

原子操作类原子操作的实现原理

原子操作 vs 非原子操作

原子操作类原子操作类详细介绍

单核,多核CPU的原子操作

原子操作的特性

linux 原子操作