【问题标题】:Is a mov to a segmentation register slower than a mov to a general purpose register?移动到分段寄存器是否比移动到通用寄存器慢?
【发布时间】:2018-12-12 07:28:01
【问题描述】:

具体是:

mov %eax, %ds

慢于

mov %eax, %ebx

或者它们的速度相同。我在网上研究过,但一直找不到明确的答案。

我不确定这是不是一个愚蠢的问题,但我认为修改分段寄存器可以让处理器做额外的工作是可以想象的。

注意,我关心的是旧的 x86 linux cpus,而不是现代 x86_64 cpus,它的分段工作方式不同。

【问题讨论】:

  • 是的,它比较慢。此外,您不能在保护模式下将任意值加载到段寄存器中(除了大小为 16 位)。指令集手册至少有提示,确实这使得 cpu 做了很多工作,可能包括内存访问:“将段选择器移动到段寄存器中会自动导致与该段选择器关联的段描述符信息是加载到段寄存器的隐藏(阴影)部分。[...]段描述符数据是从指定段选择器的 GDT 或 LDT 条目中获取的。"
  • 请参阅Agner's tables 了解时间。一般来说,向段寄存器的移动比在通用寄存器之间的移动慢大约 10-20 倍。
  • @fuz 我看了,但至少在我的副本中找不到它。啊,显然它没有列出所有处理器。
  • @Jester 这是mov r,sr resp。 mov m,srmov sr,rmov sr,m。似乎适用于大多数架构。
  • 不是全部,看起来只有旧的。例如,只有 AMD K7-10 有它,任何其他 AMD 都没有。或者我是盲人:)

标签: assembly x86 intel mov cpu-cycles


【解决方案1】:
通用寄存器之间的

mov %eax, %ebx常见的指令之一。现代硬件非常有效地支持它,通常是不适用于任何其他指令的特殊情况。在较旧的硬件上,它一直是最便宜的指令之一。

在 Ivybridge 及更高版本上,它甚至不需要执行单元并且具有零延迟。它在注册重命名阶段处理。 Can x86's MOV really be "free"? Why can't I reproduce this at all? 即使在早期的 CPU 上,任何 ALU 端口也是 1 uop(因此通常每个时钟吞吐量 3 或 4 个)。

在 AMD Piledriver / Steamroller 上,mov r32,r32 和 r64,r64 可以在 AGU 端口和 ALU 端口上运行,使其每个时钟吞吐量为 4 个,而每个时钟为 2 个用于添加,或 mov 在 8 或 16 -bit 寄存器(必须合并到目的地)。


mov 到段 reg 是典型的 32 位和 64 位代码中相当少见的指令。但是,它是内核为每个系统调用(可能还有中断)所做的一部分,因此提高它的效率将加快系统调用和 I/O 密集型工作负载的快速路径。因此,即使它只出现在少数几个地方,它也可以运行相当数量。但与mov r,r相比,它仍然是次要的!

mov 段注册很慢:它触发了来自 GDT 或 LDT 的加载以更新描述符缓存,因此它是微编码的。

即使在 x86-64 长模式下也是如此; the GDT entry 中的段基/限制字段被忽略,但它仍然必须使用 the segment descriptor 中的其他字段更新描述符缓存,包括适用于数据段的 DPL(描述符特权级别)。


Agner Fog's instruction tables 列出 Nehalem 和早期 CPU 的 mov sr, r(Intel synax,mov to segment reg)的 uop 计数和吞吐量。他停止为后来的 CPU 测试 seg reg,因为它晦涩难懂,并且不被编译器(或人类手动优化)使用,但 SnB 系列的计数可能有点相似。 (InstLatx64 也不测试 seg regs,例如不在这个 Sandybridge instruction-timing test 中)

MOV sr,r 在 Nehalem 上(大概在保护模式或长模式下测试)

  • 6 个用于前端的融合域微控制器
  • 3 微指令用于 ALU 端口 (p015)
  • 3 uop 用于加载端口 (p2)
  • 吞吐量:每 13 个周期 1 个(用于在一个巨大的循环中重复此指令数千次)。 IDK 如果 CPU 重命名段 regs。如果不是,它可能会停止以后的加载(或所有以后的指令?),直到描述符缓存被更新并且 mov 到 sr 指令退出。即我不确定这会对周围代码的乱序执行产生多大影响。

其他CPU类似:

  • PPro/PII/PIII(原始 P6):p0 为 8 uops,未列出吞吐量。 5 个周期延迟。 (请记住,这个 uarch 是在 1995 年发布之前设计的,当时 16 位代码仍然很常见。这就是 P6 系列对整数寄存器(AL、AH 与 AX 分开)进行部分寄存器重命名的原因)
  • 奔腾 4:4 微指令 + 4 微码,14c 吞吐量。

    延迟 = 12c 16 位实数或 vm86 模式,24c 32 位保护模式。 12c 是他在主表中列出的内容,因此推测他对其他 CPU 的延迟数也是实模式延迟,其中写入段 reg 只是设置基数 = sreg<<4。)

    在 P4 上读取段 reg 很慢,与其他 CPU 不同:4 微指令 + 4 微码,6c 吞吐量

  • P4 Prescott:1 uop + 8 微码。 27c 吞吐量。 读取段 reg = 8c 吞吐量。

  • Pentium M:8 uop for p0,与 PIII 相同。

  • Conroe/Merom 和 Wolfdale/Penryn(第一代和第二代 Core2):8 个融合域 uop、4 个 ALU (p015)、4 个负载/AGU (p2)。每 16 个周期吞吐量一个,是 Agner 测试过的所有 CPU 中最慢的。

  • Skylake(我的测试用我在循环外读取的值重新加载它们):在只有 dec/jnz 的循环中:10 个融合域 uops(前端),6 个未融合域(执行单位)。每 18c 吞吐量一个。

    在一个循环中写入 4 个不同 seg regs (ds/es/fs/gs),所有这些都使用相同的选择器:每个 25c 吞吐量四个 mov, 6 个融合/非融合域微指令。 (也许有些被取消了?)

    循环写入ds 4 次: 每 72c 一次迭代(每 18c 一次 mov ds,eax)。相同的 uop 计数:每个 mov 约 6 个融合和未融合。

    这似乎表明 Skylake 确实没有重命名段 regs:对一个的写入必须在下一次写入开始之前完成

  • K7/K8/K10:6 个“操作”,8c 吞吐量。

  • Atom:7 uop,21c 吞吐量

  • 通过 Nano 2000/3000:未列出的微指令、20 个周期的吞吐量和延迟。 Nano 3000 读取 seg reg (mov r, sr) 的吞吐量为 0.5 个周期。没有列出延迟,这很奇怪。也许他正在根据您何时可以将其用于负载来测量 seg-write 延迟?喜欢mov eax, [ebx] / mov ds, eax 循环吗?

奇怪的艾尔是对的,It's All About the Pentiums

有序 Pentium (P5 / PMMX) 具有更便宜的 mov-to-sr:Agner 将其列为需要 ">= 2 个周期",并且不可配对。 (P5 是有序的 2 宽超标量,具有一些指令可以一起执行的配对规则)。这对于保护模式来说似乎很便宜,所以也许 2 处于实模式而保护模式大于?我们从他的 P4 表笔记中得知,他当时确实在 16 位模式下测试过东西。


Agner Fog's microarch guide 表示 Core2 / Nehalem 可以重命名段寄存器(第 8.7 节寄存器重命名)

所有整数、浮点、MMX、XMM、标志和段寄存器都可以重命名。浮点控制字也可以重命名。

(Pentium M 可以重命名 FP 控制字,因此更改舍入模式会阻止 FP 指令的 OoO exec。例如,所有早期的 FP 指令必须在它可以修改控制字之前完成,并且后来的不能开始,直到之后。我猜段 regs 会是相同的,但用于加载和存储 uops。)

他说 Sandybridge 可以“可能”重命名段 reg,而 Haswell/Broadwell/Skylake 可以“也许”重命名它们。我对 SKL 的快速测试表明,重复编写相同的段 reg 比编写不同的段 reg 慢,这表明它们没有完全重命名。放弃支持似乎是一件显而易见的事情,因为它们很少在普通的 32 / 64 位代码中修改。

而且每个 seg reg 通常一次只修改一次,因此同一段寄存器的多个运行中的 dep 链不是很有用。 (即你不会在 Linux 中看到 WAW hazards 用于段 regs,并且 WAR 几乎没有相关性,因为内核不会将用户空间的 DS 用于内核入口点中的任何内存引用。(我认为中断正在序列化,但通过syscall 进入内核可能仍有用户空间加载或存储在运行中但尚未执行。)

在第 2 章中,一般解释了乱序执行(除了 P1 / PMMX 之外的所有 CPU),2.2 寄存器重命名说“可能可以重命名段寄存器”,但 IDK 如果他的意思是某些 CPU 会这样做并且有些人不知道,或者如果他不确定某些旧 CPU。他没有在 PII/PII 或 Pentium-M 部分中提到 seg reg 重命名,因此我无法告诉您您显然在询问的旧的仅 32 位 CPU。 (而且他在 K8 之前没有针对 AMD 的微架构指南部分。)

如果您有兴趣,可以使用性能计数器自行对其进行基准测试。 (有关如何测试阻塞乱序执行的示例,请参阅 Are loads and stores the only instructions that gets reordered?,有关在 Linux 上使用 perf 对微小循环进行微基准测试的基础知识,请参阅 Can x86's MOV really be "free"? Why can't I reproduce this at all?


读取段 reg

mov 来自段寄存器相对便宜:它只修改GP寄存器,CPU擅长写入GP寄存器,寄存器重命名等. Agner Fog 发现它是 Nehalem 上的一个微指令。有趣的事实是,在 Core2 / Nehalem 上,它在加载端口上运行,所以我猜这就是微架构上存储段 reg 的位置。

(P4 除外:显然在那里阅读 seg regs 很昂贵。)

对我的 Skylake 的快速测试(在长模式下)显示 mov eax, fs(或 csds 或其他)是 2 微指令,其中一个仅在端口 1 上运行,而另一个可以在任何 p0156 上运行。 (即它在 ALU 端口上运行)。它的吞吐量为每个时钟 1,在端口 1 上有瓶颈。


您通常只会将 FS 或 GS​​ 用于线程本地存储,而您不会将 mov 用于 FS,您进行系统调用以让操作系统使用 wrfsbase 进行修改缓存段描述中的段基础。


注意,我关心的是旧的 x86 linux cpus,而不是现代 x86_64 cpus,它的分段工作方式不同。

您说的是“Linux”,所以我假设您的意思是保护模式,而不是实模式(分段的工作方式完全不同)。 mov sr, r 可能在实模式下解码不同,但我没有测试设置,我可以使用性能计数器分析实模式或本地运行的 VM86 模式。

长模式下的 FS 和 GS 与保护模式下的工作基本相同,在长模式下“中性”的是其他 seg regs。我认为 Agner Fog 的 Core2 / Nehalem 数字可能与您在受保护模式下的 PIII 中看到的相似。它们属于同一个微架构家族。我认为我们没有在保护模式下写入 P5 Pentium 段寄存器的有用数字。

(Sandybridge 是源自 P6 家族的新家族中的第一个,具有重大的内部变化,P4 的一些想法实现了不同(更好)的方式,例如 SnB 的解码微指令缓存不是跟踪缓存。但更重要的是,SnB 使用物理寄存器文件而不是将 保留在 ROB 中,因此它的寄存器重命名机制不同。)

【讨论】:

  • re “移动到段 reg 是一个相当罕见的指令”:是的,它在应用程序代码中可能很少见。但是了解现代操作系统读取和写入段寄存器的频率会很有趣。它可能并不罕见,可能取决于系统的动态行为。
  • @HadiBrais:这就是该段的其余部分所说的:P 我的回答的第一个版本只是说它很少见且不重要,但后来我记得内核在进入/退出路径中使用它。顺便说一句,刚刚更新了对 Skylake 的测试。我很好奇。看起来 SKL 重命名 seg regs,因为重复写入 DS 比写入 DS/ES/FS/GS 慢:)
  • 感谢您将所有这些信息放在一个地方并进行测试。
  • 您的回答激励我将我的回答更新为related question
  • 惊人的答案。我很欣赏雾表的链接,它们是一个很好的资源!我已经接受了你的回答——我被它的完整性所震撼!
【解决方案2】:

补充一下 Peter 所说的,寄存器之间的移动只是在使用 Sandy Bridge 及以后的 PRF 方案时将指定架构寄存器的 RAT 指针更改为源架构寄存器的情况,因此没有执行单元.

从微序列器到段寄存器的移动大约是 8 微秒。它还在 nehalem 上具有 14 个周期的倒数吞吐量,这意味着发生了管道刷新,并且它可能作为微码辅助运行。微码例程包含将描述符的内存加载到作为 RS(预留站)中目标的专用描述符寄存器。

移动到段寄存器可以通过重命名机制来处理。段寄存器可以与描述符一起重命名,然后从逻辑地址加载会导致描述符被复制到保留站中作为源以及偏移寄存器,并由具有 AGU 的执行端口处理。这可能是浪费的,因为 RS 必须为每个条目都有一个描述符字段,其中 DS 段将为每个条目以相同的方式读取和复制到 RS 中。有英特尔专利讨论了这一点。有人建议 RS 也可以为段寄存器源或目标以及描述符源或目标提供单独的条目。

或者,移动到段寄存器可以简单地刷新和串行化管道,确保乱序内核中的所有内存操作都使用正确的段描述符。这必须在远调用中更改 CS 段时发生,因为解码阶段取决于内存和操作数大小的描述符字段。对于 mov,AGU 可以根据操作码字段中的段覆盖直接从段描述符中读取,而不必从 RS 中读取重命名的描述符。远跳实际上可能由 MSROM 在线完成,而不是退出,因为预测不是针对远跳进行的,并且它总是错误预测未采取,这具有解码器具有更新的 CS 的效果,作为 CS 和 CS 描述符在流水线重新转向正确的线性地址之前写入完成。

从段寄存器加载显然不是通过更改 RAT 指针来完成的; uops 实际执行,表明段寄存器和整数寄存器有单独的专用寄存器用于重命名。我猜它们和控制寄存器不能重命名,只有一个专用寄存器可以重命名源。

【讨论】:

  • mov-elimination 是 IvB 中的新功能,而不是第一代 SandyBridge。它也不会 100% 成功,例如用于背靠背相关的 mov 指令。 Can x86's MOV really be "free"? Why can't I reproduce this at all? 有更多信息。但是,是的,它非常便宜,并且通常在现代 Intel 和 AMD CPU 上被淘汰。
  • @PeterCordes 我没有研究过,但我认为如果目标架构寄存器当前指向 64 位寄存器,则无法消除向 32 位寄存器的移动,因为它需要归零
  • 英特尔至少会在 reg 的高字节已知为零时进行跟踪。它甚至可以消除movzx ecx, al。 (而且我认为这不需要 AL == RAX)。所以我猜它可以更新每个 RAT 条目的上零状态,或者类似的东西。不过,我还没有用上半部分非零的寄存器仔细测试过。
  • @PeterCordes 我读到了一个关于 unlamination 解码器的专利,该解码器跟踪归零 uops,然后在以下指令中从融合的归零 + 移动中删除归零操作,即如果寄存器已经写入 eax归零
猜你喜欢
  • 1970-01-01
  • 2020-11-21
  • 1970-01-01
  • 1970-01-01
  • 2022-09-25
  • 1970-01-01
  • 1970-01-01
  • 2016-11-11
相关资源
最近更新 更多