What does __asm__ __volatile__ do in C?
我查看了一些 C 代码
http://www.mcs.anl.gov/~kazutomo/rdtsc.html
他们使用诸如 __inline__、__asm__ 之类的东西,如下所示:
代码1:
1 2 3 4 5
| static __inline__ tick gettick (void) {
unsigned a, d;
__asm__ __volatile__("rdtsc":"=a" (a),"=d" (d) );
return (((tick)a) | (((tick)d) << 32));
} |
代码2:
1 2 3
| volatile int __attribute__((noinline)) foo2 (int a0, int a1) {
__asm__ __volatile__ ("");
} |
我想知道code1和code2是做什么的?
(编者注:对于这个特定的 RDTSC 用例,首选内部函数:How to get the CPU cycle count in x86_64 from C ?另见 https://gcc.gnu.org/wiki/DontUseInlineAsm)
- gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html (否则,忽略 __ 无处不在,__inline__ 只是普通的 inline。
__asm__ 块上的 __volatile__ 修饰符强制编译器的优化器按原样执行代码。没有它,优化器可能会认为它可以直接删除,或者从循环中取出并缓存。
这对于 rdtsc 指令很有用,如下所示:
1
| __asm__ __volatile__("rdtsc":"=a" (a),"=d" (d) ) |
这没有依赖关系,因此编译器可能会假设该值可以被缓存。 Volatile 用于强制它读取一个新的时间戳。
单独使用时,像这样:
1
| __asm__ __volatile__ ("") |
它实际上不会执行任何操作。但是,您可以扩展它以获得不允许重新排序任何内存访问指令的编译时内存屏障:
1
| __asm__ __volatile__ ("":::"memory") |
rdtsc 指令是 volatile 的一个很好的例子。 rdtsc 通常在您需要计算某些指令的执行时间时使用。想象一下这样的代码,您想在其中计时 r1 和 r2 的执行:
1 2 3 4 5
| __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__ 来防止缓存:
1 2 3 4 5
| __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 重新排序所有这些语句。在 r1 和 r2 已经被计算之后,它可能最终会执行 asm 块。要解决此问题,您需要添加一些强制序列化的障碍:
1 2 3 4 5
| __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 块中的"内存"说明符强制编译时屏障。在现代 CPU 上,您可以将 mfence:rdtsc 替换为 rdtscp 以获得更高效的效果。
-
那么对于空块,这是一种指令障碍吗?
-
请注意,编译器只能控制它生成的静态代码顺序,并避免在编译时将东西移过这个障碍,但它无法控制 CPU 内的实际执行顺序,这仍然可能会改变它(CPU 不知道关于 volatile 属性或空代码块)。使用 rdtsc 这可能会导致一些不准确。
-
@Leeor确实,因此是"编译时障碍"。
-
我在 Haswells(i3、i5、i7 各代)上遇到了栅栏和 rdt 问题。如果我没记错的话,不仅栅栏没有好处,而且还增加了不准确性,如果我没记错的话,rdtscp 本身有 /- 4 个刻度,而(各种)栅栏只会增加它。大多数关于它的在线资源似乎都停止在 Pentium 4 时代,并且显然有些东西发生了变化。
-
大多数情况下,问题中的代码很糟糕。它应该使用 __rdtsc 内在函数。 volatile 在 asm volatile("") 中是无用的。而且您对 volatile 的解释不好,使用 asm("rdtsc":... 编译器甚至可以重新排序 asm 块(或者如果未使用 a0 和 d0 则将其删除),而使用 volatile 它必须保持它们的顺序,但它仍然可以移动添加和存储。
-
注意:虽然不是特别相关,但应避免使用 rdtsc 进行性能监控,因为许多因素会改变结果。
asm 用于将本机汇编代码包含到 C 源代码中。例如
1 2 3
| int a = 2;
asm ("mov a, 3");
printf("%i", a ); // will print 3 |
编译器有它的不同变体。 __asm__ 应该是同义词,可能有一些特定于编译器的差异。
volatile 表示可以从外部修改变量(也就是不能通过 C 程序)。例如,在对内存地址 0x0000x1234 映射到某些设备特定接口的微控制器进行编程时(即,在为 GameBoy 编码时,按钮/屏幕/等以这种方式访问??。)
1
| volatile std::uint8_t* const button1 = 0x00001111; |
这禁用了依赖于 *button1 的编译器优化,除非被代码更改,否则不会更改。
它也用于多线程编程(今天不再需要?)其中一个变量可能被另一个线程修改。
inline 提示编译器"内联"调用函数。
1 2 3 4 5 6
| inline int f(int a) {
return a + 1
}
int a;
int b = f(a); |
这不应该编译成对f的函数调用,而是编译成int b = a + 1。好像 f 哪里有宏。编译器大多会根据函数使用/内容自动执行此优化。此示例中的 __inline__ 可能具有更具体的含义。
类似地,__attribute__((noinline))(GCC 特定语法)阻止函数被内联。
- 谢谢!! noinline 有什么好处?
-
我猜它只是确保调用 foo2 被转换为对具有两个整数参数的空函数的函数调用,并在程序集中返回一个整数。而不是被优化掉。然后可以在生成的汇编代码中实现该功能。
-
如果函数为空,它如何知道返回一个整数(哪个整数?)?
-
它被定义为返回 int 的函数。这意味着生成的汇编代码将使得调用者期望 int 返回值在某个寄存器上(取决于调用约定)。然后可以在汇编代码中对函数体进行编码以执行此操作。
-
我会说 asm 块上的 volatile 与变量上的 volatile 有很大不同。尽管仍然存在共同点,即它限制了优化器的自由。
-
"它也用于多线程编程(今天不再需要?)其中一个变量可能被另一个线程修改。" - 虽然它确实被使用它是不正确的,因为它只保证访问的指令顺序而不是访问内存的原子性(尽管对齐访问在大多数架构上是原子的)或内存栅栏(MSVC 扩展除外 - 在 ARM 上禁用)。为了正确使用,必须使用 C()11 原子或编译器内在函数。
__asm__ 属性指定要在汇编代码中用于函数或变量的名称。
__volatile__ 限定符,通常用于嵌入式系统的实时计算,解决了 status register 的编译器测试对 ERROR 或 READY 位导致优化期间出现问题的问题。 __volatile__ 被引入是为了告诉编译器对象会发生快速变化并强制对象的每个引用都是真正的引用。
-
不是真的,它适用于任何你不能/不能用操作数约束描述的副作用,例如即使所有输出操作数都未使用,当您希望它仍然发生时。
-
这不是强迫对象的每个引用都是真正的引用吗?我对"不是真的"有点困惑的原因是描述几乎是从 2014 年 10 月存在的参考文档中逐字提取的。我会看看我是否可以挖掘引用。
-
我主要不同意说它只与 RTC 有关。这不是关于"快速"改变,而是任何可能产生副作用的东西。"每个引用都是真正的引用"听起来像是对 volatile 类型限定符(例如 volatile int)的描述,而不是 GNU C asm volatile。使用内联汇编没有"对象"。
-
明白了,我想最好说 volatile 禁用优化,如果它们确定不需要输出变量,则丢弃 asm 语句,无论如何 :)
-
是的,加上一些对重新排序的预防,如果您使用 "memory" clobber 使其成为编译器屏障,则更多。