使用 cmpxchg8b for unsigned long 未获得预期输出
Posted
技术标签:
【中文标题】使用 cmpxchg8b for unsigned long 未获得预期输出【英文标题】:Not getting expected output using cmpxchg8b for unsigned long 【发布时间】:2016-06-14 20:01:18 【问题描述】:我正在尝试编写一个简单的比较和交换内联汇编代码。这是我的代码
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
static inline unsigned long
cas(volatile unsigned long* ptr, unsigned long old, unsigned long _new)
unsigned long prev=0;
asm volatile("lock cmpxchg8b %0;"
: "=m"(prev)
: "m"(*ptr),"a"(old),"c"(_new)
);
return prev;
int main()
unsigned long *a;
unsigned long b=5,c;
a=&b;
c=cas(a,b,6);
printf("%lu\n",c);
return 0;
此代码理想情况下应该打印 5,但它打印的是 0。我的代码有什么问题?请帮忙。
【问题讨论】:
***.com/questions/6756985/… 有帮助吗? 您是否阅读过 cmpxchg8b 的文档:Compare EDX:EAX with m64. If equal, set ZF and load ECX:EBX into m64. Else, clear ZF and load m64 into EDX:EAX.
由于您没有将任何特定值加载到 EDX(或 EBX)中,我假设比较总是失败,这意味着 asm 什么都不做,并且@ 987654324@(在未优化的构建中初始化为 0)原样返回。此外,您传递给 cmpxchg8b 的内存地址是 prev
(又名 %0),而不是 ptr,因此永远不会使用 ptr。这可能也一样,因为 *ptr (vs ptr) 可能不是有效的内存地址。
另外,unsigned long
在您的平台上存在多长时间(您说的是 x86)?如果答案不是 8 字节,则需要重新考虑使用 cmpxchg8b。怎么了?恐怕这几乎是一切。
【参考方案1】:
首先让我说“使用内联 asm 是个坏主意”。让我再说一遍“使用内联汇编是个坏主意”。你可以写一个完整的wiki entry 来说明为什么使用内联汇编是一个坏主意。请考虑使用内置函数(如 gcc 的 __sync_bool_compare_and_swap)或类似 的库。
如果您正在编写生产软件,使用内联 asm 的风险几乎肯定大于任何好处。如果您是出于教育目的而写作,请继续阅读。
(要进一步说明为什么不应该使用内联汇编,请等待 Michael 或 Peter 出现并指出这段代码的所有错误。这真的很难,即使对于知道这些东西的人,把它做好。)
这里有一些代码展示了如何使用cmpxchg8b
。这很简单,但应该足以给出一个大致的想法。
#include <stdio.h>
// Simple struct to break up the 8 byte value into 32bit chunks.
typedef union
struct
unsigned int lower;
unsigned int upper;
;
unsigned long long int f;
moo;
unsigned char cas(moo *ptr, moo *oldval, const moo *newval)
unsigned char result;
#ifndef __GCC_ASM_FLAG_OUTPUTS__
asm ("lock cmpxchg8b %[ptr]\n\t"
"setz %[result]"
: [result] "=q" (result), [ptr] "+m" (*ptr),
"+d" (oldval->upper), "+a" (oldval->lower)
: "c" (newval->upper), "b" (newval->lower)
: "cc", "memory");
#else
asm ("lock cmpxchg8b %[ptr]"
: [result] "=@ccz" (result), [ptr] "+m" (*ptr),
"+d" (oldval->upper), "+a" (oldval->lower)
: "c" (newval->upper), "b" (newval->lower)
: "memory");
#endif
return result;
int main()
moo oldval, newval, curval;
unsigned char ret;
// Will not change 'curval' since 'oldval' doesn't match.
curval.f = -1;
oldval.f = 0;
newval.f = 1;
printf("If curval(%u:%u) == oldval(%u:%u) "
"then write newval(%u:%u)\n",
curval.upper, curval.lower,
oldval.upper, oldval.lower,
newval.upper, newval.lower);
ret = cas(&curval, &oldval, &newval);
if (ret)
printf("Replace succeeded: curval(%u:%u)\n",
curval.upper, curval.lower);
else
printf("Replace failed because curval(%u:%u) "
"needed to be (%u:%u) (which cas has placed in oldval).\n",
curval.upper, curval.lower,
oldval.upper, oldval.lower);
printf("\n");
// Now that 'curval' equals 'oldval', newval will get written.
curval.lower = 1234; curval.upper = 4321;
oldval.lower = 1234; oldval.upper = 4321;
newval.f = 1;
printf("If curval(%u:%u) == oldval(%u:%u) "
"then write newval(%u:%u)\n",
curval.upper, curval.lower,
oldval.upper, oldval.lower,
newval.upper, newval.lower);
ret = cas(&curval, &oldval, &newval);
if (ret)
printf("Replace succeeded: curval(%u:%u)\n",
curval.upper, curval.lower);
else
printf("Replace failed because curval(%u:%u) "
"needed to be (%u:%u) (which cas has placed in oldval).\n",
curval.upper, curval.lower,
oldval.upper, oldval.lower);
几点:
如果 cas 失败(因为值不匹配),则函数的返回值为 0,而您需要使用的值在 oldval 中返回。这使得再次尝试变得简单。请注意,如果您正在运行多线程(您必须这样做,否则您不会使用lock cmpxchg8b
),第二次尝试也可能会失败,因为“其他”线程可能会击败您再次写入。
__GCC_ASM_FLAG_OUTPUTS__
定义可用于较新版本的 gcc (6.x+)。它允许您跳过执行setz
并直接使用标志。详情请参阅 gcc docs。
至于它是如何工作的:
当我们调用cmpxchg8b
时,我们将一个指向内存的指针传递给它。它将将该内存位置中的(8 字节)值与 edx:eax 中的 8 字节进行比较。如果它们匹配,则它将 ecx:ebx 中的 8 个字节写入内存位置,并设置 zero
标志。如果不匹配,则在 edx:eax 中返回当前值,并清除 zero
标志。
因此,将其与代码进行比较:
asm ("lock cmpxchg8b %[ptr]"
这里我们将指向这 8 个字节的指针传递给cmpxchg8b
。
"setz %[result]"
在这里,我们将cmpxchg8b
设置的zero
标志的内容存储到(结果)中。
: [result] "=q" (result), [ptr] "+m" (*ptr),
指定 (result) 是一个输出 (=),并且它必须是一个字节寄存器 (q)。此外,内存指针是一个 in+out (+),因为我们将同时读取它和写入它。
"+d" (oldval->upper), "+a"(oldval->lower)
+ 符号再次表示这些值是 in+out。这是必要的,因为如果比较失败,edx:eax 将被 ptr 中的当前值覆盖。
: "c" (newval->upper), "b"(newval->lower)
这些值仅供输入。 cmpxchg8b
不会改变它们的值,所以我们将它们放在第二个冒号之后。
: "cc", "memory");
由于我们正在更改标志,我们需要通过“cc”通知编译器。 “内存”约束可能不是必需的,具体取决于 cas 的用途。线程 1 可能正在通知线程 2 某事已准备好进行处理。在这种情况下,您要绝对确保 gcc 在计划稍后写入内存的寄存器中没有任何值。它绝对必须在执行cmpxchg8b
之前将它们全部刷新到内存中。
gcc docs 详细描述了扩展 asm 语句的工作原理。如果此解释的部分内容仍不清楚,阅读可能会有所帮助。
顺便说一句,如果我忘了提及,编写内联 asm 是个坏主意...
【讨论】:
嘿嘿,我想我先不管这个了;看起来你已经涵盖了。尽管有我的头像,但有时我确实需要克制自己不要尝试纠正互联网上的所有错误。虽然,你确定你需要一个"memory"
clobber 吗?如果您使用"+m"
操作数作为内存中可能会或可能不会被修改的值,编译器将不得不重新加载它。可能使用volatile moo*
是一个不错的选择。
另外,您可以通过使用"+A"
约束来避免union
,这意味着将edx:eax
组合为64 位值。但它更清晰,可移植到 x86-64 (where a 64bit value will just go into either rax or rdx)。
@MichaelPetch:嘿,我链接到我的 wiki 和我的文档。不想吹我的号角太多。
@PeterCordes:就是忍不住,嗯?+A
的忠实粉丝。出于教学目的(如当前上下文),我只是看不到它。至于volatile
,嗯。鉴于“+m”,我不知道它还能给你带来什么。从自我记录的角度来看,也许。编译器会因为某种原因需要这个吗?volatile
只是一个自我记录的东西,并防止任何过于聪明的编译器在 asm 之外加载/存储到它。此外,如果您需要在 cmpxchg8b 之前旋转它以等待某些条件,您应该这样做with normal loads (and a pause
instruction)。使用 SSE movq
或 fild
/fistp
在 32 位模式下可以进行 8 字节原子加载/存储,而 gcc 将实际使用它。对于原子 RMW,您只需要 cmpxchg8b。【参考方案2】:
很抱歉没有直接回答您的问题,但我的问题是:为什么不使用C11's <stdatomic.h>
or C++11's <atomic>
?与编写自己的函数相比,它的出错率要低得多,并且具有您不针对特定硬件架构或编译器的优势。
在您的情况下,您应该使用atomic_compare_exchange_weak()
或atomic_compare_exchange_strong()
。
【讨论】:
你提到的这两个函数,它们返回布尔值。对于我的实现,我需要旧值指向 *ptr。 这些函数也可用于此目的。第二个参数指向的变量将被覆盖以包含旧值。 @Ritesh:请编辑您的问题并添加为什么您不能使用该语言的标准功能并且必须恢复为汇编代码。一般来说,编写自己的存根是个坏主意,因为它们可能会干扰编译器优化并导致代码变差。更不用说它不便携。可以找到如何使用标准原子模拟 CAS 的应用说明。但实际上,该标准提供了更多有用的功能,这使得 CAS 仿真可能是多余的。以上是关于使用 cmpxchg8b for unsigned long 未获得预期输出的主要内容,如果未能解决你的问题,请参考以下文章
为啥 C 和 C++ for 循环使用 int 而不是 unsigned int?
为什么int和而不是unsigned int用于C和C ++ for循环?
在“for”循环中运行整个“unsigned char”范围
C++ builder中如何把unsigned char类型转换成16进制的输出
错误:the apk for your currently selected variant(app-release-unsigned.apk)is not signed.Please specity