Clang+OpenMP低效循环不变量



Clang在回答另一个问题时遇到了一些低效的代码生成(如何使用openmp并行化此代码并减少(

让我们考虑一下这个简单的代码:

void scale(float* inout, ptrdiff_t n, ptrdiff_t m, ptrdiff_t stride, float value)
{
const float inverse = 1.f / value;
#   pragma omp parallel for
for(ptrdiff_t i = 0; i < n; ++i) {
#       pragma omp simd
for(ptrdiff_t j = 0; j < m; ++j)
inout[i * stride + j] *= inverse;
}
}

你把逆的计算放在哪里?它重要吗?我探索过的选项:

  1. 循环外,在示例中
  2. 在平行段中,但在环路之前
  3. 在外环
  4. 在内环

对于GCC-11,选项1生成最佳代码:一个分区,然后每个线程加载和广播一个内存。选项2-4都生成基本相同的代码,每个线程进行一次除法运算。

Clang组件

然而,Clang-13的代码却大不相同。

选项1:在内部循环中执行冗余内存加载和广播。它不通过堆栈指针加载,而是浪费了一个通用寄存器作为常量的指针。如果您将代码更改为需要多个常量,Clang将浪费多个GP寄存器。

选项2:与GCC 相同的代码模式

选项3:外循环每次迭代重复一次除法

选项4:在内部环路中重复划分

摘要

Clang的代码生成似乎在从OpenMP循环中提取冗余计算方面存在一些问题。有趣的是,它似乎不会影响数组索引的计算。它可以很好地从内环中拉出。

如果我想要在GCC和Clang上都能很好地工作的代码,我必须写这样的东西:

void scale(float* inout, ptrdiff_t n, ptrdiff_t m, ptrdiff_t stride, float value)
{
#   pragma omp parallel
{
const float inverse = 1.f / value;
#       pragma omp for nowait
for(ptrdiff_t i = 0; i < n; ++i) {
#           pragma omp simd
for(ptrdiff_t j = 0; j < m; ++j)
inout[i * stride + j] *= inverse;
}
}
}

但这太冗长了。在这个代码示例中,这整件事是一个小麻烦,但如果您查看上面另一个答案中的代码,它会变得非常糟糕(尤其是GP寄存器浪费(,严重影响性能。

总之,我是不是错过了什么?我是否应该以不同的方式编写循环,以确保Clang和GCC都有良好的代码?

补充信息

这是一个允许轻松测试的代码版本,这里是Godbolt链接

#include <cstddef>
// using std::ptrdiff_t
#define CONST_LOCATION 1
void scale(float* inout, std::ptrdiff_t n, std::ptrdiff_t m, std::ptrdiff_t stride,
float value)
{
# if CONST_LOCATION == 1
/*
* Clang-13.0.1: Redundant broadcast from memory in inner loop.
*               Wastes GP register for pointer to constant
* GCC-11.2: Optimal
*/
const float inv = 1.f / value;
#endif
#   pragma omp parallel
{
#     if CONST_LOCATION == 2
/*
* Clang: Redundant computation in outer loop setup. Otherwise optimal
* GCC: Same as Clang
*/
const float inv = 1.f / value;
#     endif
#       pragma omp for nowait
for(std::ptrdiff_t i = 0; i < n; ++i) {
#         if CONST_LOCATION == 3
/*
* Clang: Redundant computation in inner loop setup!
* GCC: Same as 2
*/
const float inv = 1.f / value;
#         endif
#           pragma omp simd
for(std::ptrdiff_t j = 0; j < m; ++j) {
#             if CONST_LOCATION == 4
/*
* Clang: Redundant computation in inner loop!
* GCC: Same as 2
*/
const float inv = 1.f / value;
#             endif
inout[i*stride + j] *= inv;
}
}
}
}

使用-O3 -mavx2 -mfma -fopenmp进行测试,以获得一个相当通用的现代编译。

回答我自己,是内部循环中的pragma omp simd扰乱了clang的代码生成。这是一种耻辱,因为在某些情况下,它确实对一些编译器产生了积极的影响。

最新更新