如何找到在英特尔x86处理器上解码为微操作的指令?

21

根据Intel优化参考手册第3.5.1节指出:

"青睐单微操作指令。"

"避免使用需要多个周期解码且有超过4个微操作的复杂指令(例如enter、leave或loop)。改用一系列简单的指令代替。"

虽然Intel告诉编译器编写者使用解码为少量微操作的指令,但我在他们的任何手册中都找不到任何解释每个ASM指令解码为多少微操作的信息!这些信息是否可在其他地方获得?(当然,我期望回答会因不同的CPU代而有所不同。)


6
你不能这样做,因为它们是商业机密,并且每个微架构都会有所变化。当前的趋势是通过“融合”使它们再次更像CISC。Agner Fog的指令表文档是一个非常不错的资源。 - Hans Passant
@HansPassant,看起来你链接的指令表是最好的参考资料。你想把它作为答案让我接受吗? - Alex D
可能是为什么英特尔在其处理器中隐藏内部RISC核心?的重复问题。 - Ciro Santilli OurBigBook.com
一个是关于版本的介绍:https://dev59.com/0W855IYBdhLWcg3wZzXf,另一个则是如何操作它们的版本:http://security.stackexchange.com/questions/29730/processor-microcode-manipulation-to-change-opcodes。 - Ciro Santilli OurBigBook.com
1
这些都不是与我的问题重复的。我问的是完全不同的事情。 - Alex D
4个回答

14

Agner Fog的PDF 文件 关于x86指令(链接到Hans引用的主页面)是我发现的唯一指令时序和微操作的参考资料。我从未看过Intel关于微操作细节的文件。


14

除了其他答案中提到的资源(Agner Fog的表格IACA)外,您还可以在我们的网站uops.info上找到有关大多数x86指令μops的详细信息(Intel CPU从Nehalem到Cannon Lake)。该网站还包含每个指令的延迟和吞吐量信息。数据是通过在实际硬件上(使用硬件性能计数器)以及在不同版本的Intel IACA的顶部运行自动生成的微基准测试获得的。

与Agner Fog的指令表相比,uops.info上的数据在多个情况下更准确。以Nehalem上的PBLENDVB指令为例,根据Agner Fog的表格,该指令有一个μop只能使用端口0,一个μop只能使用端口5。这可能是基于观察单独执行该指令时,平均有一个μop在端口0上,一个μop在端口5上的结果。在uops.info上的微基准测试显示,实际上两个μops都可以使用端口0和端口5。这是通过将该指令与只能使用端口0或端口5的指令一起执行来确定的。 uops.info上的数据还揭示了Intel的IACA中存在几个不准确之处。例如,在Skylake上,CVTPI2PS XMM,MM指令的两个μops在IACA中只能使用端口0(http://uops.info/html-ports/SKL/CVTPI2PS_XMM_MM-IACA3.0.html)。在实际硬件上,有一个μop只能使用端口0,另一个μop可以使用端口0和端口1。Agner Fog也观察到该指令的一个μop可以使用端口1;然而,他声称该μop只能使用端口1,这是不正确的。

1
Agner的旧式表格,每个端口都有单独的列,我认为只是不能区分p0 p52p05之间的区别。我认为这并不是错误,而是缺乏足够的细节来告诉您pblendvbp0 p5(2个单端口uop)还是2p05(2个uop可以在任何一个端口上运行)。但他的新表格确实使用了更好的格式,可以区分它们。 - Peter Cordes
但他区分了在列中有数字和有“x”的情况。我的理解是,p0和p5两列都有1意味着p0 p5,而两列都有“x”则意味着2p05 - Andreas Abel
1
哦,你说得对。我原本以为他只是在单uop指令中放置了“x”来表示它是1或另一个,但他也会在其他一些指令中有更多的uops时这样做。Agner的表格肯定不是没有错误的,而且,仅测量*该指令的重复块并不总是足够的。例如,对于shl r32,cl:https://www.agner.org/optimize/blog/read.php?i=415#860。您的https://uops.info/测试看起来非常好,感谢您发布(和创建)它。我在SO的https://stackoverflow.com/tags/x86/info标签wiki中添加了一个链接。 - Peter Cordes

5

已经指出Agner Fog的优化手册是一个很好的资源,特别是他的指令表,几乎涵盖了所有感兴趣的x86微架构。

但是你还有另一个选择: Intel的体系结构代码分析器(IACA)。这里有一篇关于如何使用它的文章Stack Overflow,但它很容易上手(虽然对于一次性分析来说有点乏味)。你只需要下载可执行文件,在要分析的指令块周围发出一些序言和尾声代码(它包括一个用于此目的的C头文件(iacaMarks.h),适用于各种编译器,或者你可以指示汇编器发出适当的字节),然后运行二进制文件通过iaca.exe。当前版本(v2.2)仅支持64位二进制文件,但这不是一个主要限制,因为指令级分析在32位和64位模式下不会有实质性的区别。当前版本还支持所有现代英特尔微架构,可能会引起专业软件开发人员的兴趣,从 Nehalem 到 Broadwell。
从这个工具获得的输出将告诉您哪些端口可以执行特定的指令,以及在指定的微架构中该指令将分解成多少µops。
这就是你能得到的最接近直接回答的答案,因为正如Hans Passant在评论中指出的那样,每个指令分解成的确切µops是由英特尔故意保密的。它们不仅是专有的商业秘密,而且英特尔希望能够自由地从一个微体系结构更改其工作方式。事实上,当优化代码时,你只需要知道一个指令分解成了多少个µops,无论它分解成了哪些µops都无关紧要。
但我想重申Peter Cordes's answer中的一部分:“虽然在某些情况下很容易猜测”。如果您必须为考虑的每个指令查找此类型的详细信息,那么您将浪费很多时间。而且,如您所知,它因微体系结构而异,这也会让您发疯。真正的诀窍在于对x86 ISA中的哪些指令是“简单的”和哪些是“复杂的”获得直觉感。从阅读文档中应该很明显,并且这种直觉感实际上是英特尔优化建议推动您朝着正确方向前进的全部。避免使用“复杂的”(旧的CISC风格)指令,例如LOOPENTERLEAVE等。例如,选择DEC+JNZ而不是LOOP。相对而言,“经典”的x86指令只有少数几个解码为超过一个或两个µop。*研究良好优化编译器的输出也将引导您朝着正确的方向前进,因为您永远不会看到编译器使用这些“复杂”的指令。
有些与Peter的回答相反,我非常确定Intel优化手册中引用的部分不是指SIMD指令。他们谈论的是实现在微代码中的老式CISC指令,如果不必支持它们以实现向后兼容性,他们早就放弃了。如果需要SSE3的HADDPS行为,则最好使用HADDPS而不是尝试将其拆分为“更简单”的组件。(当然,除非您可以通过在无关代码中交错它们来更好地调度这些操作。但在实践中这很难做到。)
*要完全准确,有些看似简单的指令实际上是使用微码实现并分解为多个µops。64位除法(DIV)就是一个例子。如果我没记错,这是使用大约30-40个µops(变量)的微码编码。然而,这不是您应该避免的指令,这表明英特尔手册在这里提供建议时非常通用。如果需要进行除法运算,请使用DIV。当优化速度时,显然最好不要进行除法运算,但也不要尝试编写自己的除法算法来避免微码编码的DIV 另一个重要的例外是字符串指令。对于它们的性能计算比“避免因为它们解码为多个µops”更加复杂。

幸运的是,有一件事很简单:永远不要使用没有 REP 前缀的字符串指令。这毫无意义,而且通过将指令“分解”为更简单的“组件”指令,例如 MOVSBMOV AL, [ESI]+MOV ES:[EDI], AL+INC/DEC ESI+INC/DEC EDI,您将获得更好的性能。

当您开始利用 REP 前缀时,决定变得有点棘手。虽然这确实会导致指令解码成许多 µops,但有时仍然比手动编写循环更有效率。但并非总是如此。关于这个问题已经在 Stack Overflow 和其他地方进行了大量讨论;例如,请参见 this question

我的快速经验法则是,你可以完全忽略REP LOADSREP SCASREP CMPS。另一方面,REP MOVSREP STOS需要重复相当多次时非常有用。始终使用尽可能大的字大小:32位时使用DWORD,64位时使用QWORD(但请注意{{link2:在现代处理器上,您最好使用MOVSB/STOSB,因为它们可以在内部移动更大量的数据}})。即使满足所有这些条件,如果目标具有矢量指令可用,则可能要验证使用矢量移动/存储是否更快。

另请参见Agner Fog第150页的通用建议


是的,如果您需要haddps所做的一切,您应该使用它(例如在转置和加法中)。但是,如果您将其作为简单水平求和的一部分两次使用相同的输入,例如haddps xmm0,xmm0,则会比使用pshufdvshufps进行复制和洗牌设置以进行addps,造成2个Shuffle uops成本。在x86上执行水平浮点向量求和的最快方法。在循环之外,这并没有太大的区别,但这与x86代码大小有关,而不是uop-cache占用量。 - Peter Cordes

1
Agner Fog的指令表显示微操作运行在哪个端口上,这是性能所关注的。它并不显示每个uop确切的操作,因为这是无法逆向工程的。(即在该端口上使用哪个执行单元)。
在某些情况下很容易猜测:Haswell上的haddps是一个端口的1个uop,和端口5的2个uops。这显然是2次洗牌(端口5)和一个FP-add(端口1)。端口5还有许多其他的执行单元,例如向量布尔、SIMD整数加法和许多标量整数等,但考虑到haddps需要多个uops,显然英特尔用洗牌和常规的“垂直”添加uop来实现它。

可能可以确定这些微操作之间的依赖关系(例如,是两个类似shufps的洗牌操作 feeding 一个FP加法器,还是shuffle-add-shuffle?)。我们也不确定这些洗牌操作是否彼此独立:Haswell只有一个洗牌端口,因此即使它们是独立的,资源冲突也会导致总延迟为5c,因为即使它们是独立的,这些洗牌操作也不能并行运行。

两个洗牌微操作可能都需要两个输入,因此即使它们彼此独立,较早准备好一个输入并不能提高关键路径(从较慢的输入到输出)的延迟。

如果能够使用两个独立的单输入洗牌实现HADDPS,那么在循环中进行HADDPS xmm0,xmm1,其中xmm1是一个常量,将仅向涉及xmm0的dep链添加4c的延迟。我没有测量过,但我认为这是不可能的;几乎肯定是两个独立的双输入洗牌 feeding 一个ADDPS微操作。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接