指令级别分析:指令指针的含义是什么?

16

当在汇编指令级别对代码进行分析时,考虑到现代CPU不按顺序或顺序执行指令,那么指令指针的位置实际上意味着什么?例如,假设有以下x64汇编代码:

mov RAX, [RBX];         // Assume a cache miss here.
mov RSI, [RBX + RCX];   // Another cache miss.             
xor R8, R8;        
add RDX, RAX;           // Dependent on the load into RAX.
add RDI, RSI;           // Dependent on the load into RSI.

指令指针将在哪个指令上花费大部分时间? 对于所有这些指令,我都能想出很好的论据:

  • mov RAX,[RBX]可能需要100多个周期,因为它是缓存未命中。
  • mov RSI,[RBX + RCX]也需要100多个周期,但可能与前一个指令并行执行。 那么指令指针在其中一个指令上是什么意思呢?
  • xor R8,R8可能会乱序执行,并且会在内存加载完成之前完成,但指令指针可能会停留在此处,直到所有先前的指令也完成。
  • add RDX,RAX生成管道停顿,因为它是实际使用RAX值的指令,在缓慢的缓存未命中加载后才会使用它。
  • add RDI,RSI也会停顿,因为它依赖于对RSI的加载。
2个回答

12

中央处理器(CPU)维护一个虚构,即只有架构寄存器(RAX、RBX等)和特定的指令指针(IP),程序员和编译器都是基于这个虚构来工作。

然而,正如你所指出的,现代 CPU 并不按顺序进行串行执行。在你作为程序员/用户请求 IP 之前,它就像量子物理学中一样,IP 是被执行的一系列指令的波形;所有这些都是为了让处理器以最快的速度运行程序。当你请求当前 IP 时(例如,通过调试器断点或分析器中断),处理器必须重新创建你期望的虚构,因此它会折叠这个波形(所有在途指令),将寄存器值收集回架构名称,并构建一个上下文以执行调试器例程等。

在这种情况下,存在一个 IP,指示处理器应该恢复执行的指令。在乱序执行期间,该指令是尚未完成的最旧指令,尽管在中断时,处理器可能已经获取了远远超过该点的指令。

例如,也许中断指示 mov RSI, [RBX + RCX]; 作为 IP,但是 xor 已经执行并完成了;然而,当处理器在中断后恢复执行时,它将重新执行 xor。


1
你能解释一下硬件性能监测计数器在这种情况下是如何操作的吗?例如,Linux有一个名为“perf”的子系统,它基于PMCs提供统计分析。内核是否只是生成一个高频中断,然后根据您非常好的比喻 - 使IP波函数坍缩并读取PMCs,并将该PMC的当前值分配给当前发现的IP(在波函数坍缩后)?然后重置PMCs并从中断中恢复? - oberstet
当上下文切换发生时,同样的事情会发生吗?操作系统将存储最早未完成的上下文,并且必须重新执行乱序执行的指令? - phuclv
1
是的。上下文切换(通常)依赖于定时器中断。在任何中断上,硬件都会向操作系统提供下一条指令的IP,以便操作系统可以在该指令处恢复进程的执行。 - Brian

2
这是一个很好的问题,但在我进行性能调优的类型中,这并不重要。这并不真正重要,因为你要寻找的是速度上的漏洞。这些是代码执行的需要占用时钟时间而本应更好地避免或不做的事情。例如:
- 浪费I/O时间在DLL中查找实际上不需要查找的资源。
- 在内存分配例程中花费时间制作和释放可以简单地重新使用的对象。
- 在函数中重新计算可以进行记忆化的事情。
... 这只是我想到的一部分。
你最大的敌人是自我恭维的倾向,认为“我不会有意识地写任何漏洞。我为什么要写?”当然,你知道这就是为什么要测试软件的原因。但速度上的漏洞也是如此,如果你不知道如何找到它们,那么你就会假定没有,这就是说“我的代码没有可能加速,除了也许通过使用分析器可以帮助我提高一些操作周期”。
在我半个世纪的经验中,没有一种代码是一开始就不包含速度缺陷的。而且,有一个巨大的乘数效应,每次消除一个速度缺陷都会使其余的更加明显。举个人为的例子,假设 bug A 占时钟时间的 90%,bug B 占 9%。如果你只修复 B,那没什么了不起——代码会快 11%。如果你只修复 A,那很好——它会快 10 倍。但如果你两个都修复了,那就真的很好——它会快 100 倍。修复 A 使 B 变得重要。
因此,在性能调优中最需要的是找到速度缺陷,并不漏掉任何一个。当你完成所有这些后,你可以开始进行周期刮削。

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