【问题标题】:Busy loop and the barrier繁忙的循环和障碍
【发布时间】:2017-01-18 10:58:35
【问题描述】:
void loop(int loops) 
    {
      while (loops-- > 0)
        asm volatile ("" : : : "memory")
    }
  1. 我知道asm volatile ("" : : : "memory") 会阻止编译器重新排序指令。但是,在这里,我看不到可以重新排序的内容以及为什么它在并发方面可能存在问题。 (我考虑到可能的中断)。那么,为什么会有障碍呢?

  2. 其次,连接问题。 假设我们有 10000000 行代码(见下文)。正如我们所知,CPU 可以重新排序 StoreLoad。

    mov [eax], $2; 不; 不; ...; 不; mov ebx, [ecx];

CPU 能够预测应用 StoreLoad 的机会有多深?

同样的问题可以应用于编译器,但它涉及各种重新排序(不仅是 StoreLoad,而且不仅是内存操作)

【问题讨论】:

  • 1) 也许作为副作用,它会阻止优化器丢弃“循环”的其他“不必要”修改。
  • 如果你把它拿出来用-O3编译函数会发生什么?代码与设置屏障有何不同?
  • 我明白了。因此,您是否建议存在障碍,否则编译器会删除它? 2. 问题呢?也许,我没有很好地解释我的意思?
  • 我对 #2 没有任何想法。但是,是的,没有内存破坏器,它可以完全消除“浪费时间”的循环。这是延迟的一种不好的方法。如果你想替换它,也许像 __builtin_ia32_pause()?
  • 我没有收到您的回复通知,因为您没有使用@peter。澄清一下,您是说这是其他人编写的现有代码?从你正在看的那个教学操作系统?这并不明显,尤其是缺少分号,这意味着它甚至无法编译。

标签: x86 operating-system barrier


【解决方案1】:

TL:DR:这里的问题是您只将其视为std::atomic_thread_fence(std::memory_order_seq_cst),但这并不是 GNU C volatile asm 语句所做的唯一事情。


是的,显然障碍是制造令人讨厌的忙等待延迟循环。请记住,volatile asm 语句不能与任何其他 C 语句重新排序,而不仅仅是内存操作。

Godbolt

void loop_nomemclobber(int loops) {
  do {     // loop rearranged for simpler asm
    asm volatile ("" : : : /* "memory" */ );
  } while (--loops > 0);
}

loop_nomemclobber:
.L3:
    sub     edi, 1
    test    edi, edi
    jg      .L3
    ret

即使没有强制所有可访问的内存都是最新的并被视为已破坏,我们仍然会得到一个循环。所以asm volatile 语句这样做的原因与"memory" clobber 无关。

int loops 是具有自动存储功能的本地。编译器可以证明没有任何东西(包括 asm 语句)可以确定它在内存中的位置,因此它根本不必在内存中。


CPU 能够预测应用 StoreLoad 机会的深度有多大?

CPU 不会无缘无故地寻找重新排序内存的机会!重新排序自然发生(除非使用 MFENCE 阻止),因为 CPU 需要缓冲存储,直到确定它们不是推测性的,并且缓存未命中存储。所以它将存储放入 Store Buffer 中,它们最终会提交到缓存中。

CPU 中没有一个小恶魔在说“啊哈,这又是一个让 Gilgamesz 为难的机会,也许这次我真的会用这个重新排序来欺骗他!”


这里有一个真正的问题,那就是 在一个特定的微架构没有足够的空间之前,两条指令需要相距多远(在时间上,或 insn 的数量,或干预的加载/存储的数量)该存储的无序资源一直被缓冲,直到加载之后。

我不知道,但是由于不允许 StoreStore 重新排序,因此在数百万条其他指令运行时,对高度竞争的缓存行的缓存未命中存储不能坐在那里等待获得对缓存行的访问权。除非这些说明都不是商店。

我不知道答案,但我认为理论上在 Intel Haswell 上存储可能会延迟数百万个周期是合理的,可能仅受处理多核情况的硬件仲裁机制的公平算法的限制争夺对同一缓存行的访问权。

我忘记了我所读到的关于现代英特尔硬件是否以这种方式工作的内容,但我认为商店可能会从乱序核心中退出,但仍未承诺使用 L1 缓存。相反,它只是在商店队列中作为肯定会发生的商店。这将使缓存未命中存储避免阻止新指令进入 ROB。 (负载需要探测存储缓冲区以在单核内保持正确执行,但这样做并不需要 ROB 也跟踪存储)。

【讨论】:

  • 整个 asm 语句可以简化为 asm("");
  • @MichaelPetch:哦,哈哈。是的,因为没有输出的asm() 是隐式易变的。
  • "即使没有这个,编译器也必须假设 asm volatile 语句可能读取任何全局变量" 实际上,它没有。从 gcc 的扩展 asm 中访问(非易失性)全局变量(没有明确将它们列为输入)是不安全的。直到 gcc v7.x,这在基本 asm 中也是不安全的。也就是说,在这种特殊情况下,它是这样工作的,因为使用 asm 通常会禁用内联,并且函数调用会隐式刷新内存。
  • 我应该指出 pintOS 不使用 OPs 代码 AFAIK。认为这是他自己的代码。 PintOS 定义了一个barrier() 宏,可以在许多不同的上下文中使用。它被定义为#define barrier() asm volatile ("" : : : "memory") 他们记录为 是一个特殊的语句,它阻止编译器对跨屏障的内存状态做出假设。编译器不会对跨屏障的变量的读取或写入进行重新排序,也不会假设变量的值未通过屏障进行修改,但从未获取地址的局部变量除外。
  • @PeterCordes 考虑:globalX = 1; asm("mov $6, globalX":::); globalX++;(或参见this)。无论是否使用 -fwhole-program,这将返回 2,而不是 7。使用 memoryvolatile"+rm"(globalX) 给出“正确”答案。在 gcc v7.x+ 中,基本 asm 会执行隐式内存破坏,所以我希望它也能工作。还有,“CPU里面没有小恶魔”来源?这肯定会解释很多......
猜你喜欢
  • 2014-09-07
  • 2016-07-29
  • 2020-05-18
  • 1970-01-01
  • 2021-12-20
  • 2016-11-10
  • 2018-03-09
  • 2015-06-12
  • 2010-11-14
相关资源
最近更新 更多