通用寄存器之间的
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:对一个的写入必须在下一次写入开始之前完成。
有序 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(或 cs 或 ds 或其他)是 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 中,因此它的寄存器重命名机制不同。)