什么可能导致相同的 SSE 代码在相同的函数中 运行 慢几倍?

What might cause the same SSE code to run a few times slower in the same function?

编辑 3: 图片是全尺寸版本的链接。抱歉,图片是文字,但图表很难 copy/paste 成文本 table。


我有以下针对使用 icc --std=c++14 -qopenmp -axS -O3 -fPIC 编译的程序的 VTune 配置文件:

在该配置文件中,两个指令集在程序集视图中突出显示。尽管指令相同且顺序相同,但上部集群比下部集群花费的时间要少得多。两个集群都位于同一个函数内,显然都被调用了 n 次。每次我 运行 在我现在使用的 Westmere Xeon 和 Haswell 笔记本电脑上使用分析器时都会发生这种情况(使用 SSE 编译,因为这是我现在的目标和学习目标)。

我错过了什么?

忽略较差的并发性,这很可能是笔记本电脑节流造成的,因为它不会出现在桌面 Xeon 机器上。

我认为这不是微优化的例子,因为这三个加在一起占总时间的相当大的百分比,我真的很想知道这种行为的可能原因。

编辑: OMP_NUM_THREADS=1 taskset -c 1 /opt/intel/vtune...

相同的配置文件,尽管这次 CPI 略低。

嗯,分析汇编代码请注意 运行 时间归于下一条指令 - 因此,您需要仔细解释指令所查看的数据。 VTune Release Notes中有对应的注释:

Running time is attributed to the next instruction (200108041)

To collect the data about time-consuming running regions of the target, the Intel® VTune™ Amplifier interrupts executing target threads and attributes the time to the context IP address.

Due to the collection mechanism, the captured IP address points to an instruction AFTER the one that is actually consuming most of the time. This leads to the running time being attributed to the next instruction (or, rarely to one of the subsequent instructions) in the Assembly view. In rare cases, this can also lead to wrong attribution of running time in the source - the time may be erroneously attributed to the source line AFTER the actual hot line.

In case the inline mode is ON and the program has small functions inlined at the hotspots, this can cause the running time to be attributed to a wrong function since the next instruction can belong to a different function in tightly inlined code.

HW perf 计数器通常会对必须等待其输入的指令充电,而不是产生输出速度较慢的指令。

第一组的输入来自您的收集。这可能会丢失很多缓存,并且不会向那些 SUBPS/MULPS/ADDPS 指令收取费用。它们的输入直接来自 voxel[] 的矢量负载,因此存储转发失败会导致一些延迟。但这只是大约 10 个周期的 IIRC,与收集期间的缓存未命中相比很小。 (那些缓存未命中在您突出显示的第一组之前的指令显示为大条)

第二组的输入直接来自缓存中可能丢失的负载。在第一组中,缓存未命中负载的直接消费者是像设置 voxel[0] 这样的行的指令,它有一个非常大的条。

但是在第二组中,a_transfer[] 中缓存未命中的时间归因于您突出显示的组。或者,如果不是缓存未命中,那么可能是地址计算速度慢,因为加载必须等待 RAX 准备就绪。


看起来有很多你可以在这里优化.

  • 而不是 a_pointf 的 store/reload,只需在 __m128 变量中跨循环迭代保持热。 C 源代码中的 Storing/reloading 仅当您发现编译器在选择要溢出的向量寄存器时做出错误的选择(如果它 运行 溢出寄存器)才有意义。

  • _mm_cvttps_epi32(vf) 计算 vi,因此 ROUNDPS 不是收集索引的依赖链的一部分。

  • voxel 通过将窄负载洗牌到向量中来收集自己,而不是编写复制到数组然后从中加载的代码。 (gua运行teed 存储转发失败,请参阅 Agner Fog's optimization guides and other links from the 标签 wiki)。

    部分矢量化地址数学(base_0 的计算,使用 PMULDQ with a constant vector)可能是值得的,因此您只需 store/reload(~5 周期延迟)有一个或两个 MOVQ(Haswell 大约有 1 或 2 个周期延迟,我忘记了。)

    使用MOVD加载两个相邻的short值,并用PINSRD将另一对合并到第二个元素中。您可能会从 _mm_setr_epi32(*(const int*)base_0, *(const int*)(base_0 + dim_x), 0, 0) 获得好的代码,除了指针别名是未定义的行为。您可能会从 _mm_setr_epi16(*base_0, *(base_0 + 1), *(base_0 + dim_x), *(base_0 + dim_x + 1), 0,0,0,0).

    得到更糟糕的代码

    然后用PMOVSX将低4位16位元素展开为32位元素整数,并与_mm_cvtepi32_ps (CVTDQ2PS)并行全部转换为float

  • 您的标量 LERP 未被自动矢量化,但您正在并行执行两个(并且可能会保存一条指令,因为无论如何您都希望将结果放入矢量中)。

  • 调用 floorf() 很愚蠢,函数调用会强制编译器将所有 xmm 寄存器溢出到内存中。用 -ffast-math 或任何让它内联到 ROUNDSS 的东西进行编译,或者手动进行。特别是因为你继续将你计算出的浮点数加载到一个向量中!

  • 使用向量比较而不是标量 prev_x / prev_y / prev_z。使用 MOVMASKPS 将结果转换为您可以测试的整数。 (你只关心较低的 3 个元素,所以用 compare_mask & 0b0111 测试它(如果设置了 4 位掩码的任何低 3 位,在与 _mm_cmpneq_ps 进行不等于比较之后. 请参阅指令的 double 版本以获取有关其工作原理的更多表格:http://www.felixcloutier.com/x86/CMPPD.html).