【问题标题】:Does the CPU actually executes an instruction before the other when re-ordering, or is it only the end result that gives this "illusion"?CPU在重新排序时实际上是否在另一个指令之前执行了一条指令,还是只是最终结果给出了这种“错觉”?
【发布时间】:2017-08-01 11:47:51
【问题描述】:

根据我所读到的,CPU 可以重新排序指令的执行,而内存屏障防止指令从前到后以及从后到前重新排序。

但有些事情我不确定。假设我有以下说明:

store x
store y

假设 CPU 决定在 store x 之前执行 store y

CPU 是如何做到这一点的,它会完全忽略store x 并首先执行store y 吗?还是会出现以下情况?:

  1. store x 被执行,但并没有立即完成(它变成 待定)。
  2. store y被执行,立即完成。
  3. 待处理的store x 已完成。

所以基本上,这给出了指令被乱序执行的“错觉”,即使它们没有,它们只是乱序完成。


我问这个问题是为了了解内存屏障的工作原理。

例如说我有以下说明:

store x
mfence
store y

现在当 CPU 执行这些指令时,会发生以下情况吗?:

  1. store x 被执行,但并没有立即完成(它变成 待定)。
  2. mfence 被执行,现在因为这条指令是一个内存 屏障,CPU 将确保所有挂起的操作之前 它 (store x) 将在继续执行指令之前完成。
  3. store y 被执行。

【问题讨论】:

  • 乱序的唯一意义就是实际上是乱序执行,错觉是它们按顺序执行。请注意,有一些规则,其中之一是:写入内存不会与其他写入重新排序(假设 WB 并且没有像显式非时间这样的花哨的东西)
  • 所有外围设备都需要在写入前进行设置才能说 go/enable/run 等,乱序写入将是一场灾难。
  • 认为 a=b+c; d=e+f; h=5; g=a+d。 d= 可能发生在 a= 之前,一切都会好起来的,或者可能移动 h=5。想一想,如果某个寄存器很忙,并且还有其他一些不忙的东西,可以在不改变程序功能的情况下插入,然后运行它。
  • 不了解 x86,但在 arm 上,您使用内存屏障进行诸如刷新写入缓冲区或使缓存无效之类的事情,然后让其他任何人进行任何内存操作使缓存无效并基本上完成任何未决的内存事务。数据屏障基本上会说完成飞行中或队列中的任何数据事务。并且指令障碍会说在继续之前完成管道。
  • 搜索开源项目(如 linux),看看他们在哪些地方使用了内存屏障,在哪些地方没有使用...它应该开始阐明您的困惑。

标签: assembly x86 memory-barriers


【解决方案1】:

mfence 不会阻止乱序执行
它只是确保mfence 之前的所有内存加载和存储在执行mfence 之后的任何内存加载或存储之前都被序列化。

见:http://x86.renejeschke.de/html/file_module_x86_id_170.html

对在 MFENCE 指令之前发出的所有从内存加载和存储到内存指令执行序列化操作。这种序列化操作保证了在程序顺序中位于 MFENCE 指令之前的每条加载和存储指令都是全局可见的,然后在 MFENCE 指令之后的任何加载或存储指令都是全局可见的。

X86 在任何情况下都限制了 OoO 内存访问
x86 架构确实已经内置了一些内存排序规则。
其要点是内存访问很少接受重新排序。

这是来自英特尔的官方文章:http://www.cs.cmu.edu/~410-f10/doc/Intel_Reordering_318147.pdf

索引中列出了要点:-)

回写 (WB) 内存的内存排序
* 负载不会与其他负载一起重新排序,并且存储不会与其他存储重新排序
* 商店不会使用旧负载重新排序
* 负载可能会与旧商店重新排序到不同的位置
[...]
* 加载和存储不会用锁重新排序

回到你的问题

在重新排序时,CPU 是否真的先执行一条指令
是的,您可以在对代码计时时看到这一点。

让我举个例子,假设我们有一个 AMD jaguar,它可以并行执行 2 条指令并且具有完整的 OoO。

a: mov ebx,[eax]      //1 cycle throughput
b: mov ecx,2          //pairs
c: imul eax,edx       //3 cycles latency
d: add eax,ebp        //1 cycle, needs to wait for c

通常这个 sn-p 需要 1+3+1 = 5 个周期。 但是,CPU 会按以下顺序执行:

c: imul eax,edx      //3 cycle latency
a: mov ebx,[eax']    //pairs, eax is renamed to eax' in the register rename buffer
b: mov ecx,2         //1 cycle
d: add eax,ebp       //1 cycle waits for c

这只需要 4 个周期。 3 代表 a 和 1 代表 d,其余的都是交错的。
显然,在 c 和 d 之间有空间可以压缩更多指令,如果 CPU 有任何适用的指令,它就会这样做。

请注意,CPU 会重新排序内存负载,只要它与另一个内存负载无关(以及其他一些限制,请参见上文)。
另请注意,AMD 和 Intel 遵循完全相同的语义。

【讨论】:

    【解决方案2】:

    在超标量处理器上,您可以让操作排队等待前面的指令完成。想象一下这样的代码:

    ...
    div %esi        # divide edx:eax by esi
    mov %eax,(%ebx) # store quotient in (%ebx)
    mov $1,(%ecx)   # store 1 in (%ecx)
    

    在超标量处理器上,第一条mov 指令将在div 指令被调度后立即遇到。不过,那个时候div还没有说完。因此,存储指令在指令队列中排队,直到div %esi 的结果在%eax 中可用。在下一个周期,处理器遇到mov $1,(%ecx)。由于立即的$1 立即可用,处理器不必等待并且可以立即执行存储。在 store 被调度一段时间后,div 指令完成,导致 store 从指令队列中释放并执行。

    这就是存储发生的顺序与机器代码指定的顺序不同的情况。 CPU 有额外的逻辑来确保程序员通常看不到这个细节,但根据您编程的架构,可能存在不同的工件。

    【讨论】:

      猜你喜欢
      • 2017-07-31
      • 1970-01-01
      • 2015-11-10
      • 1970-01-01
      • 2020-08-29
      • 2019-04-16
      • 1970-01-01
      • 2019-04-08
      • 2019-11-30
      相关资源
      最近更新 更多