内联asm编译器屏障(内存阻塞器)是算作外部函数,还是算作静态函数调用

Does the inline asm compiler barrier (memory clobber) count as an external function, or as static function call?

本文关键字:外部 函数 函数调用 静态 编译器 asm 内存 内联      更新时间:2023-10-16

基本事实介绍/确认

众所周知,使用GCC风格的C和C++编译器,可以使用带有"内存"阻塞器的内联汇编:

asm("":::"memory");

以防止(大多数)代码的重新排序超过它,充当(线程本地)"内存屏障"(例如,为了与异步信号交互)。

注意:这些"编译器障碍"无法实现线程间同步

它相当于对非内联函数的调用,可能读取当前范围之外可以读取的所有对象,并更改所有可以更改的对象(非常量对象):

int i;
void f() {
int j = i;
asm("":::"memory"); // can change i
j += i; // not j *= 2
// ... (assume j isn't unused)
}

从本质上讲,它与调用单独编译的NOP函数相同,只是非内联NOP函数调用稍后(1)内联,因此没有任何内容可以从中幸存下来

(1) 比如说,在编译器中间通过之后,在分析之后

因此,这里j不能更改,因为它是本地的,并且仍然是旧i值的副本,但i可能已经更改,因此编译与几乎相同

volatile int vi;
int f2() {
int j = vi;
; // can "change" vi
j += vi; // not j *= 2
return j;
}

vi的两个读取都是必需的(出于不同的原因),所以编译器不会将其更改为2*vi

到目前为止,我的理解正确吗?(我想是的。否则这个问题就没有意义了。)

真正的问题:外部还是静态

以上只是序言。我遇到的问题是静态变量,对静态函数(或C++等价的匿名命名空间)的可能调用:

如果asm指令的输入参数中没有显式命名,内存阻塞器是否可以访问通过非静态函数无法访问的静态数据,并从其他模块调用无法调用的静态函数,因为这些函数在链接阶段都不可见,

static int si;
int f3() {
int j = si;
asm("":::"memory"); // can access si?
j += si; // optimized to j = si*2; ?
return j;
}

[注意:static的使用有点模糊。建议TU的边界很重要,静态变量是TU私有的,但我没有描述它是如何被操纵的。让我们假设它在那个TU中真的被操纵了,或者编译器可能会假设它实际上是一个常量。]

换言之,这是不是相当于对的调用

  • 一个外部NOP函数,它不能直接命名si,也不能以任何间接方式访问它,因为TU中没有任何函数传达si的地址,或者使si可以间接修改
  • 可以访问si的本地定义的NOP函数

奖励问题:全局优化

如果答案是在这种情况下静态变量不像外部变量那样处理,那么一次编译程序会有什么影响?更具体地说:

在整个程序的全局编译过程中,通过对变量值的全局分析和推断,是否知道这样一个事实:例如,全局变量从未被修改(或从未被分配负值…),除非可能在asm"clobber"中,优化器的输入?

换句话说,如果非静态i只在一个TU中命名,那么即使有asm语句,它是否也可以像静态int一样进行优化?在这种情况下,全局变量是否应该被明确列为clobber?

它相当于对非内联函数的调用,可能读取当前范围之外可以读取的所有对象,并更改所有可以更改的对象(非常量对象):

否。

编译器可以决定在同一编译单元中内联任何函数(然后,如果该函数不是static,还可以为其他编译单元中的调用方提供一个单独的"未内联"副本,以便链接器可以找到一个);并且通过链接时间代码优化/链接时间代码生成,链接器可以决定内联不同编译单元中的任何函数。目前唯一不可能内联任何函数的情况是当它在共享库中时;但目前存在这种限制,因为操作系统目前无法进行"加载时间优化"。

换句话说;任何函数出现任何类型的障碍都是优化器弱点的意外副作用,不能保证;因此不能/不应该依赖

真正的问题:内联组装

有5种可能性:

a) 编译器理解所有的程序集,并且能够检查内联程序集并确定哪些程序集被破坏了;没有打击名单(也不需要)。在这种情况下(取决于编译器/优化器的先进程度),编译器可能能够确定诸如"该内存区域可能被破坏,但该内存区域不会被破坏"之类的事情,并避免从未被破坏的内存区域重新加载数据的成本。

b) 编译器不理解任何程序集,也没有clobber列表,因此编译器必须假设所有程序集都将被clobber;这意味着编译器必须在执行内联程序集之前生成代码,将所有内容(例如,寄存器中当前使用的值等)保存到内存中,然后重新加载所有内容,这将导致非常糟糕的性能。

c) 编译器不理解任何程序集,并希望程序员提供一个clobber列表,以避免(一些)不得不假设所有程序都会被clobber的性能灾难。

d) 编译器可以理解某些程序集,但不能理解所有程序集,并且没有clobber列表。如果它不理解这个程序集,它就会认为一切都可能被破坏了。

e) 编译器可以理解某些程序集,但不能理解所有程序集,并且有一个(可选?)clobber列表。如果它不理解程序集,它将依赖于clobber列表(和/或如果没有clobber名单,则返回到"假设所有东西都被clobbed"),如果它理解程序集则忽略clobber清单。

当然,使用"选项c)"的编译器可以改进为使用"选项e)";使用"选项e)"的编译器可以改进为使用"选项a)"。

换句话说;对于类似"asm("":::"memory");"的东西,任何形式的障碍的出现都是编译器"可改进"的意外副作用;因此不能/不应该依赖

摘要

你提到的任何事情都不是真正的障碍。这一切都只是"意外和不希望的优化失败"。

如果你确实需要一个屏障,那么就使用一个实际的屏障(例如"asm("mfence":::"memory");")。然而(除非你需要线程间同步并且不使用原子),否则你很可能一开始就不需要屏障。