Xcode工具中反汇编时间分析的可靠性

10

我使用Instrument的时间分析器对我的代码进行了分析,放大到反汇编部分,以下是其中一部分结果:

Instrument Screenshot

我不会期望一条mov指令占用23.3%的时间,而一条div指令几乎不占用时间。 这让我相信这些结果是不可靠的。 这是真的吗?已知的吗?还是我只是遇到了Instruments的bug?或者有什么选项可以获得可靠的结果?
有没有任何关于这个问题的参考资料?

2
看起来仪器正在经历“滑行” - 即,昂贵指令的时间往往会分散到后续指令中,而不是正确地分配给慢速指令。这可以通过使用英特尔的PEBS(精确采样)来大部分缓解。如果您在汇编中共享整个循环以及“典型”的输入来驱动它,我可以在本地运行一些测试,以查看是否使用各种PEBS设置看到类似的结果。 - BeeOnRope
2
此外,divps 本身不会计算任何次数,因为它不必等待其输入。这些次数适用于必须等待缓慢的 divss 结果的指令。(但这并不是完整的解释;看起来除非您在 Skylake 上,否则应该会瓶颈 divss 吞吐量。而且后面链中的指令有很多计数,不集中在使用 divss 结果的第一条指令上。) - Peter Cordes
1
@BeeOnRope:Andy Glew发布了一个有趣的答案,解释了英特尔P6微架构流水线中性能计数器中断的内部原理,以及为什么(在PEBS之前)它们总是被延迟。 - Peter Cordes
1
@PeterCordes - 是的,我最近读到了这篇文章。需要明确的是,即使今天使用非PEBS的采样方式仍然具有固有的不精确性,因为它仍然依赖于中断和IP检查:即使中断是由导致PMU计数器溢出的指令同步发出的(如果这个概念对于事件来说是清晰定义的话),流水线通常会处于许多指令正在执行、还未退役或者已经退役但未执行等状态。当中断发生时,所有的推测性东西都被丢弃了,你只剩下IP指向最后一个已退役的… - BeeOnRope
1
除此之外,即使使用PEBS,我也不太清楚如何进行精确的“周期”事件。当许多指令同时运行时,如何为指令分配周期?我想一个合理的方法是每个周期将其分配给最老的未退役指令,但在某些情况下,这可能仍会选择不在关键路径上的指令,因此实际上并没有对执行时间做出贡献(但我认为PMU无法解决这个问题)。这很复杂-例如,请参见此处 - BeeOnRope
显示剩余5条评论
2个回答

4
首先,有可能一些真正属于 divss 的计数被算到后续的指令中,这被称为“skid”。(有关更多详细信息,请参见该评论线程的其余部分。)Xcode 可能类似于 Linux 的 perf,并使用固定的 cpu_clk_unhalted.thread 计数器来计算 cycles,而不是可编程计数器之一。这不是一个“精确”的事件(PEBS),因此可能会出现 skids。正如 @BeeOnRope 指出的那样,您可以使用每个周期触发一次的 PEBS 事件(例如 UOPS_RETIRED < 16)作为固定周期计数器的 PEBS 替代品,从而减少对中断行为的依赖。

但是,计数器在流水线/乱序执行中的基本工作方式也可以解释您所看到的大部分内容。或许可以这样说;你没有展示完整的循环,因此我们无法使用像IACA这样的简单流水线模型或手动使用硬件指南(如http://agner.org/optimize/和Intel的优化手册)来模拟代码。(而且你甚至没有说明你有什么微架构。我猜它是Mac上的Intel Sandybridge家族成员。)


cycles计数通常是针对等待结果的指令进行计费,而不是通常较慢生成结果的指令。 流水线CPU在尝试读取尚未准备好的结果之前不会停顿。

乱序执行会使这个问题变得非常复杂,但当有一个非常慢的指令(比如经常错过缓存的加载指令)时,这仍然是普遍适用的。当cycles计数器溢出(触发中断)时,有许多指令正在执行,但只有一个可以与该性能计数器事件相关联的RIP。 这也是中断后执行将恢复的RIP。

那么当引发中断时会发生什么?请参见Andy Glew's answer,其中解释了Intel P6微架构管道中perf-counter中断的内部原理以及为什么(在PEBS之前)它们总是延迟的。Sandybridge系列在这方面类似于P6。

我认为在英特尔CPU上,perf-counter中断的一个合理的心理模型是丢弃尚未分派到执行单元的任何uops。但已经分派的ALU uops将通过管道到达退休(如果没有更年轻的uops被丢弃),而不是被中止,这是有道理的,因为sqrtpd的最大额外延迟约为16个周期,并且清空存储器队列可能需要更长的时间。(已经退休的挂起存储器无法回滚)。对于尚未退休的加载/存储器,我不知道;至少加载可能会被丢弃。
我基于这样一个猜测:当CPU有时在等待divss产生其输出时,很容易构建不显示任何计数的循环。如果它在未退休的情况下被丢弃,那么它将是恢复中断后的下一条指令,因此(除了skids之外),你将看到大量的计数。
因此,cycles计数的分布显示了哪些指令在调度器中是最老的未分派指令所花费的时间最长。(或者在遇到前端停顿时,CPU正在尝试获取/解码/发布哪些指令)。请记住,这通常意味着它向您展示正在等待输入的指令,而不是生产速度慢的指令。

(嗯,这可能不正确,我没有测试过这个。我通常使用perf stat来查看微基准测试中整个循环的总体计数,而不是使用perf record进行统计分析。 addssmulss的延迟高于andps,因此如果我的提议模型正确,您会期望andps获得等待其xmm5输入的计数。)

无论如何,一般问题是,cycles计数器回绕时,有多个指令同时运行,硬件会“责怪”哪一个?


请注意,divss在生成结果时速度较慢,但它只是一个单一uop指令(与整数div不同,后者在AMD和Intel上是微码化的)。如果您没有被其延迟或未完全流水线化的吞吐量所限制,它并不比mulss,因为它可以像周围的代码一样重叠执行。
divss/divps不完全流水线化。例如,在Haswell上,独立的divps每7个周期就能开始一个。但每个运算只需要10-13个周期来生成结果。所有其他执行单元都是完全流水线化的;能够在每个周期上启动对独立数据的新操作。)
考虑一个大循环,瓶颈在吞吐量上,而不是任何循环依赖的延迟,并且每20个FP指令只需要运行一次divss。使用常数除以mulss与倒数常数相比,性能几乎没有区别。(实际上,乱序调度并不完美,即使不是循环传递的情况下,更长的依赖链也会损害一些性能,因为它们需要更多的指令来隐藏所有的延迟并维持最大吞吐量。即乱序核心找到指令级并行性。)

总之,这里的重点是divss是单个uop,根据周围的代码,它不应该获得太多的cycles事件计数。


你会看到缓存未命中加载也有同样的效果:加载本身只有在等待寄存器地址模式时才会计数,而使用加载数据的依赖链中的第一条指令会获得很多计数。

你的个人资料结果可能告诉我们的信息:

  • divss 不必等待其输入准备就绪。(在 divss 之前的 movaps %xmm3, %xmm5 有时需要一些周期,但 divss 永远不需要。)

  • 我们可能接近于瓶颈 divss 的吞吐量。

  • 涉及 divss 后的 xmm5 的依赖链正在获得一些计数。乱序执行必须工作以同时保持多个独立迭代。

  • maxss / movaps 循环传递的依赖链可能是一个重要的瓶颈。(特别是如果你在 Skylake 上,其中 divss 的吞吐量为每 3 个时钟周期 1 个,但 maxss 的延迟为 4 个时钟周期。并且来自端口 0 和 1 竞争的资源冲突将延迟 maxss。)


高计数的 movaps 可能是由于它跟随 maxss,在您展示的循环部分中形成了唯一的循环依赖关系。因此,maxss 确实缓慢产生结果是有道理的。但如果真的是循环依赖关系是主要瓶颈,你会预期看到许多 maxss 的计数,因为它将等待上一次迭代的输入。

但也许 mov-elimination 是“特殊”的,由于某种原因所有计数都被归结到 movaps? 在 Ivybridge 和更高版本的 CPU 上,寄存器复制不需要执行单元,而是在流水线的发布/重命名阶段处理。


有一件事我不确定:已经派遣的ALU uops会通过流水线到达退役而不是被中止。你认为这是为什么呢?这是PMU中断的特殊功能吗?我肯定不认为普通中断会像这样工作:所有正在执行的指令(即尚未退役)都将被丢弃,即使它们已经执行。只有存储缓冲区被保留(因为它具有后退休状态)。我猜测PMU中断不会有所不同。如果您想保留(提交)已经执行的指令,... - BeeOnRope
...你需要执行更多的指令,以便在中断时状态具有单一一致的IP:任何未执行的指令都比最年轻的已执行指令旧。这可能很复杂,而且可能需要大量的工作(如果有100条指令并且它们很慢)。因此,我认为在中断情况下,您正在查看的是IP指向中断发生时最老的未退役指令。因此,我认为您编写“因此,分布...”的部分最可能应该说“最老的未退役指令”,但这是没有经过测试的。 - BeeOnRope
@BeeOnRope:我猜测的依据是当divss不必等待其输入时,它的计数很少。如果它在产生输出时速度较慢并且被丢弃而没有退役,那么应该会因周期而获得大量计数吧?在我的SKL实验中,我看到了同样的情况,其中有一个独立的divss馈送一个循环传递的依赖链。无论如何,我认为已经派遣的指令可能会被允许继续退役如果没有任何年轻的未执行指令。 - Peter Cordes
1
话虽如此,这都是关于非PEBS的。一个好的分析器应该使用PEBS方法来采样周期。perf支持cycles:pcycles:ppp(我认为ppp相同),并使用一个在每个周期都会计数的计数器,例如“UOPS_RETIRED <16”,而PEBS(据我所知)则会用事件的详细信息填充一个单独的缓冲区,因此中断行为并不重要(中断仅需要读取数据本身来自PEBS缓冲区)。 - BeeOnRope
1
是的,老实说我也不完全确定。除了我提到的之外,中断发生的方式可能还有其他的滞后。对于像 divss 这样的长延迟操作,中断可能会延迟,并且当它完成时,如果退役队列为空,则可能立即退役,因此从未出现过?这可能可以通过一些测试来解决 - 但是 PEBS 使中断行为变得相当不重要,因此我不确定是否需要进行深入研究。 - BeeOnRope
显示剩余2条评论

1

这是真的吗?已知吗?

是的,这是在Intel x86上使用分析工具时已知的问题。我观察到了它(时间花费被怀疑地分配给看似无辜的指令),包括Linux perf_events和Intel VTune。其他人也在别处报告过。

更好、更诚实的收集结果可视化将汇总每个基本块内的所有样本,并展示与基本块相关联的结果值,而不是其单个指令。虽然不是100%可靠,但更好且更诚实。

还是说我需要使用某些选项才能获得可靠的结果?

我不知道是否有更新的分析硬件,即基于Intel Processor Trace(从Broadwell开始提供,但在Skylake中得到改进)而不是旧的PEBS,会提供更准确的数据。我想人们需要先尝试使用这些工具进行实验。


Intel PT 看起来非常适合用于基本块的计时,但它不会为块内指令提供细分。按设计,它仅记录分支,但它确实在事件上记录时间戳。因此,它应该非常适合对第一次运行冷缓存效果进行分析,而不仅仅是稳态,因为您在每个分支处都有时间戳日志,而不仅仅是块的所有执行的统计平均值。 - Peter Cordes
“快速”指令的高计数并不总是问题,这是硬件工作方式的预期结果。因此,“问题”在于错误解释分析结果。请参见有关该问题的评论。 - Peter Cordes
VTune和perf与cycles:ppp通常在一般情况下提供非常准确的结果,除非它们在不支持PEBS的某些旧硬件上。使用cycles:p也可以很好地工作,但具有一个“确定性滑移”,因此所有样本都显示在下一条指令上。这也没关系(只要你知道),除了在罕见情况下,样本所在的指令是跳转的目标。 - BeeOnRope

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