【问题标题】:fastest way to atomically compare two integers in C?在C中以原子方式比较两个整数的最快方法?
【发布时间】:2011-09-22 20:58:01
【问题描述】:
uint64_t n;      // two 32-bit integers

return ( (uint32_t)(n >> 32) == (uint32_t)n );

以原子方式比较 uint64_t 的 32 个最高有效位和 32 个最低有效位的最快方法是什么?

我认为一个可怕的解决方案是:获取自旋锁,读取 32 LSB,读取 32 MSB,比较得到结果,释放自旋锁,返回结果。有什么方法可以做到这一点而不必使用自旋锁?

【问题讨论】:

  • 这不完全取决于底层架构而不是 C 语言吗?

标签: c caching atomic bit


【解决方案1】:
  1. 只有当您能够在您的平台上以原子方式检索 64 位数字时,您才能在不加锁的情况下执行此操作。如果有可能,那么首先 - 您以您最喜欢的方式自动检索 64 位值(例如 InterlockedOr64(ptr,0) 在 64 位窗口上,如果您有 32 位 x86 CPU 则没有办法 - 除非您Intel CPU 不早于 Pentium 并且您确保您的 64 位值是 64 位对齐的,不确定其他供应商的 x86 CPU),其次 - 与您检索的值进行比较。

  2. 你显然不能以便携的方式做到这一点。在您无法以原子方式获取 64 位数字的平台上,如果没有锁定,则无法做到这一点。

编辑

由于一些严重误导的想法在这次讨论中大受欢迎,我觉得我有责任写一些关于未能使用 32 位数字的比较和交换来解决问题的说明。

假设我们有 x86 平台,那么我们可能会编写 asm 代码:​​

    mov eax, [num+4]
    lock cmpxchg [num], eax
    jz  equal_case_code
    ; non-equal case code follows
equal_case_code:
    ; equal case code follows

显然这个实现不是原子的 - 线程可能在 movcmpxchg 指令之间被中断(通常在一条指令中不允许有两个内存操作数)。

来自不同 API(如来自 Win32 API 的InterlockedCompareExchange)的基于 32 位的 Compare&Exchange 函数也不能提供正确的解决方案,因为它们的语义只允许原子访问一个 32 位内存地址。

【讨论】:

  • 在 32 位 x86 上,您应该能够使用 LOCK FILD 以原子方式加载 64 位。
  • @augustss:不,你不能那样做。 “LOCK 前缀只能添加到以下指令,并且只能添加到目标操作数是内存操作数的那些指令形式:ADD、ADC、AND、BTC、BTR、BTS、CMPXCHG、CMPXCH8B、DEC、INC、NEG 、NOT、OR、SBB、SUB、XOR、XADD 和 XCHG。” (英特尔® 64 和 IA-32 架构软件开发人员手册,第 2A 卷:指令集参考,A-M)。
  • 我相信fild 已经是原子的(没有锁定前缀),至少只要 64 位值是对齐的(因此不会跨缓存行)。它可能不是在 fpu 是外部的古代 cpu 上,但在任何现代的 cpu 上它应该是......
  • @R..:“我相信 fild 已经是原子的(没有锁定前缀),至少只要 64 位值对齐”是的(至少对于英特尔 CPU - 我没有t 检查了其他 verdors 的文档)。但我假设解决方案没有对齐限制。我会更新我的答案以使其更清楚。
  • 另外,您的 chmpxchg 示例不能解决原始问题。您至少可以使用 cmpxchg8b 制作一个可行的解决方案,在这种情况下,您甚至不需要循环。
【解决方案2】:

对两个不同的地址使用比较交换操作怎么样?

类似:CMPXCHG (int*)&n, (((int*)&n)+1)(注意 - 这实际上不起作用)。

编辑:将语法更改为更接近实际的 x86 语法。

编辑 2: 正如 Serge 指出的那样,大多数程序集不支持在汇编指令中使用两个内存地址,因此这种方式不能直接从内存中工作。这意味着这种方法不能用于以原子方式比较 64 位变量的两个 32 位部分。

一些程序集(至少是 PowerPC)能够提供特殊指令(对于 PowerPC、LWARX 和 STWCX),可以使这项工作以多线程安全的方式工作,但这并不是 OP 所要求的,也不会为 x86 工作。

【讨论】:

  • 您不需要使用+1 而不是+sizeof(int)吗?
  • @brooksbp:“不知道为什么我当时没有想到分解地址。”可能是因为这个解决方案是错误的?我不知道CMPXCHG (int*)&n, (((int*)&n)+1), res 是什么意思。有 x86 指令 CMPXCHG 但它只有 2 个操作数(以及 EAX 中的隐式操作数 - 要比较的数字),并且仅此指令无法实现请求的操作(显然不止一条指令需要锁定)。
  • @Eli Iser:“我发布的 CMPXCHG 语法只是示例” 什么例子?受思想启发的无效x86汇编代码在大多数情况下(包括x86情况)不能用来解决问题?
  • 请注意,在大多数汇编程序(包括 x86)中,没有指令可以在一条指令中使用两个不同的内存地址。因此,“在两个不同地址上使用比较交换操作”的整个想法是行不通的,因为在大多数架构上都无法以原子方式实现。
  • @brooksbp:正如 Serge 所说,这个答案是完全错误的,不应该是公认的答案。您可能还想重新考虑您的实现,因为它可能不像您想象的那样以原子方式工作。
【解决方案3】:

使用联合怎么样?像这样:

typedef union {
    uint32_t small[2];
    uint64_t full;
} bigint_t;

那你就去做吧:

uint64_t n;      // two 32-bit integers

bigint_t mybigint;
mybigint.full = n;
return mybigint.small[0] == mybigint.small[1];

不知道这样是不是最快,但是如果你不把uint64_t复制到union中直接使用union的话,应该还是挺快的,因为比较不需要做任何事情。

【讨论】:

  • 如果你从一个联合体的成员而不是最后一个用于写入的联合体中读取,则行为未定义。
  • @dreamlax - 你确定吗?我很高兴看到对此的参考,因为我认为这确实有效(可能在特定的编译器中,但在严格的 C 标准中不行?)。
  • @dreamlax 至少对于 gcc 来说,它确实可以工作,据我所知,unix 套接字库使用这种机制来表示 IP 地址。可以提供参考吗?
  • 具体看Andrey's answer
【解决方案4】:

这整个操作(内存中两个值的原子比较)是没有意义的,除非您还可以确保写入它们始终是原子的。它还受制于固有的竞争条件;当您确定它们相等时,它们可能已经改变,反之亦然。无论您试图解决什么问题,几乎肯定都需要锁,而不是原子操作。

【讨论】:

    【解决方案5】:

    使用内联汇编将 64 位整数加载到 MMX 或 SSE 寄存器(64 位读取是原子的),然后比较两半。

    【讨论】:

    • "(64 位读取是原子的)" 是什么让你这么认为?不保证您是否有 SMP 系统(除非您使用 FPU/MMX/SSE 指令不允许的 LOCK 指令前缀)。
    • 是的,他们是。 Intel 的手册第 3A 卷,第 8.1.1 节:保证原子操作:“P6 系列处理器(以及此后的更新处理器)保证以下附加内存操作将始终以原子方式执行:未对齐的 16 位、32 位和 64 位访问适合缓存行的缓存内存。”
    • @zvrba:在您引用之后,英特尔手册中的下一行:“英特尔酷睿 2 双核不保证对跨缓存行和页面边界拆分的可缓存内存的访问是原子的, Intel® Atom™、Intel Core Duo、Pentium M、Pentium 4、Intel Xeon、P6 家族、Pentium 和 Intel486 处理器。”什么应该确保该值在页面边界内和缓存行内?
    • @zvrba:“正确对齐”。正确的。因此,这意味着您依赖于对齐方式(即依赖于实现,即特定于编译器),或者您必须自己确保正确对齐。对于 P6 或更新的处理器也是如此(因此对于旧处理器而言并非如此)。对于某些非英特尔 x86 CPU 而言,可能并非如此。 IMO 条件太多。
    • @zvrba:“并且一定要找到一个无需任何特殊设置(对齐等)且不特定于 CPU 的解决方案。”好吧,显然通用解决方案(适用于任何 CPU)正在使用锁。显然不存在非 CPU 特定的无锁解决方案(因此我无法提供这样的解决方案 - 没有人可以)。
    猜你喜欢
    • 2013-06-18
    • 2015-07-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多