__asm__ __volatile__ 在 C 语言中的作用是什么?

68

我查看了来自http://www.mcs.anl.gov/~kazutomo/rdtsc.html的一些C代码。
他们使用了诸如__inline____asm__等内容,例如以下示例:

代码1:

static __inline__ tick gettick (void) {
    unsigned a, d;
    __asm__ __volatile__("rdtsc": "=a" (a), "=d" (d) );
    return (((tick)a) | (((tick)d) << 32));
}

代码2:

volatile int  __attribute__((noinline)) foo2 (int a0, int a1) {
    __asm__ __volatile__ ("");
}

我在想,code1和code2具体是做什么的?

(编辑注:对于这个特定的RDTSC用例,建议使用内部函数:如何从C++中获取x86_64的CPU周期计数?另请参见https://gcc.gnu.org/wiki/DontUseInlineAsm)


1
https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html(否则,请忽略所有`__`,`__inline__`只是普通的`inline`)。 - Marc Glisse
3个回答

94
__volatile__修饰符作用于__asm__块,可以强制编译器按照原样执行代码。如果不使用它,优化器可能会认为该代码可以被彻底删除或从循环中提取并缓存。

对于像rdtsc指令这样的情况,这非常有用:

__asm__ __volatile__("rdtsc": "=a" (a), "=d" (d) )

这不需要任何依赖,所以编译器可能会假设该值可以被缓存。使用Volatile可以强制使其读取新的时间戳。
当像这样单独使用时:
__asm__ __volatile__ ("")

实际上它并不会执行任何操作。但你可以将其扩展,以获得一个编译时内存屏障,这将禁止重排序任何内存访问指令:

__asm__ __volatile__ ("":::"memory")

rdtsc指令是易变的好例子。rdtsc通常用于计算某些指令执行所需的时间。想象一下这样的代码,您希望计时r1r2的执行:

__asm__ ("rdtsc": "=a" (a0), "=d" (d0) )
r1 = x1 + y1;
__asm__ ("rdtsc": "=a" (a1), "=d" (d1) )
r2 = x2 + y2;
__asm__ ("rdtsc": "=a" (a2), "=d" (d2) )

在这里,编译器实际上允许缓存时间戳,有效的输出可能显示每行代码执行所需的时钟数为0。显然,这并不是你想要的结果,因此你引入了__volatile__关键字以防止缓存:

__asm__ __volatile__("rdtsc": "=a" (a0), "=d" (d0))
r1 = x1 + y1;
__asm__ __volatile__("rdtsc": "=a" (a1), "=d" (d1))
r2 = x2 + y2;
__asm__ __volatile__("rdtsc": "=a" (a2), "=d" (d2))

现在每次都会得到一个新的时间戳,但它仍然存在一个问题,编译器和CPU都有权重新排列所有这些语句。结果可能是执行asm块之后r1和r2已经被计算出来。为了解决这个问题,您需要添加一些强制序列化的屏障:

__asm__ __volatile__("mfence;rdtsc": "=a" (a0), "=d" (d0) :: "memory")
r1 = x1 + y1;
__asm__ __volatile__("mfence;rdtsc": "=a" (a1), "=d" (d1) :: "memory")
r2 = x2 + y2;
__asm__ __volatile__("mfence;rdtsc": "=a" (a2), "=d" (d2) :: "memory")

注意这里的mfence指令,它强制执行CPU端的屏障,以及volatile块中的"memory"说明符,它强制执行编译时屏障。在现代CPU上,您可以将mfence:rdtsc替换为更高效的rdtscp

那么,使用空块,它就像是一种指令屏障? - Bryan Chen
2
请注意,编译器只能控制它生成的静态代码顺序,并避免在编译时将内容移动到此屏障之后,但它无法控制 CPU 内部的实际执行顺序,这可能仍然会改变它(CPU 不知道 volatile 属性或空代码块)。使用 rdtsc 可能会导致一些不准确性。 - Leeor
@Leeor 确实,因此是“编译时屏障”。 - Cory Nelson
我在Haswell处理器(各代i3,i5,i7)上遇到了关于fences和rdt的问题。不仅围栏没有起到好的作用,反而增加了不准确性。如果我没记错,单独使用rdtscp与平均值相差+/-4个滴答,而(各种)围栏只会增加这个误差。大多数在线资源似乎停留在Pentium 4时代,显然有些东西已经改变了。 - PTwr
1
大多数情况下,问题中的代码都很糟糕。它应该使用__rdtsc内置函数。在asm volatile("")中使用volatile是无用的。而且你对volatile的解释不好,使用asm("rdtsc":...,编译器甚至可以重新排序asm块(或者如果a0和d0未使用,则删除它们),而使用volatile仍然必须按照这个顺序保留它们,但它仍然可以移动添加和存储操作。 - Marc Glisse
1
注意:尽管不是特别相关,但应避免使用rdtsc进行性能监测,因为许多因素可能会改变结果。 - edmz

20

asm 用于将本地汇编代码包含到 C 源代码中。例如:

int a = 2;
asm("mov a, 3");
printf("%i", a); // will print 3

编译器有不同的变体,__asm__ 应该是同义词,可能具有一些特定于编译器的差异。

volatile 表示变量可以从外部修改(即非 C 程序修改)。例如,在编写微控制器程序时,内存地址 0x0000x1234 映射到某个设备特定接口(例如,在为 GameBoy 编程时,通过此方式访问按钮/屏幕等)。

volatile std::uint8_t* const button1 = 0x00001111;

这禁用了依赖于*button1除非被代码更改否则不会更改的编译器优化。

它还用于多线程编程(今天不再需要?),其中变量可能会被另一个线程修改。

inline是对编译器的提示,以“内联”函数调用。

inline int f(int a) {
    return a + 1
}

int a;
int b = f(a);

这不应该被编译为对函数调用f的调用,而应该编译为int b = a + 1。就像f是宏一样。编译器通常根据函数使用/内容自动执行此优化。在这个例子中,__inline__可能具有更具体的含义。

类似地,__attribute__((noinline))(GCC特定语法)可以防止函数被内联。


1
谢谢!noinline有什么好处? - user3692521
1
我猜它只是确保调用 foo2 被翻译为一个带有两个整数参数并返回一个整数的空函数的函数调用,而不是被优化掉。那个函数可以在生成的汇编代码中实现。 - tmlen
如果函数为空,它是如何知道返回一个整数(哪个整数)的? - user3692521
它被定义为返回int的函数。这意味着生成的汇编代码将是这样的,即调用者希望某个寄存器上有一个int返回值(取决于调用约定)。 在汇编代码中,函数体可以编码为执行该操作。 - tmlen
2
我认为在asm块上使用volatile与在变量上使用volatile是有很大区别的。尽管共同点仍然存在,即它限制了优化器的自由。 - MvG
2
它也用于多线程编程(今天不再需要?),其中变量可能会被另一个线程修改。虽然确实使用了它,但它仅保证访问的指令顺序而不是对内存的原子性访问(尽管大多数架构上的对齐访问是原子的)或内存栅栏(除了MSVC扩展 - 在ARM上禁用)。为了正确使用,必须使用C(++)11原子或编译器内置函数。 - Maciej Piechotka

3
__asm__属性指定函数或变量在汇编代码中使用的名称。 __volatile__限定符通常用于实时计算的嵌入式系统中,它解决了编译器在检测status registerERRORREADY位时导致优化问题的问题。 __volatile__被引入作为告诉编译器对象正在快速更改并强制每个对象引用都是真正引用的方法。

不完全是这样,它适用于任何具有副作用的东西,您无法使用操作数约束来描述它,例如当您希望即使所有输出操作数都未使用时仍然发生时。 - Peter Cordes
这不就是强制每个对象引用都是真实引用的意思吗?我有点困惑于“不是很”的原因是,该描述几乎是从2014年10月的参考文档中直接摘录的。我会看看能否找到引用。 - David C. Rankin
我主要不同意说它只与RTC相关。这不是关于“快速”更改,而是任何可能具有副作用的东西。那个“每个引用都是真实引用”的听起来像是volatile类型限定符(例如volatile int)的描述,而不是GNU C asm volatile。在内联汇编中,没有“对象”。 - Peter Cordes
我猜更好的措辞应该是说volatile禁用了优化,这样就不会丢弃asm语句,如果它们确定没有必要输出变量,无论如何:) - David C. Rankin
是的,加上一些防止重新排序的措施,如果您使用“memory” clobber 来使其成为编译器屏障,则可以获得更多保证。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接