【问题标题】:Gcc misoptimises sse functionGcc 错误优化 sse 函数
【发布时间】:2021-06-10 20:13:58
【问题描述】:

我正在将一个项目转换为使用来自 clang 的 gcc 进行编译,但我遇到了一个使用 sse 函数的函数的问题:

void dodgy_function(
    const short* lows,
    const short* highs,
    short* mins,
    short* maxs,
    int its
)
{
    __m128i v00[2] = { _mm_setzero_si128(), _mm_setzero_si128() }; 
    __m128i v10[2] = { _mm_setzero_si128(), _mm_setzero_si128() };

    for (int i = 0; i < its; ++i) {
        reinterpret_cast<short*>(v00)[i] = lows[i];
        reinterpret_cast<short*>(v10)[i] = highs[i];
    }

    reinterpret_cast<short*>(v00)[its] = reinterpret_cast<short*>(v00)[its - 1];
    reinterpret_cast<short*>(v10)[its] = reinterpret_cast<short*>(v10)[its - 1];

    __m128i v01[2] = {_mm_setzero_si128(), _mm_setzero_si128()};
    __m128i v11[2] = {_mm_setzero_si128(), _mm_setzero_si128()};

    __m128i min[2];
    __m128i max[2];

    min[0] = _mm_min_epi16(_mm_max_epi16(v11[0], v01[0]), _mm_min_epi16(v10[0], v00[0]));
    max[0] = _mm_max_epi16(_mm_max_epi16(v11[0], v01[0]), _mm_max_epi16(v10[0], v00[0]));

    min[1] = _mm_min_epi16(_mm_min_epi16(v11[1], v01[1]), _mm_min_epi16(v10[1], v00[1])); 
    max[1] = _mm_max_epi16(_mm_max_epi16(v11[1], v01[1]), _mm_max_epi16(v10[1], v00[1]));

    reinterpret_cast<__m128i*>(mins)[0] = _mm_min_epi16(reinterpret_cast<__m128i*>(mins)[0], min[0]);
    reinterpret_cast<__m128i*>(maxs)[0] = _mm_max_epi16(reinterpret_cast<__m128i*>(maxs)[0], max[0]);

    reinterpret_cast<__m128i*>(mins)[1] = _mm_min_epi16(reinterpret_cast<__m128i*>(mins)[1], min[1]);
    reinterpret_cast<__m128i*>(maxs)[1] = _mm_max_epi16(reinterpret_cast<__m128i*>(maxs)[1], max[1]);
}

现在有了 clang,它给了我预期的输出,但在 gcc 中它打印全零:godbolt link

我发现当我使用 -O1 编译时 gcc 给了我正确的结果,但使用 -O2 和 -O3 时出错,这表明优化器出错了。我做的有什么特别错误的事情会导致这种行为吗?

作为一种变通方法,我可以将事情包装在一个联合中,然后 gcc 会给我正确的结果,但这感觉有点恶心:godbolt link 2

有什么想法吗?

【问题讨论】:

  • 是的,看起来就像消除写入中的别名一样:godbolt.org/z/bfanne。如果你不能像我一样强制对齐,我认为联合是最干净的方式
  • 这么多 reinterpret_cast...
  • "优化器出错了"...实际发生的情况是由于严格的别名违规,您正在调用未定义的行为,因此对于结果没有任何意义程序。
  • 这就是我试图理解的部分内容,如果这是一个 gcc 错误或其他东西,如 UB,或两者兼而有之。
  • 我们也应该期待一个严格的别名违规警告吗?

标签: c++ gcc sse intrinsics strict-aliasing


【解决方案1】:

问题是您使用short* 来访问__m128i* 对象的元素。这违反了严格的混叠规则。只有使用__m128i* 取消引用或更通常的_mm_load_si128( (const __m128i*)ptr ) 才能安全。

__m128i*char* 完全一样——你可以将它指向任何东西,但反之则不行:Is `reinterpret_cast`ing between hardware SIMD vector pointer and the corresponding type an undefined behavior?


类型双关的唯一标准方法是使用 memcpy:

    memcpy(v00, lows, its * sizeof(short));
    memcpy(v10, highs, its * sizeof(short));
    memcpy(reinterpret_cast<short*>(v00) + its, lows + its - 1, sizeof(short));
    memcpy(reinterpret_cast<short*>(v10) + its, highs + its - 1,  sizeof(short));

https://godbolt.org/z/f63q7x

我更喜欢直接使用正确类型的对齐内存:

    alignas(16) short v00[16];
    alignas(16) short v10[16];
    auto mv00 = reinterpret_cast<__m128i*>(v00);
    auto mv10 = reinterpret_cast<__m128i*>(v10);
    _mm_store_si128(mv00, _mm_setzero_si128());
    _mm_store_si128(mv10, _mm_setzero_si128());
    _mm_store_si128(mv00 + 1, _mm_setzero_si128());
    _mm_store_si128(mv10 + 1, _mm_setzero_si128());

    for (int i = 0; i < its; ++i) {
        v00[i] = lows[i];
        v10[i] = highs[i];
    }

    v00[its] = v00[its - 1];
    v10[its] = v10[its - 1];

https://godbolt.org/z/bfanne

我不确定这个设置是否实际上符合标准(它绝对适用于_mm_load_ps,因为你可以在没有类型双关语的情况下做到这一点)但它似乎也修复了问题。我猜想加载/存储内在函数的任何合理实现都必须提供与memcpy 相同的别名保证,因为它或多或少是在x86 中从直线到矢量化代码的犹太方式。

正如您在问题中提到的,您还可以强制与联合对齐,我在 c++11 之前的上下文中也使用了它。即使在这种情况下,我个人仍然总是明确地编写加载和存储(即使它们只是进出对齐的内存),因为如果你不这样做,这样的问题往往会弹出。

【讨论】:

  • __m128i 是 GNU C 中的 __attribute__((may_alias)) 类型;英特尔的内在函数的定义方式要求您能够将__m128i* 指向其他任何内容。 (但不是相反,这是 OP 出错的地方并通过将 short* 指向 __m128i 对象来引入 UB。)Is `reinterpret_cast`ing between hardware SIMD vector pointer and the corresponding type an undefined behavior?
  • 虽然这是安全的,但与从低点和高点(例如_mm_loadu_si128((const __m128i*)lows))加载的 SIMD 相比,它看起来也效率低下。毕竟,这就是使用 SIMD 的全部意义...... 填充数组,或检查它不会进入不包含您要加载的任何数据的页面。您可以然后look up an AND mask to zero the tail of the array,但在执行SIMD 存储后,最简单的方法可能是memset(mins+its, 0, 32 - 2*its):将我们使用SIMD 存储垃圾的高元素归零,因为我们总是存储32B
  • 哦,忘了提一下:所有_mm_load* / 存储内在函数,即使是带有float*int*__int64* arg 的那些,都是严格混叠安全的。 (或者至少应该是。)
  • @PeterCordes 我同意,我可能也更喜欢来自低点/高点的未对齐负载,但我不打算重写这个函数,因为我不确定它是如何被使用的并且不想要在its &lt; 16 或上帝禁止its &lt; 8 的情况下处理部分负载。我认为通常整个函数都可以很好地重写
  • 好吧,就像我说的,您只需要在继续加载 32 个字节之前检查 ((uintptr_t)lows &amp; 4095) &lt;= (4096-32),或者以某种方式将 its 混入其中。否则,对不太可能的页面交叉情况使用慢速回退。但是,如果您想避免那么多重写,我可能会使用alignas(16) short v00[16] = {0}; 要求编译器将数组初始化为零,而不是编写 4 个内在函数来完成编译器已经要做的事情。
猜你喜欢
  • 2023-04-01
  • 2011-12-16
  • 1970-01-01
  • 2011-10-18
  • 2017-12-24
  • 2013-10-07
  • 2023-03-06
  • 2012-02-17
  • 1970-01-01
相关资源
最近更新 更多