方案:您正在使用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个额外延迟”之类的概念?还是不确定性,以至于每个案例都不同,并且只能通过反复试验+剖析才能完全优化?
答案 0 :(得分:3)
一个好的优化编译器应该为两个版本生成相同的机器代码。只需将向量常量定义为局部常量,或匿名使用它们即可最大程度地提高可读性;让编译器不必担心寄存器分配,并选择最便宜的方法来处理寄存器用尽的情况。
您最好的帮助编译器的方法是,如果可能的话,使用更少的不同常量。例如而不是同时使用_mm_and_si128
和set1_epi16(0x00FF)
的{{1}},请使用0xFF00
来掩盖另一种方式。通常,您不能做任何事情来影响它选择保留在寄存器中的内容,而不是影响它们,但是幸运的是,编译器在此方面非常擅长,因为它对于标量代码也必不可少。
编译器会将常量提升到循环之外(甚至内联包含常量的辅助函数),或者如果仅在分支的一侧使用,则将设置带入分支的那一侧。
源代码计算的是完全相同的东西,没有明显的副作用,因此,按条件规则允许编译器自由地执行此操作。
我认为编译器通常会在执行CSE(常见子表达式消除)并确定可提升的循环不变式和常数之后,进行寄存器分配并选择要溢出/重新加载的内容(或仅使用只读矢量常数)。
当发现没有足够的寄存器来将所有变量和常量保留在循环内的regs中时,不保留在寄存器中的东西的通常选择通常是循环不变的向量,可以是编译时常量,也可以是在循环之前计算的东西。
在L1d缓存中命中的额外负载要比在循环内存储(又称溢出)/重新加载变量便宜。因此,无论您将定义放在源代码中的什么位置,编译器都会选择从内存中加载常量。
使用C ++编写代码的部分原因是您需要一个编译器来为您做出此决定。由于允许对两个源执行相同的操作,因此对于至少一种情况,执行不同的操作可能会错过优化。 (在任何特定情况下,最好的做法取决于周围的代码,但是通常在编译器的regs值较低时,使用向量常量作为内存源操作数就可以了。)
是不是像“ ptr增加了2个额外的延迟”这样的概念?
内存源操作数的微融合不会延长从非恒定输入到输出的关键路径。一旦地址准备好,加载uop就可以开始,对于矢量常量,通常是相对RIP或_mm_andn_si128
寻址模式。因此,通常,一旦负载被发布到内核的乱序部分,就可以立即执行。假设一个L1d缓存命中(因为每次循环迭代加载它都会在缓存中保持高温),这大约只有5个周期,所以如果向量寄存器输入上存在依赖链瓶颈,那么很容易及时做好准备。>
它甚至没有损害前端吞吐量。除非您在负载端口吞吐量上遇到瓶颈(现代x86 CPU上每个时钟2个负载),否则通常没有什么区别。 (即使使用高度精确的测量技术。)