【问题标题】:Does the inline asm compiler barrier (memory clobber) count as an external function, or as static function call?内联 asm 编译器屏障(内存破坏器)算作外部函数还是静态函数调用?
【发布时间】:2019-12-08 03:43:26
【问题描述】:

基本事实的介绍/确认

众所周知,使用 GCC 风格的 C 和 C++ 编译器,您可以将内联汇编与“内存”破坏器一起使用:

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

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

注意:这些“编译器屏障”不会实现线程间同步。

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

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;
}

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

换句话说,“clobber”是否相当于调用:

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

?

额外问题:全局优化

如果答案是在这种情况下静态变量不被视为外部变量,那么在一次编译程序时会产生什么影响?更具体地说:

在整个程序的全局编译过程中,通过对变量值的全局分析和推断,知道例如全局变量永远不会被修改(或永远不会分配负值......),除非可能在asm“clobber”,优化器的输入?

也就是说,如果非静态i仅在一个TU中命名,那么即使有asm语句,是否可以将其优化为静态int?在这种情况下,是否应该将全局变量明确列为 clobbers?

【问题讨论】:

  • 你的理解有误。内存破坏的影响仅限于非局部变量。
  • @prl 在访问自动对象方面受到限制。 memory effect on automatic GCC 9.2 表明 "memory" 不会破坏局部变量 j。 (与 clang 相同。)
  • @prl:我对asm("":::"memory")效果的体验是,它就像一个非内联函数调用;只有“转义”的局部变量被强制在内存中同步,否则 asm 语句将没有合理的方式来读取它们(例如,通过从假设的全局变量中读取它们的地址)。如果逃逸分析证明一个变量是真正的局部变量;它可以忽略"memory" clobber。要强制它进入内存,请在 asm 语句中使用 "m"(var) 输入操作数。
  • @curiousguy:未经测试,因此尚未发布答案:IIRC GCC 确实尊重static 变量的可能修改,至少在全球范围内。如果它们不是 asm 语句的输入,则可能不在函数范围内。但如果可能的话,它会优化它们,所以如果你想从内联汇编中访问static,请在它们上使用__attribute__((used))。 (或者从 asm 调用一个函数;未使用的或总是内联的函数也会被优化掉。)
  • 是的,我明白了。我认为如果不删除static 对象,它将尊重asm 内存屏障。应该很简单地编写一些测试用例,看看它是否适用于没有volatile 的简单用例。显然volatile 根本不需要编译器屏障。

标签: c++ c gcc inline-assembly memory-barriers


【解决方案1】:

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

没有。

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

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

真正的问题:内联汇编

有5种可能性:

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

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

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

d) 编译器理解一些程序集,但不是所有程序集,并且没有破坏列表。如果它不理解程序集,它会假定所有内容都可能已被破坏。

e) 编译器理解一些程序集,但不是所有程序集,并且确实有一个(可选的?)clobber 列表。如果它不理解程序集,它依赖于 clobber 列表(和/或如果没有 clobber 列表,则回退到“假设一切都被破坏”),如果它确实理解程序集,它会忽略 clobber 列表。

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

换句话说;对于“asm("":::"memory");”之类的任何类型的障碍的任何外观都是编译器“可改进”的意外副作用;因此不能/不应依赖。

总结

你提到的这些东西实际上都不是任何形式的障碍。这一切都只是“无意和不希望的优化失败”。

如果您确实需要屏障,则使用实际的屏障(例如“asm("mfence":::"memory");”。但是(除非您需要线程间同步并且不使用原子)您极有可能不需要屏障第一名。

【讨论】:

  • 我很确定 asm("":::"memory") 实际上已被 GCC 记录为作为编译器屏障。让未来的 GCC 开始解析 asm 模板中的指令并对其进行优化会破坏该合同。
  • 另外,我认为您错过了“非内联函数调用”这一点。我们的意思是一个真正不能内联的函数调用,因为在代码生成期间定义对优化器不可见(单独的 TU,或者即使使用 LTO 也不存在,或者必须尊重符号插入的可能性对于非“隐藏”符号。)只是它是否具有 C++ inline 关键字!
  • asm("":::"memory") 是 GNU C(gcc/clang/ICC / 任何其他支持扩展的编译器)中 std::atomic_signal_fence() 的滚动你自己的实现,在 x86 上是滚动你的 -自己实现 StoreLoad 之外的所有障碍。我通常也不建议使用它,但这是一个有用的问题,它究竟会阻碍什么,没有阻碍什么。 (例如,它是否强制非转义的本地人到内存:它现在没有,即使在实践中也是如此。)此外,Linux 内核在实践中确实使用它,因此如果查看内核代码,了解它的作用是很有用的.还有微台
  • 好的,那么就不会有任何不可内联的函数了。这并没有消除概念,现实世界的编译器必须能够处理这种情况。因此,即使没有针对整个程序优化的行为实例,您也可以谈论他们在这种情况下做什么。
  • GNU C 是一种定义良好的 C 方言(由少数编译器支持),它对 C 语言的扩展行为由 GCC 手册在纸上定义。该政策已记录在案 = 支持。文档详细说明了 asm volatile"memory" clobbers 的工作原理,因此询问这意味着什么很有用。询问asm("":::"memory") 在 ISO C 中的含义显然没有用; ISO C 没有定义任何这样的东西。
猜你喜欢
  • 2013-07-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-11-21
  • 2016-12-09
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多