AVX指令中寄存器和指针之间的客观差异

时间:2018-10-08 23:13:42

标签: c++ performance avx opcode

方案:您正在使用SIMD编写复杂的算法。使用了几个常数和/或不经常更改的值。最终,该算法最终使用了16个以上的ymm,导致使用了堆栈指针(例如,操作码包含vaddps ymm0,ymm1,ymmword ptr [...]而不是vaddps ymm0,ymm1,ymm7)。

为了使算法适合可用的寄存器,可以将常量“内联”。例如:

const auto pi256{ _mm256_set1_ps(PI) };
for (outer condition)
{
    ...
    const auto radius_squared{ _mm256_mul_ps(radius, radius) };
    ...
    for (inner condition)
    {
        ...
        const auto area{ _mm256_mul_ps(radius_squared, pi256) };
        ...
    }
}

...变成...

for (outer condition)
{
    ...
    for (inner condition)
    {
        ...
        const auto area{ _mm256_mul_ps(_mm256_mul_ps(radius, radius), _mm256_set1_ps(PI)) };
        ...
    }
}

所讨论的一次性变量是常量还是不经常计算(计算出的外部循环),如何确定哪种方法可获得最佳吞吐量?是否存在“ ptr增加2个额外延迟”之类的概念?还是不确定性,以至于每个案例都不同,并且只能通过反复试验+剖析才能完全优化?

1 个答案:

答案 0 :(得分:3)

一个好的优化编译器应该为两个版本生成相同的机器代码。只需将向量常量定义为局部常量,或匿名使用它们即可最大程度地提高可读性;让编译器不必担心寄存器分配,并选择最便宜的方法来处理寄存器用尽的情况。

您最好的帮助编译器的方法是,如果可能的话,使用更少的不同常量。例如而不是同时使用_mm_and_si128set1_epi16(0x00FF)的{​​{1}},请使用0xFF00来掩盖另一种方式。通常,您不能做任何事情来影响它选择保留在寄存器中的内容,而不是影响它们,但是幸运的是,编译器在此方面非常擅长,因为它对于标量代码也必不可少。


编译器会将常量提升到循环之外(甚至内联包含常量的辅助函数),或者如果仅在分支的一侧使用,则将设置带入分支的那一侧。

源代码计算的是完全相同的东西,没有明显的副作用,因此,按条件规则允许编译器自由地执行此操作。


我认为编译器通常会在执行CSE(常见子表达式消除)并确定可提升的循环不变式和常数之后,进行寄存器分配并选择要溢出/重新加载的内容(或仅使用只读矢量常数)。

当发现没有足够的寄存器来将所有变量和常量保留在循环内的regs中时,保留在寄存器中的东西的通常选择通常是循环不变的向量,可以是编译时常量,也可以是在循环之前计算的东西。

在L1d缓存中命中的额外负载要比在循环内存储(又称溢出)/重新加载变量便宜。因此,无论您将定义放在源代码中的什么位置,编译器都会选择从内存中加载常量。

使用C ++编写代码的部分原因是您需要一个编译器来为您做出此决定。由于允许对两个源执行相同的操作,因此对于至少一种情况,执行不同的操作可能会错过优化。 (在任何特定情况下,最好的做法取决于周围的代码,但是通常在编译器的regs值较低时,使用向量常量作为内存源操作数就可以了。)

  

是不是像“ ptr增加了2个额外的延迟”这样的概念?

内存源操作数的微融合不会延长从非恒定输入到输出的关键路径。一旦地址准备好,加载uop就可以开始,对于矢量常量,通常是相对RIP或_mm_andn_si128寻址模式。因此,通常,一旦负载被发布到内核的乱序部分,就可以立即执行。假设一个L1d缓存命中(因为每次循环迭代加载它都会在缓存中保持高温),这大约只有5个周期,所以如果向量寄存器输入上存在依赖链瓶颈,那么很容易及时做好准备。

它甚至没有损害前端吞吐量。除非您在负载端口吞吐量上遇到瓶颈(现代x86 CPU上每个时钟2个负载),否则通常没有什么区别。 (即使使用高度精确的测量技术。)