具体是:
mov %eax, %ds
慢于
mov %eax, %ebx
还是它们的速度相同。我在网上搜索过,但一直找不到明确的答案。
我不确定这是否是一个愚蠢的问题,但我认为修改分段寄存器可以使处理器做额外的工作是可以想象的。
注意我关心的是旧的 x86 linux cpus,而不是现代的 x86_64 cpus,其中分段的工作方式不同。
具体是:
mov %eax, %ds
慢于
mov %eax, %ebx
还是它们的速度相同。我在网上搜索过,但一直找不到明确的答案。
我不确定这是否是一个愚蠢的问题,但我认为修改分段寄存器可以使处理器做额外的工作是可以想象的。
注意我关心的是旧的 x86 linux cpus,而不是现代的 x86_64 cpus,其中分段的工作方式不同。
mov %eax, %ebx
通用寄存器之间是最常见的指令之一。现代硬件非常有效地支持它,通常是不适用于任何其他指令的特殊情况。在较旧的硬件上,它一直是最便宜的指令之一。
在 Ivybridge 及更高版本上,它甚至不需要执行单元并且具有零延迟。它在注册重命名阶段处理。 x86的MOV真的可以“免费”吗?为什么我根本无法重现这个? 即使在早期的 CPU 上,任何 ALU 端口也是 1 uop(因此通常每个时钟吞吐量为 3 或 4 个)。
在 AMD Piledriver / Steamrollermov r32,r32
和 r64 上,r64 可以在 AGU 端口和 ALU 端口上运行,给它每个时钟 4 个吞吐量,而每个时钟 2 个用于添加,或者用于mov
8 位或 16 位寄存器(必须合并到目的地)。
mov
到段 reg 是典型的 32 位和 64 位代码中相当少见的指令。但是,它是内核为每个系统调用(可能还有中断)所做的一部分,因此提高它的效率将加快系统调用和 I/O 密集型工作负载的快速路径。因此,即使它只出现在少数几个地方,它也可以运行相当数量。但与 ! 相比,它仍然是次要的mov r,r
!
mov
到段 reg 很慢:它触发来自 GDT 或 LDT 的加载以更新描述符缓存,因此它是微编码的。
即使在 x86-64 长模式下也是如此;GDT 条目中的段基/限制字段被忽略,但它仍然必须使用段描述符中的其他字段更新描述符缓存,包括适用于数据段的 DPL(描述符特权级别)。
Agner Fog 的指令表列出了 Nehalem 和早期 CPU 的 uop 计数和吞吐量mov sr, r
(Intel synax,mov to segment reg)。他停止为后来的 CPU 测试 seg reg,因为它晦涩难懂,并且不被编译器(或人类手动优化)使用,但 SnB 系列的计数可能有点相似。( InstLatx64也不测试 seg regs,例如不在这个Sandybridge 指令时序测试中)
MOV sr,r
在 Nehalem 上(大概在保护模式或长模式下测试):
其他 CPU 类似:
Pentium 4:4 微指令 + 4 微码,14c 吞吐量。
延迟 = 12c 16 位实数或 vm86 模式,24c 在 32 位保护模式下。12c 是他在主表中列出的内容,因此推测他对其他 CPU 的延迟数也是实模式延迟,其中写入段 reg 只是设置基数 = sreg<<4
。)
与其他 CPU 不同,在 P4 上读取段 reg 很慢:4 微指令 + 4 微码,6c 吞吐量
P4 Prescott:1 uop + 8 微码。27c 吞吐量。 读取段 reg = 8c 吞吐量。
Pentium M:p0 为 8 uop,与 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 reg (ds/es/fs/gs),所有这些都具有相同的选择器mov
:每 25c 吞吐量四个,6 个融合/未融合域 uops。(也许有些被取消了?)
在一个循环中写入ds
4 次:每 72c 一个迭代(mov ds,eax
每 18c 一个)。相同的 uop 计数:~6 fused 和 unfused per mov
.
这似乎表明 Skylake 不会重命名段 regs:对一个的写入必须在下一次写入开始之前完成。
K7/K8/K10:6 个“ops”,8c 吞吐量。
Atom:7 uop,21c 吞吐量
mov r, sr
)的吞吐量为 0.5 个周期。没有列出延迟,这很奇怪。也许他正在根据您何时可以将其用于负载来测量 seg-write 延迟?喜欢mov eax, [ebx]
/mov ds, eax
在一个循环?有序 Pentium (P5 / PMMX) 具有更便宜的 mov-to-sr:Agner 将其列为需要 ">= 2 个周期",并且不可配对。(P5 是有序的 2 宽超标量,具有一些指令可以一起执行的配对规则)。这对于保护模式来说似乎很便宜,所以也许 2 处于实模式而保护模式大于?我们从他的 P4 表中得知,他当时确实在 16 位模式下测试过东西。
Agner Fog 的微架构指南说 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 中看到段 regs 的WAW 危害,并且 WAR 几乎没有相关性,因为内核不会将用户空间的 DS 用于内核入口点中的任何内存引用。(我认为中断正在序列化,但是通过进入内核syscall
可能仍然有用户空间加载或存储在飞行中但尚未执行。)
在第 2 章中,一般解释了乱序 exec(除 P1 / PMMX 之外的所有 CPU),2.2 寄存器重命名说“可能可以重命名段寄存器”,但 IDK 如果他的意思是某些 CPU 做而一些不做t,或者如果他不确定某些旧 CPU。他没有在 PII/PII 或 Pentium-M 部分中提到 seg reg 重命名,因此我无法告诉您您显然在询问的旧的仅 32 位 CPU。(而且他在 K8 之前没有针对 AMD 的微架构指南部分。)
如果您好奇,可以使用性能计数器自行对其进行基准测试。(有关如何测试阻塞乱序执行的示例,请参阅加载和存储唯一的指令是否被重新排序,以及x86 的 MOV 真的可以“免费”吗?为什么我根本不能重现这个?)了解perf
在 Linux 上使用在微小循环上进行微基准测试的基础知识。
mov
from a segment reg 相对便宜:它只修改 GP 寄存器,CPU 擅长写入 GP 寄存器,寄存器重命名等。Agner Fog 发现它是 Nehalem 上的单个 uop。有趣的事实是,在 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 的 decoded-uop 缓存不是跟踪缓存。但更重要的是,SnB 使用一个物理寄存器文件,而不是在 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 实际执行,表明段寄存器和整数寄存器有单独的专用寄存器用于重命名。我猜想它们和控制寄存器不能重命名,并且只有一个专用寄存器可以重命名源。