【问题标题】:how does store operation in memory performance work?内存性能中的存储操作如何工作?
【发布时间】:2021-12-15 14:24:21
【问题描述】:

我正在使用这本教科书 Randal E. Bryant, David R. O'Hallaron - Computer Systems。 A Programmer's Perspective [3rd ed.] (2016, Pearson),其中有一段我不太理解。

C 代码:

void write_read(long *src, long *dst, long n)
{
 long cnt = n;
 long val = 0;

 while (cnt) {
  *dst = val;
  val = (*src)+1;
  cnt--;
 }
}

write_read的内循环:

#src in %rdi, dst in %rsi, val in %rax
 .L3: 
    movq %rax, (%rsi)  # Write val to dst
    movq (%rdi), %rax  # t = *src
    addq $1, %rax      # val = t+1
    subq $1, %rdx      # cnt--
    jne .L3            # If != 0, goto loop

给定这段代码,教科书给出这个图来描述程序流程

这是给那些无法接触到 TB 的人的解释:

图 5.35 显示了这个循环代码的数据流表示。该指令 movq %rax,(%rsi) 被翻译成两个操作: s_addr 指令计算存储操作的地址,创建一个 存储缓冲区中的条目,以及 设置该条目的地址字段。 s_data 操作为 入口。正如我们将看到的,这两个计算独立执行的事实对程序性能很重要。 这促使分离 参考机中这些操作的功能单元。

除了操作之间的数据依赖关系之外 寄存器的写和读,操作符右边的弧线表示 这些操作的一组隐式依赖项。特别是地址 s_addr 操作的计算必须明显在 s_data 操作之前。

在 此外,解码指令movq (%rdi), %rax生成的加载操作必须检查任何未决存储操作的地址,创建一个数据 它与 s_addr 操作之间的依赖关系。该图显示了一个虚线弧 在 s_data 和加载操作之间。这种依赖是有条件的:如果 两个地址匹配,加载操作必须等到 s_data 已经存入 其结果存入存储缓冲区,但如果两个地址不同,则这两个操作 可以独立进行。

a) 我不太清楚的是为什么在movq %rax,(%rsi) 这行之后需要在调用s_data 之后完成load?我假设在调用s_data 时,%rax 的值存储在%rsi 的地址指向的位置?这是否意味着每次s_data 之后都需要一个load 调用?

b) 它并没有真正显示在图表中,但根据我从书中给出的解释中了解到,movq (%rdi), %rax 这一行需要自己的一组s_addrs_data?那么是否准确地说所有movq 调用都需要s_addrs_data 调用,然后在调用load 之前检查地址是否匹配?

对这些部分很困惑,如果有人能解释s_addrs_data 调用如何与load 一起工作,以及何时需要这些功能,谢谢!!

【问题讨论】:

  • 我根本没有得到示例代码——rsidsi 没有递增,所以代码只是一次又一次地复制相同的字节。那些movq指令不应该是lodsqstosq吗?
  • 不要认为它是lodsqstosq,在这个练习中,我们使用movq 来工作:) 我们必须将它“分解”为 s_addr/s_data如图所示...@TonyK
  • 现在您已经添加了C 代码,同样适用。我知道这只是一个例子,但它不必如此毫无意义! (此外,如果 cnt 为零,则两个 sn-ps 的行为非常不同。)
  • @TonyK:谈论memory disambiguation(是否存储转发)看起来像是一个人为的例子。它不需要是一个循环,但是让它成为一个循环可以为多次迭代计时。有趣的事实:现代 CPU 动态预测加载是否来自与早期存储相同的地址,如果存在地址未知的存储(s_addr uop 尚未执行,无论@ 987654360@)。 github.com/travisdowns/uarch-bench/wiki/… 有一些 SKL 实验
  • @TonyK:我假设 asm do{}while() 样式循环只是循环,省略了对 cnt == 0 的检查以跳过整个循环 that a compiler would have put around it。或者它来自一个构建,其中cnt 是一个编译时常量,因此已知为非零。由于问题与此无关,所以没什么大不了的,我认为这个问题不需要更多混乱。

标签: assembly optimization x86 cpu-architecture


【解决方案1】:

蓝色方框中的操作是流水线解码器发出的微操作(也称为微指令或微指令)。它们是正在执行的程序的一部分。 movq (%rdi), %rax 指令被解码到加载微指令中。 uop 是流水线中的执行单元。 Uop 不是调用,而是执行

根据书中讨论的假设处理器设计,像movq %rax, (%rsi) 这样的简单存储指令被解码为两个微指令,分别称为s_addrs_data。这也发生在真正的 x86 处理器中。宏指令可能被解码为多个微指令的一个原因是微指令的格式不允许它保存指令中给出的所有信息,例如当指令具有太多操作数或表示复杂任务时.另一个原因是增加指令级并行度。存储的地址和存储的数据可以在不同的周期中变得可用。如果地址可用但数据不可用,则可以将 s_addr uop 分派到加载-存储单元,以使下游加载 uop 的地址更早地与存储的地址进行比较,而无需等待商店的数据。确定后面的加载是否依赖于前面的存储的过程称为内存消歧。如果加载movq (%rdi), %rax与之前的存储movq %rax, (%rsi)不重叠,则可以立即执行,无论%rax中的值是否准备好。

s_data uop 被执行时,%rax 中的值被存储在存储缓冲区条目的数据字段中,其中分配了存储 uop。在所有较早的指令完成执行以维护程序顺序之后,将值存储在目标内存位置。

书上说“s_addr 操作的地址计算必须清楚地在 s_data 操作之前”可能是因为,根据书,s_addr uop 必须首先在存储缓冲区中创建一个条目在数据可以存储在其中之前。这对于假设的设计可能没问题,但这是不必要的依赖,因为分配可以在执行之前完成。无论如何,本书都没有讨论资源分配和回收。

一个简单的加载指令被解码为一个加载微指令。没有理由将负载分成多个微指令。

【讨论】:

  • 很明显这本书声称s_data 不能先执行。实际上,在真正的 Intel CPU(它确实将存储解码为 2 个像这样的单独的微指令)中,存储缓冲区条目在发出/重命名/分配期间分配,因为存储指令的微指令被复制到无序的后端. AMD 将存储作为单个 uop 处理,但我认为对于单独的执行单元有单独的队列,所以我认为 uop 只是被复制到两个队列中,这与英特尔的宏融合 store-addr + store-data uop 几乎相同。
猜你喜欢
  • 2010-12-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-04-21
  • 1970-01-01
  • 1970-01-01
  • 2021-07-29
  • 2021-02-12
相关资源
最近更新 更多