到分段寄存器的 mov 是否比到通用寄存器的 mov 慢?

Is a mov to a segmentation register slower than a mov to a general purpose register?

具体是:

mov %eax, %ds

mov %eax, %ebx

或者它们的速度相同。我已经在网上进行了研究,但一直无法找到明确的答案。

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

N.B 我关心旧的 x86 linux cpus,而不是现代 x86_64 cpus,其中分段的工作方式不同。

通用寄存器之间的

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

在 Ivybridge 及更高版本上,它甚至不需要执行单元并且具有零延迟。它在注册重命名阶段处理。 即使在更早的 CPU 年代,任何 ALU 端口都是 1 uop(因此通常每个时钟吞吐量 3 或 4 个)。

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


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

相比,它仍然是次要的

mov 一个段reg很慢:它会触发GDT或LDT的加载来更新描述符缓存,所以它是微编码的。

即使在x86-64长模式下也是如此the GDT entry are ignored, but it still has to update the descriptor cache with other fields from the segment descriptor 中的段 base/limit 字段,包括适用于数据段的 DPL(描述符特权级别)。


Agner Fog's instruction tables list uop counts and throughput for mov sr, r (Intel synax, mov to segment reg) for Nehalem and earlier CPUs. He stopped testing seg regs for later CPUs because it's obscure and not used by compilers (or humans optimizing by hand), but the counts for SnB-family are probably somewhat similar. (InstLatx64 doesn't test seg regs either, e.g. not in this Sandybridge instruction-timing test)

MOV sr,r on Nehalem(推测是在保护模式或长模式下测试):

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

其他CPU类似:

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

  • Pentium 4:4 uops + 4 微码,14c 吞吐量。

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

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

  • P4 普雷斯科特:1 uop + 8 微码。 27c 吞吐量。 读取段 reg = 8c 吞吐量。

  • Pentium M:p0 为 8 微指令,与 PIII 相同。

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

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

    在一个循环中写入 4 个 不同的 seg regs (ds/es/fs/gs) 都具有相同的选择器:四个 mov 每 25c 吞吐量,6 fused/unfused domain uops。 (也许有些会被取消?)

    在循环中写入 ds 4 次: 每 72c 一个迭代(每 18c 一个 mov ds,eax)。相同的 uop 计数:每个 mov.

    ~6 个融合和未融合

    这似乎表明 Skylake 不会重命名段 regs:在下一次写入开始之前必须先完成对一个的写入 .

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

  • Atom:7 微指令,21c 吞吐量

  • 通过 Nano 2000/3000:未列出的 uops,20 个周期的吞吐量和延迟。 Nano 3000 的 读取 段寄存器 (mov r, sr) 的周期吞吐量为 0.5。没有列出延迟,这很奇怪。也许他正在根据何时可以将其用于负载来测量段写入延迟?像循环中的 mov eax, [ebx] / mov ds, eax

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

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


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

All integer, floating point, MMX, XMM, flags and segment registers can be renamed. The floating point control word can also be renamed.

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

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

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

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

如果您有兴趣,可以使用性能计数器自行对其进行基准测试。 (请参阅 加载和存储唯一被重新排序的指令吗? 有关如何测试阻止乱序执行的示例,以及 )有关使用的基础知识perf 在 Linux 上对小循环进行微基准测试。


正在读取段 reg

mov 一个segment reg比较便宜:它只修改一个GP寄存器,而CPUs 擅长写入 GP 寄存器,使用寄存器重命名等。Agner Fog 发现它是 Nehalem 上的单个 uop。有趣的是,在 Core2 / Nehalem 上它 运行 在加载端口上,所以我猜这就是在该微体系结构上存储段 regs 的地方。

(P4 除外:显然那里读取段寄存器很昂贵。)

对我的 Skylake(长模式)的快速测试表明 mov eax, fs(或 csds 或其他)是 2 uops,其中一个只能运行在端口1上,另一个可以运行在任何p0156上。 (即它在 ALU 端口上 运行s)。它的吞吐量为每个时钟 1 个,在端口 1 上出现瓶颈。

未测试: 与内存指令交错,包括缓存未命中加载,以查看是否有多个 dep 链正在运行。所以我真的只测试了吞吐量。如果存在吞吐量瓶颈而不是 WAW 危害本身,则不排除跟踪段寄存器以及 loads/stores。但这对于现代代码来说似乎不太值得:段 regs 通常只在特权级更改之前或之后更改,无论如何都会耗尽无序后端,而不是与各种 loads/stores 混合。除了可能根据上下文切换更改 FS 或 GS​​。


你一般只会搞乱FS或者GS来做线程本地存储,你不会用mov给FS搞,你做一个系统调用让OS 使用 MSR 或 wrfsbase 修改缓存段描述中的段基数。(或者如果 OS 允许并且 CPU 支持,您可以使用 wrfsbase 在用户-space.)


N.B I'm concerned with old x86 linux cpus, not modern x86_64 cpus, where segmentation works differently.

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

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

(Sandybridge 是派生自 P6 系列的第一个新系列,具有重大的内部变化,P4 的一些想法以不同(更好)的方式实现,例如 SnB 的解码 uop 缓存 不是 跟踪缓存。但更重要的是,SnB 使用物理寄存器文件而不是将 values 保存在 ROB 中,因此其寄存器重命名机制不同。)

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

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

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

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

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

正如问题中提到的x86 CPU,我们可以一直追溯到1985年,使用原始的80386。它的手册给出了所有指令的时钟周期计数。

  • movl %reg, %reg: 2 个时钟

  • movw %reg, %sreg 在实模式下:2 个时钟

  • movw %reg, %sreg在保护模式下:18个时钟

所以是的,慢很多。

I think it's conceivable modifying a segmentation register could make the processor do extra work.

该手册给出了在保护模式下加载段寄存器时所做的所有检查的伪代码,它占用了大约一整页纸。绝对是额外的工作。