当前位置: 首页 > 知识库问答 >
问题:

为什么XCHG reg,reg是现代英特尔体系结构上的3微操作指令?

黄逸清
2023-03-14

我正在对代码中性能关键的部分进行微优化,并遇到了指令序列(在

add %rax, %rbx
mov %rdx, %rax
mov %rbx, %rdx

我想我终于有了一个xchg的用例,它可以让我删除指令并编写:

add  %rbx, %rax
xchg %rax, %rdx

然而,令我惊讶的是,我从Agner Fog的指令表中发现,xchg是一个3微操作指令,在Sandy Bridge、Ivy Bridge、Broadwell、Haswell甚至Skylake上具有2个周期延迟。3个完整的微操作和2个周期的延迟!3个微操作丢弃了我的4-1-1-1节奏,2个周期的延迟使它在最好的情况下比原来的更糟糕,因为原来的最后2条指令可能并行执行

现在我发现CPU可能会将指令分解为微操作,这些微操作相当于:

mov %rax, %tmp
mov %rdx, %rax
mov %tmp, %rdx 

其中,tmp是一个匿名内部寄存器,我想最后两个微操作可以并行运行,因此延迟为2个周期。

然而,考虑到寄存器重命名发生在这些微架构上,我认为这样做没有意义。为什么寄存器重命名器不只是交换标签?理论上,这只有1个周期的延迟(可能为0?)并且可以表示为单个微操作,因此它会便宜得多。

共有1个答案

解修然
2023-03-14

支持高效的xchg是非常重要的,可能不值得为CPU的各个部分增加额外的复杂性。一个真正的CPU的微体系结构比你在为它优化软件时可以使用的心智模型要复杂得多。例如,推测执行使一切变得更加复杂,因为它必须能够回滚到发生异常的点。

使fxch高效对于x87性能很重要,因为x87的堆栈特性使其(或fld st(2)等替代方案)难以避免。编译器生成的FP代码(对于没有SSE支持的目标)确实使用了大量的fxch。似乎快速fxch是因为它很重要,而不是因为它很容易。Intel Haswell甚至放弃了对单uopfxch的支持。它仍然是零延迟,但在HSW及更高版本上解码为2 uops(从P5中的1上升到PPro通过IvyBridge)。

xchg通常很容易避免。在大多数情况下,您可以展开一个循环,因此相同的值现在在不同的寄存器中是可以的。例如Fibonacci withadd rax, rdx/add rdx, rax而不是add rax, rdx/xchg rax, rdx。编译器通常不使用xchg reg, reg,通常手写的ash也不使用。(这个鸡/蛋问题非常类似于循环很慢(为什么循环指令很慢?英特尔不能有效地实现它吗?)。循环对于Core2/Nehalem上的adc循环非常有用,其中adcdec/jnz循环导致部分标志停顿。)

由于xchg在以前的CPU上仍然很慢,编译器在几年内都不会将其与通用CPU一起使用。与fxch或mov消除不同,支持fast的设计更改不会帮助CPU更快地运行大多数现有代码,并且只会在极少数情况下提高性能,因为这实际上是一种有用的窥视孔优化。

有4个操作数大小为xchg,其中3个使用相同的操作码,前缀为REX或操作数大小。(xchg r8,r8是一个单独的操作码,因此可能更容易让解码器对其进行不同的解码)。解码器已经必须将内存操作数的xchg识别为特殊操作数,这是因为有隐式的lock前缀,但如果reg-reg对不同操作数大小的相同数量的uop进行解码,则解码器的复杂性(晶体管计数功率)可能会降低。

让一些r, r表单解码为单个uop会更加复杂,因为单uop指令必须由“简单”解码器和复杂解码器处理。所以他们都需要能够解析xchg并决定它是单个uop还是多uop表单。

程序员的角度来看,AMD和Intel CPU的行为有些相似,但有许多迹象表明,内部实现有很大不同。例如,受某种微体系结构资源的限制,Intel mov消除只在部分时间有效,但执行mov消除的AMD CPU在所有时间都能做到这一点(例如,vector Reg低端的推土机)。

参见英特尔的优化手册,示例3-23。重新排序序列以提高零延迟MOV指令的有效性,其中他们讨论了立即覆盖零延迟-movzx结果以更快地释放内部资源。(我尝试了Haswell和Skylake上的示例,发现mov消除实际上在执行此操作时确实工作得更多,但它实际上在总周期中略慢,而不是更快。该示例旨在展示IvyBridge的好处,它可能会在其3个ALU端口上遇到瓶颈,但HSW/SKL仅在dep链中的资源冲突上遇到瓶颈,并且似乎不会因为需要ALU端口来执行更多movzx指令而感到烦恼。)

我不知道在有限大小的表中需要跟踪什么(?)以消除移动。可能与不再需要寄存器文件条目时需要尽快释放它们有关,因为物理寄存器文件大小限制而不是ROB大小可能是无序窗口大小的瓶颈。围绕索引交换可能会使这变得更难。

xor-在Intel Sandybridge系列上100%消除归零;假设这是通过重命名为物理零寄存器来工作的,并且该寄存器永远不需要释放。

如果xchg使用与mov消除相同的机制,那么它也可能只在某些时候起作用。它需要解码到足够的UOP,以便在重命名时不处理它的情况下工作。(否则,当一个xchg需要超过1个uop时,发布/重命名阶段将不得不插入额外的uop,就像将微熔合uop与索引寻址模式(无法在ROB中保持微熔合)进行分层一样,或者为标志或高8部分寄存器插入合并uop。但这是一个非常复杂的操作,只有当xchg常见且重要的说明。)

请注意,xchg r32, r32必须将两个结果都扩展为零到64位,因此它不能是RAT(寄存器别名表)条目的简单交换。这更像是就地截断两个寄存器。请注意,英特尔CPU永远不会消除mov相同,相同。它确实已经需要支持mov r32, r32movzx r32, r8没有执行端口,因此大概它有一些位指示rax=al或其他东西。(是的,英特尔HSW/SKL做到了这一点,而不仅仅是Ivybridge,尽管Agner的微拱指南说。)

我们知道P6和SnB有这样的高位零位,因为xor eax,eax在读取eax时避免了部分寄存器暂停。HSW/SKL一开始从不单独重命名al,只重命名ah。部分寄存器重命名(AH除外)似乎在引入mov消除(Ivybridge)的同一uarch中被丢弃,这可能不是巧合。不过,同时为2个寄存器设置该位将是一种特殊情况,需要特殊支持。

xchg r64,r64可能只是交换RAT条目,但解码与r32不同是另一个复杂问题。它可能仍然需要为两个输入触发部分寄存器合并,但添加r64、r64也需要这样做。

另请注意,Intel uop(fxch除外)只产生一个寄存器结果(加上标志)。不接触标志不会“释放”输出槽;例如mulx r64, r64, r64仍然需要2个uops才能在HSW/SKL上产生2个整数输出,即使所有“工作”都在端口1上的乘法单元中完成,与mul r64一样,它确实会产生标志结果。)

即使它像“交换RAT条目”一样简单,构建一个支持每个uop写入多个条目的RAT也是一个复杂的问题。在单个问题组中重命名4个xchguops时该怎么办?在我看来,这似乎会使逻辑变得更加复杂。请记住,这必须由逻辑门/晶体管构建。即使你说“用微码陷阱处理那个特殊情况”,你也必须构建整个管道,以支持管道阶段可能出现这种异常的可能性。

单uop fxch需要支持在FP RAT(fRAT)中交换RAT条目(或其他机制),但它是一个独立于整数RAT(iRAT)的硬件块。即使你在兄弟会(哈斯韦尔之前)也有这种复杂情况,在iRAT中省略这种复杂情况似乎是合理的。

不过,问题/重命名复杂性肯定是功耗的一个问题。请注意,Skylake扩展了许多前端(旧版解码和uop缓存获取)和退休,但保留了4个范围的问题/重命名限制。SKL还在后端的更多端口上添加了复制的执行单元,因此问题带宽在更多情况下是一个瓶颈,尤其是在混合加载、存储和ALU的代码中。

RAT(或整数寄存器文件IDK)甚至可能具有有限的读取端口,因为在发布/重命名许多3输入UOP时似乎存在一些前端瓶颈,如添加rax、[rcx rdx]。我发布了一些微基准点(这篇文章和后续文章),显示Skylake在读取大量寄存器时比Haswell快,例如索引寻址模式的微融合。或者可能是因为存在其他微体系结构限制的瓶颈。

但是1-uop fxch是如何工作的呢?IDK Sandybridge/Ivybridge的做法。在P6系列CPU中,基本上存在一个额外的重新映射表来支持FXCH。这可能只是因为P6使用一个失效寄存器文件,每个“逻辑”寄存器有一个条目,而不是物理寄存器文件(PRF)。正如您所说,当“冷”寄存器值只是指向PRF条目的指针时,您希望它更简单。(来源:美国专利5499352:浮点寄存器别名表FXCH和失效浮点寄存器阵列(描述Intel的P6 uarch)。

rfRAT阵列802包括在本发明fRAT逻辑中的一个主要原因是本发明实现FXCH指令的方式的直接结果。

(谢谢安迪·格鲁(@krazyglew),我没有想过要查阅专利来了解中央处理器的内部结构。)这是相当繁重的工作,但可能会为投机性执行所需的簿记提供一些见解。

有趣的花絮:该专利也描述了整数,并提到有一些“隐藏”的逻辑寄存器保留供微码使用。(Intel的3-uop xchg几乎肯定会使用其中一种作为临时工具。)

有趣的是,AMD在K10、推土机系列、山猫/捷豹和Ryzen中拥有2-uop xchg r、r。(但捷豹xchg r8、r8是3个uop。可能是为了支持xchg ah、al角盒,而无需特殊uop来交换单个reg的低16)。

大概两个uop都在第一个更新RAT之前读取输入架构寄存器的旧值。IDK到底是如何工作的,因为它们不一定在同一个周期中发布/重命名(但它们至少在uop流中是连续的,所以最坏的情况是第二个uop是下一个周期中的第一个uop)。我不知道Haswell的2-uopfxch是否工作类似,或者他们是否在做其他事情。

Ryzen是在mov消除被“发明”之后设计的一种新架构,所以他们可能会尽可能利用它。(推土机系列重命名矢量移动(但仅适用于YMM矢量的低128b车道);Ryzen也是第一个为GP regs实现此功能的AMD架构。)<代码>xchg r32、r32和r64、r64都是零延迟(重命名),但仍各有2个UOP。(r8和r16需要一个执行单元,因为它们与旧值合并,而不是零扩展或复制整个reg,但仍然只有2个UOP)。

Ryzen的fxch为1 uop。AMD(与Intel一样)可能不会在x87的快速制造上花费太多晶体管(例如,fmul仅为1个时钟,并且与fadd位于同一端口),因此他们大概可以在没有太多额外支持的情况下实现这一点。他们的微代码x87指令(如fyl2x)比最近的Intel CPU更快,因此Intel可能更不关心(至少是微代码x87指令)。

或许AMD也可以比Intel更容易地制造出xchg r64、r64单uop。甚至可能xchg r32,r32也可以是单uop,因为与Intel一样,它需要支持无执行端口的mov r32,r32零扩展,所以可能它可以设置任何存在的“高位32零”位来支持它。Ryzen并没有在重命名时消除movzx r32,r8,所以可能只有一个upper32零位,而不是其他宽度的位。

Intel可能会像Ryzen那样支持2-uopxchg r, rr32, r32r64, r64表单的零延迟,或r8, r8r16, r16表单的1c),而在核心的关键部分没有太多额外的复杂性,例如管理寄存器别名表(RAT)的问题/重命名和退休阶段。但也许不是,如果他们不能让2个uop在第一个uop写入寄存器时读取它的“旧”值。

像xchg-ah,al这样的东西肯定是一个额外的麻烦,因为英特尔CPU不再单独重命名部分寄存器,除了ah/BH/CH/DH。

你对其内部工作原理的猜测是好的。几乎可以肯定的是,它使用了一个内部临时寄存器(只能由微码访问)。不过,你对他们如何重新订购的猜测太有限了。事实上,一个方向有2c延迟,另一个方向有大约1c延迟。

00000000004000e0 <_start.loop>:
  4000e0:       48 87 d1                xchg   rcx,rdx   # slow version
  4000e3:       48 83 c1 01             add    rcx,0x1
  4000e7:       48 83 c1 01             add    rcx,0x1
  4000eb:       48 87 ca                xchg   rdx,rcx
  4000ee:       48 83 c2 01             add    rdx,0x1
  4000f2:       48 83 c2 01             add    rdx,0x1
  4000f6:       ff cd                   dec    ebp
  4000f8:       7f e6                   jg     4000e0 <_start.loop>

该循环在Skylake上每次迭代运行约8.06个周期。反转xchg操作数可以使它在每次迭代中以大约6.23c的周期运行(在Linux上使用perf stat进行测量)。UOP发出/执行的计数器相等,因此未发生消除。它看起来像dst

如果您想在关键路径上使用xchg reg,reg(代码大小原因?),使用dst执行此操作-

3个微操作使我的4-1-1-1节奏变慢

Sandybridge系列解码器不同于Core2/Nehalem。它们总共可以产生4个UOP,而不是7个,因此模式是1-1-1-1、2-1-1、3-1或4。

还要注意,如果最后一个uop是可以宏融合的,他们将一直保留到下一个解码周期,以防下一个块中的第一条指令是jcc。(当代码每次解码时从uop缓存运行多次时,这是一个胜利。通常每个时钟解码吞吐量仍为3 uops。)

Skylake有一个额外的“简单”解码器,所以它可以做1-1-1-14-1我猜,但是

我真的在寻找大约1%的减速带,所以手动优化一直在主循环代码上进行。不幸的是,这大约是18kB的代码,所以我甚至不再考虑uop缓存了。

这似乎有点疯狂,除非您主要将自己限制在主循环中较短循环中的asm级优化。主循环中的任何内部循环仍将从uop缓存运行,这可能应该是您花费大部分时间进行优化的地方。编译器通常做得足够好,以至于人类无法大规模完成太多工作。当然,尝试以这样一种方式编写您的C或C,以便编译器可以很好地使用它,但是在18kB的代码中寻找这样的微小窥视孔优化似乎就像掉进兔子洞。

使用性能计数器,如idq。dsb\U uops与发布的uops相比。查看有多少uop来自uop缓存(DSB=解码流缓冲区或其他)。英特尔的优化手册对其他性能计数器提出了一些建议,以查找不适合uop缓存的代码,例如DSB2MITE\U开关。惩罚周期。(MITE是传统解码路径)。在pdf中搜索DSB以查找其中提到的几个位置。

Perf计数器将帮助您找到具有潜在问题的点,例如uops_issued.stall_cycles高于平均水平的区域可以从找到暴露更多ILP的方法(如果有的话)、解决前端问题或减少分支错误预测中受益。

正如评论中所讨论的,单个uop最多产生1个寄存器结果

另一方面,对于mul%rbx,您真的可以同时获得rdx和rax,还是ROB技术上可以比较高部分提前一个周期访问结果的较低部分?或者就像“mul”uop进入乘法单元,然后乘法单元直接向ROB发出两个uop,以便在最后写入结果?

术语:乘法结果不会进入ROB。它通过转发网络到达任何其他uops读取它,并进入PRF。

mul%rbx指令在解码器中解码为2 uops。它们甚至不必在同一周期中发出,更不用说在同一周期中执行了。

然而,Agner Fog的指令表只列出了一个延迟数。结果表明,3个周期是从两个输入到RAX的延迟。根据在Haswell和Skylake-X上进行的Inslatx64测试,RDX的最小延迟为4c。

由此,我得出结论,第二个uop依赖于第一个uop,它的存在是为了将结果的高半部写入架构寄存器。port1 uop生成完整的128b乘法结果。

在p6 uop读取它之前,我不知道高半结果在哪里。也许在乘法执行单元和连接到端口6的硬件之间存在某种内部队列。通过对低半结果进行依赖的p6 uop调度,这可能会安排来自多个飞行中的mul指令的p6 uop以正确的顺序运行。但是,uop将从连接到端口6的执行单元中的队列输出中获取高半结果,并将其作为结果返回。(这纯粹是猜测工作,但我认为这是一种可能的内部实现。一些早期的想法见评论)。

有趣的是,根据Agner Fog的指令表,在Haswell上,mul r64的两个UOP转到端口1和6<代码>mul r32是3个UOP,在p1 p0156上运行。Agner没有说这是真的2p1 p0156还是像他对其他INSN所做的那样。(然而,他说mulx r32、r32、r32在p1 2p056上运行(注意p056不包括p1)。)

更奇怪的是,他说Skylake在p1p5上运行mulx r64、r64、r64,但在p1p6上运行mul r64。如果这是准确的,而不是拼写错误(这是可能的),那么几乎排除了额外uop是上半乘数的可能性。

 类似资料:
  • 问题内容: 从我读到的内容来看,它用于修复CPU中的错误,而无需修改BIOS。根据我对汇编的基本知识,我知道汇编指令在内部由CPU分解为微代码,并相应地执行。但是intel以某种方式可以在系统启动和运行时进行一些更新。 有人有更多信息吗?是否有关于微码可以做什么以及如何使用的文档? 编辑:我读过维基百科的文章:没弄清楚我怎么能自己写一些,以及它有什么用。 问题答案: 在较早的时期,微代码在CPU中

  • 第3.5.1节下的英特尔优化参考建议: “支持单个微操作指令。” "避免使用具有超过4个微操作并且需要多个周期来解码的复杂指令(例如,回车、离开或循环)。改用简单指令序列。" 虽然英特尔自己告诉编译器编写者使用解码为几个微操作的指令,但我在他们的任何手册中都找不到任何解释每个ASM指令解码为多少微操作的说明!这些信息在任何地方都可以找到吗?(当然,我预计不同代CPU的答案会有所不同。)

  • 冯·诺依曼体系结构 计算机处理的数据和指令一律用二进制数表示 顺序执行程序 计算机运行过程中,把要执行的程序和处理的数据首先存入主存储器(内存),计算机执行程序时,将自动地并按顺序从主存储器中取出指令一条一条地执行,这一概念称作顺序执行程序。 计算机硬件由运算器、控制器、存储器、输入设备和输出设备五大部分组成。 数据的机内表示 二进制表示 机器数 由于计算机中符号和数字一样,都必须用二进制数串来表

  • 伙计们,最近我决定回到PHP,做一些比简单的登录页面更复杂的事情。3年来,我一直在Java /JavaEE编程,对Java应用程序的体系结构有很好的理解。基本上,一个虚拟机(一个简单的操作系统进程)运行被称为字节码的编译代码。一个简单的Javaweb服务器基本上是一个java应用程序,它监听提供给Http请求的TCP端口,并相应地做出响应,当然它更复杂但这是它最初的工作。 现在,PHP怎么样?它是

  • 现代WebGIS是现代Web技术在GIS中的应用。现代WebGIS的体系结构与其他现代Web项目的体系结构没有太多本质上的区别,唯一不同的是WebGIS需要提供一些地图方面的功能服务,即:GIS服务资源。 图1-1 现代WebGIS体系结构 如图1-1所示,现代WebGIS底层是数据层,提供空间数据与业务数据等基础数据支撑;中间层一般包括提供基础GIS服务的GIS服务器和提供应用服务支撑的业务逻辑

  • 本文向大家介绍javascript中的ribs.js的体系结构是什么?,包括了javascript中的ribs.js的体系结构是什么?的使用技巧和注意事项,需要的朋友参考一下 BackboneJS为Web应用程序提供了一种结构,该结构允许分离业务逻辑和用户界面逻辑。 BackboneJS的体系结构包含以下模块- HTTP请求 HTTP客户端以请求消息的形式向服务器发送HTTP请求,其中Web浏览器