关于 gcc:__asm__ __volatile__ 在 C 中做了什么?

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)


__asm__ 块上的 __volatile__ 修饰符强制编译器的优化器按原样执行代码。没有它,优化器可能会认为它可以直接删除,或者从循环中取出并缓存。

这对于 rdtsc 指令很有用,如下所示:

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

这没有依赖关系,因此编译器可能会假设该值可以被缓存。 Volatile 用于强制它读取一个新的时间戳。

单独使用时,像这样:

1
__asm__ __volatile__ ("")

它实际上不会执行任何操作。但是,您可以扩展它以获得不允许重新排序任何内存访问指令的编译时内存屏障:

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

rdtsc 指令是 volatile 的一个很好的例子。 rdtsc 通常在您需要计算某些指令的执行时间时使用。想象一下这样的代码,您想在其中计时 r1r2 的执行:

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 以获得更高效的效果。


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 特定语法)阻止函数被内联。


__asm__ 属性指定要在汇编代码中用于函数或变量的名称。

__volatile__ 限定符,通常用于嵌入式系统的实时计算,解决了 status register 的编译器测试对 ERRORREADY 位导致优化期间出现问题的问题。 __volatile__ 被引入是为了告诉编译器对象会发生快速变化并强制对象的每个引用都是真正的引用。