【问题标题】:Timing of executing 8 bit and 64 bit instructions on 64 bit x64/Amd64 processors在 64 位 x64/Amd64 处理器上执行 8 位和 64 位指令的时序
【发布时间】:2021-03-29 05:04:12
【问题描述】:

8 it 和 64 位指令在 64 位 x64/Amd64 处理器上是否有任何执行时序差异,当这些指令除了位宽相似/相同时? 有没有办法找到执行这两个微型汇编函数的真正处理器时序?

-谢谢。

; 64 bit instructions
add64:
     mov  $0x1, %rax
     add  $0x2, %rax
     ret

; 8 bit instructions
add8:
     mov  $0x1, %al
     add  $0x2, %al
     ret

【问题讨论】:

  • 处理器计时是不确定的。它是流水线的,在这种情况下是微编码的,等等。您可能会发现任何记录在案的时间都在特定的理想情况下,您不会真正看到连续 n 条以上的指令......在这种情况下,尽管总线足够宽以至于它不应该有所作为,但 8 位可能会因为屏蔽而变慢
  • 理论上,现代处理器执行 64 位加法的速度与执行 8 位加法的速度一样快。但是,时间在很大程度上取决于一组指令所处的上下文——在这些示例中,调用函数的指令也会发挥作用,实际结果在实际使用中可能会有很大差异。掩码 old_timer 是指从架构的角度来看,8 位操作需要处理器保持旧的rax 的高 7 字节,并将其与 8 位答案结合起来;而 64 位操作会产生一个新的rax
  • 作为一个非常粗略的经验法则,您可以期望“简单”指令(mov、add、sub、按位)是相同的,而“复杂”指令(乘法、除法等)对于较大的操作数可能会更慢。 x86 指令时序的有用资源是uops.info

标签: performance assembly 64-bit x86-64 micro-optimization


【解决方案1】:

是的,有区别。 mov $0x1, %al 在大多数 CPU 上对 RAX 的旧值具有错误的依赖性,包括比 Sandybridge 更新的所有 CPU。这是一个2输入1输出指令;从 CPU 的角度来看,它就像add $1, %al 一样独立调度或不相对于 RAX 的其他用途。只有写入 32 位或 64 位寄存器才会启动新的依赖链。

这意味着 add8 函数的 AL 返回值可能要等到调用者在调用之前恰好在 EAX 中执行的某些独立工作的缓存未命中后才准备好,但 add64 的 RAX 结果可能是立即准备好乱序执行,以开始调用者中使用返回值的后续指令。 (假设他们的其他输入也准备好了。)

它们的代码大小也不同:这两条 8 位指令都是 2 字节长。 (感谢 AL,imm8 短格式编码;add $1, %dl 将是 3 个字节)。 RAX 指令有 7 个和 4 个字节长。这对于 L1i 缓存占用很重要(在大规模上,对于必须从磁盘调入多少字节)。在小范围内,如果 CPU 正在执行旧版解码,则有多少指令可以放入 16 或 32 字节的提取块中,因为代码在 uop 缓存中还不是很热。后面指令的代码对齐也会受到前面指令长度不同的影响,有时会影响哪些分支相互别名。

https://agner.org/optimize/ 解释了各种 x86 微架构的流水线细节,包括前端解码效果,它可以使指令长度不仅仅是 I-cache / uop-cache 中的代码密度。

通常 32 位操作数大小是最有效的(就性能而言,并且对于代码大小来说非常好)。 32 和 8 是 x86-64 可以在没有额外前缀的情况下使用的操作数大小,并且在实践中使用 8 位以避免停顿和错误,您需要更多指令或更长的指令,因为它们不会零扩展。 The advantages of using 32bit registers/instructions in x86-64.

在 64 位操作数大小的 ALU 中,一些指令实际上更慢,而不仅仅是前端效果。这包括大多数 CPU 上的 div 和一些旧 CPU 上的 imul。还有 popcnt 和 bswap。例如Trial-division code runs 2x faster as 32-bit on Windows than 64-bit on Linux

请注意 mov $0x1, %rax 将使用 GAS 组装成 7 个字节,除非您使用 as -O2(与 gcc -O2 不同,请参阅 this 示例)使其优化为 mov $1, %eax 完全相同的架构效果,但更短(没有 REX 或 ModRM 字节)。一些汇编程序默认情况下会进行优化,但 GAS 不会。 Why NASM on Linux changes registers in x86_64 assembly 有更多关于为什么这个优化是安全和好的,以及为什么你应该在源代码中自己做,特别是如果你的汇编程序不为你做。


但除了错误的 dep 和代码大小之外,它们对于 CPU 的后端是相同的:所有这些指令都是单 uop 并且可以在任何标量整数 ALU 执行端口上运行1.https://uops.info/ 对每条非特权指令的每种形式都有自动测试结果)。

脚注 1:挖掘机(上一代 Bulldozer 系列)还可以在另外 2 个端口 (AGU) 上运行 mov $imm, %reg,用于 32 位和 64 位操作数大小。但是将新的低 8 或低 16 合并到一个完整的寄存器中需要一个 ALU 端口。所以mov $1, %rax 在 Excavator 上有 4 个/时钟的吞吐量,但mov $1, %al 只有 2 个/时钟的吞吐量。 (当然,仅当您使用几个不同的目标寄存器时,实际上不是重复使用 AL;这将是 1/clock 的延迟瓶颈,因为在该微架构上写入部分寄存器会产生错误的依赖性。)

以前从 Piledriver 开始的 Bulldozer 系列 CPU 可以在 EX0、EX1、AGU0、AGU1 上运行 mov reg, reg(对于 r32 或 r64),而包括 mov $imm, %reg 在内的大多数 ALU 指令只能在 EX0/1 上运行。进一步扩展 AGU 端口的功能以处理 mov-immediate 是 Excavator 中的一项新功能。

幸运的是 Bulldozer 被 AMD 更好的 Zen 架构淘汰了,该架构具有 4 个完整的标量整数 ALU 端口/执行单元。 (以及更宽的前端和 uop 缓存、良好的缓存,并且通常不会像 Bulldozer 那样糟糕。)


有没有办法测量它?

是的,但通常不在您使用call 调用的函数中。而是把它放在一个展开的循环中,这样你就可以用最少的其他指令运行它很多次。查看 CPU 性能计数器结果以查找前端/后端 uop 计数以及循环的总时间特别有用。

您可以构建循环来测量延迟或吞吐量;见RDTSCP in NASM always returns the same value (timing a single instruction)。另外:

一般情况下,您不需要测量自己(虽然了解如何测量很好,这有助于您了解测量的真正含义)。人们已经为大多数 CPU 微架构做到了这一点。您可以根据分析指令来预测特定 CPU 的某些循环的性能(如果您可以假设没有停顿或缓存未命中)。通常这可以相当准确地预测性能,但是 OoO exec 只能部分隐藏的中等长度的依赖链使得准确预测或解释每个周期变得过于困难。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-02-23
    • 2011-01-30
    • 1970-01-01
    • 1970-01-01
    • 2013-01-11
    • 2010-09-19
    • 1970-01-01
    相关资源
    最近更新 更多