【问题标题】:Is it possible to convince clang to auto-vectorize this code without using intrinsics?是否可以说服 clang 在不使用内在函数的情况下自动矢量化此代码?
【发布时间】:2019-10-07 10:39:58
【问题描述】:

想象一下,我有这个简单的函数来检测球体重叠。这个问题的重点并不是真正讨论对球体进行命中测试的最佳方法,所以这只是为了说明。

inline bool sphere_hit(float x1, float y1, float z1, float r1,
        float x2, float y2, float z2, float r2) {
    float xd = (x1 - x2);
    float yd = (y1 - y2);
    float zd = (z1 - z2);

    float max_dist = (r1 + r2);

    return xd * xd + yd * yd + zd * zd < max_dist * max_dist;
}

我在嵌套循环中调用它,如下:

std::vector<float> xs, ys, zs, rs;
int n_spheres;
// <snip>
int n_hits = 0;
for (int i = 0; i < n_spheres; ++i) {
    for (int j = i + 1; j < n_spheres; ++j) {
        if (sphere_hit(xs[i], ys[i], zs[i], rs[i],
                xs[j], ys[j], zs[j], rs[j])) {
            ++n_hits;
        }
    }
}
std::printf("total hits: %d\n", n_hits);

现在,clang(使用-O3 -march=native)足够聪明,可以弄清楚如何将此循环向量化(和展开)为 256 位 avx2 指令。太棒了!

但是,如果我做的事情比增加点击次数更复杂,例如调用某个任意函数 handle_hit(i, j),clang 会发出一个简单的标量版本。

命中应该是非常罕见的,所以我认为应该检查每个矢量化循环迭代,如果 any 车道的值为真,如果是,则跳转到一些标量慢路径。这应该可以使用vcmpltps 后跟vmovmskps。但是,即使我用__builtin_expect(..., 0) 包围对sphere_hit 的调用,我也无法发出此代码。

【问题讨论】:

  • 也许将另一个内部循环写入当前的内部循环,该内部循环首先将 8 个 sphere_hit 调用的结果写入位集(并希望这会生成一个 @ 987654330@ 指令)可以工作。 (如果 bitset 不为零,则遍历 bitset)。

标签: vectorization simd llvm-clang micro-optimization avx2


【解决方案1】:

确实可以说服 clang 对这段代码进行矢量化。使用编译器选项 -Rpass-analysis=loop-vectorize -Rpass=loop-vectorize -Rpass-missed=loop-vectorize,clang 声称浮点运算是矢量化的,Godbolt output 证实了这一点。 (红色下划线的fors 不是错误,而是矢量化报告)。

可以通过将sphere_hit 的结果作为字符存储到临时数组hitx8 中来实现向量化。 之后,每次迭代都会测试 8 个 sphere_hit 结果,方法是将 8 个字符从内存中读取为一个 uint64_t a。这应该非常有效,因为条件a!=0 (见下面的代码)仍然很少见,因为球体命中非常罕见。此外,数组hitx8 可能大部分时间都在L1 或L2 缓存中。

我没有测试代码的正确性,但至少自动矢量化的想法应该可行。

/* clang -Ofast -Wall -march=broadwell -Rpass-analysis=loop-vectorize -Rpass=loop-vectorize -Rpass-missed=loop-vectorize */
#include<string.h>
char sphere_hit(float x1, float y1, float z1, float r1,
        float x2, float y2, float z2, float r2);
void handle_hit(int i, int j);

void vectorized_code(float* __restrict xs, float* __restrict ys, float* __restrict zs, float* __restrict rs, char* __restrict hitx8, int n_spheres){
    unsigned long long int a;
    for (int i = 0; i < n_spheres; ++i) {
        for (int j = i + 1; j < n_spheres; ++j){
            /* Store the boolean results temporarily in char array hitx8.     */
            /* The indices of hitx8 are shifted by i+1, so the loop           */
            /* starts with hitx8[0]                                           */
            /* char array hitx8 should have n_spheres + 8 elements            */
            hitx8[j-i-1] = sphere_hit(xs[i], ys[i], zs[i], rs[i],
                    xs[j], ys[j], zs[j], rs[j]);
        }
        for (int j = n_spheres; j < n_spheres+8; ++j){
            /* Add 8 extra zeros at the end of hitx8.                   */
            hitx8[j-i-1] = 0;     /* hitx8 is 8 elements longer than xs */
        }
        for (int j = i + 1; j < n_spheres; j=j+8){
            memcpy(&a,&hitx8[j-i-1],8);
            /* Check 8 sphere hits in parallel:                                   */
            /* one `unsigned long long int a` contains 8 boolean values here      */ 
            /* The condition a!=0 is still rare since sphere hits are very rare.  */
            if (a!=0ull){ 
                if (hitx8[j-i-1+0] != 0) handle_hit(i,j+0);
                if (hitx8[j-i-1+1] != 0) handle_hit(i,j+1);
                if (hitx8[j-i-1+2] != 0) handle_hit(i,j+2);
                if (hitx8[j-i-1+3] != 0) handle_hit(i,j+3);
                if (hitx8[j-i-1+4] != 0) handle_hit(i,j+4);
                if (hitx8[j-i-1+5] != 0) handle_hit(i,j+5);
                if (hitx8[j-i-1+6] != 0) handle_hit(i,j+6);
                if (hitx8[j-i-1+7] != 0) handle_hit(i,j+7);
            }
        }
    }
}


inline char sphere_hit(float x1, float y1, float z1, float r1,
        float x2, float y2, float z2, float r2) {
    float xd = (x1 - x2);
    float yd = (y1 - y2);
    float zd = (z1 - z2);

    float max_dist = (r1 + r2);

    return xd * xd + yd * yd + zd * zd < max_dist * max_dist;
}

【讨论】:

  • 非常聪明的答案。
  • 不如手动矢量化,但是有足够的 SUB/FMA 工作来隐藏使用 extract / vpackssdw / vpackuswb 的 3 次洗牌的吞吐量成本。不过,命中位置检测的效率远低于vpmovmskb / bsf。不过,如果您需要它的便携性,总比没有好。
  • @PeterCordes:原则上,可以用bsfa=a&amp;(a-1) 来重置大小为popcount(a) 的短循环替换用于命中检测的8 个ifs每一步的最低设置位(或 BMI1 blsr 指令)。这可能会更有效率,但对于罕见的a!=0,我不希望有巨大的性能优势。
  • 对,我应该说命中 checking 的效率低于vpmovmskb / test eax,eax 与 SIMD 工作交错的效率。 a!=0ull 的代码是在循环内向前跳转的循环的一部分,它的循环开销比您想要简单地检查您期望全为零的数组要多。我担心将有效的微型循环体分成两部分的 uop-cache 吞吐量,尤其是在微码更新禁用 LSD 的 Intel CPU 上。并且2个采取的分支是不好的。可能UNLIKELY() 宏可能会有所帮助,或者配置文件引导的优化。
  • 如果它本身有一个核心,它可能基本上没问题,并且只比理想的矢量化慢一点,但由于额外的 uops 用于洗牌,所以对超线程不太友好。
猜你喜欢
  • 2019-04-28
  • 2019-06-16
  • 2023-03-17
  • 2019-02-12
  • 1970-01-01
  • 2011-08-09
  • 1970-01-01
  • 1970-01-01
  • 2012-10-13
相关资源
最近更新 更多