为啥 gcc -O3 处理 avx256 的内在比较与 gcc -O0 和 clang 不同?

Posted

技术标签:

【中文标题】为啥 gcc -O3 处理 avx256 的内在比较与 gcc -O0 和 clang 不同?【英文标题】:Why does gcc -O3 handle avx256 compare intrinsic differently than gcc -O0 and clang?为什么 gcc -O3 处理 avx256 的内在比较与 gcc -O0 和 clang 不同? 【发布时间】:2020-05-18 09:48:25 【问题描述】:

我想设置两个整数向量并将它们与 SIMD 进行比较,然后使用此掩码对打包浮点数进行混合操作。我生成了以下代码:

#include <immintrin.h>
#include <stdio.h>
#include <string.h>


int main()
    __m256i is =  _mm256_setr_epi32(1, 2, 3, 4, 5, 6, 7, 8);
    __m256i js =  _mm256_set1_epi32(1);               // integer bit-patterns
    __m256 mask = _mm256_cmp_ps(is,js, _CMP_EQ_OQ);   // compare as subnormal floats

    float val[8];
    memcpy(val, &mask, sizeof(val));
    printf("%f %f %f %f %f %f %f %f \n", val[0], val[1], val[2], val[3], val[4], val[5], val[6], val[7]);

适用于gcc -mavx main.c 以及clang -mavx main.cclang -O3 -mavx main.c

(编者注:当 cmpps 将这些非正规输入视为 0.0 时,它将与 -ffast-math 中断,因此所有比较都是正确的。您希望 AVX2 _mm256_cmp_epi32 进行 integer 比较, 和_mm256_castsi256_ps 结果。但这与关于gcc -O0 和clang 允许从__m256i__m256 的隐式转换的问题无关)

但是,当我使用 gcc -O3 -mavx main.c 时,我收到以下错误消息:

main.c: In function ‘main’:
main.c:9:33: error: incompatible type for argument 1 of ‘_mm256_cmp_ps’
    9 |     __m256 mask = _mm256_cmp_ps(is,js, _CMP_EQ_OQ);
      |                                 ^~
      |                                 |
      |                                 __m256i aka __vector(4) long long int
In file included from /usr/lib/gcc/x86_64-pc-linux-gnu/9.3.0/include/immintrin.h:51,
                 from main.c:1:
/usr/lib/gcc/x86_64-pc-linux-gnu/9.3.0/include/avxintrin.h:404:23: note: expected ‘__m256’ aka ‘__vector(8) float’ but argument is of type ‘__m256i’ aka ‘__vector(4) long long int’
  404 | _mm256_cmp_ps (__m256 __X, __m256 __Y, const int __P)
      |                ~~~~~~~^~~
main.c:9:36: error: incompatible type for argument 2 of ‘_mm256_cmp_ps’
    9 |     __m256 mask = _mm256_cmp_ps(is,js, _CMP_EQ_OQ);
      |                                    ^~
      |                                    |
      |                                    __m256i aka __vector(4) long long int
In file included from /usr/lib/gcc/x86_64-pc-linux-gnu/9.3.0/include/immintrin.h:51,
                 from main.c:1:
/usr/lib/gcc/x86_64-pc-linux-gnu/9.3.0/include/avxintrin.h:404:35: note: expected ‘__m256’ aka ‘__vector(8) float’ but argument is of type ‘__m256i’ aka ‘__vector(4) long long int’
  404 | _mm256_cmp_ps (__m256 __X, __m256 __Y, const int __P)
      |                            ~~~~~~~^~~

我注意到两件事。首先,编译器似乎将is 视为__m256i aka __vector(4) long long int,而它包含8 个整数。其次,编译器的抱怨是正确的,因为英特尔内在函数指南1 将参数显示为__m256。我现在很困惑为什么这段代码甚至在一开始就可以工作。如果它确实是正确的,因为整数被转换为浮点数,那么我不明白为什么它不适用于gcc -O3

我不想使用返回__m256i_mm256_cmpeq_epi32 并且(似乎没有)没有接受这种掩码的blend_ps 指令。

为什么编译器的行为会有所不同,以及执行此操作的正确方法是什么?


编译器版本

$ gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-pc-linux-gnu/9.3.0/lto-wrapper
Target: x86_64-pc-linux-gnu
Configured with: /build/gcc/src/gcc/configure --prefix=/usr --libdir=/usr/lib --libexecdir=/usr/lib --mandir=/usr/share/man --infodir=/usr/share/info --with-pkgversion='Arch Linux 9.3.0-1' --with-bugurl=https://bugs.archlinux.org/ --enable-languages=c,c++,ada,fortran,go,lto,objc,obj-c++,d --enable-shared --enable-threads=posix --with-system-zlib --with-isl --enable-__cxa_atexit --disable-libunwind-exceptions --enable-clocale=gnu --disable-libstdcxx-pch --disable-libssp --enable-gnu-unique-object --enable-linker-build-id --enable-lto --enable-plugin --enable-install-libiberty --with-linker-hash-style=gnu --enable-gnu-indirect-function --enable-multilib --disable-werror --enable-checking=release --enable-default-pie --enable-default-ssp --enable-cet=auto gdc_include_dir=/usr/include/dlang/gdc
Thread model: posix
gcc version 9.3.0 (Arch Linux 9.3.0-1) 
$ clang -v
clang version 10.0.0 
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin
Found candidate GCC installation: /usr/bin/../lib/gcc/x86_64-pc-linux-gnu/8.4.0
Found candidate GCC installation: /usr/bin/../lib/gcc/x86_64-pc-linux-gnu/9.3.0
Found candidate GCC installation: /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/8.4.0
Found candidate GCC installation: /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/9.3.0
Found candidate GCC installation: /usr/lib/gcc/x86_64-pc-linux-gnu/8.4.0
Found candidate GCC installation: /usr/lib/gcc/x86_64-pc-linux-gnu/9.3.0
Found candidate GCC installation: /usr/lib64/gcc/x86_64-pc-linux-gnu/8.4.0
Found candidate GCC installation: /usr/lib64/gcc/x86_64-pc-linux-gnu/9.3.0
Selected GCC installation: /usr/bin/../lib64/gcc/x86_64-pc-linux-gnu/9.3.0
Candidate multilib: .;@m64
Candidate multilib: 32;@m32
Selected multilib: .;@m64
Found CUDA installation: /opt/cuda, version 10.1

[1]https://software.intel.com/sites/landingpage/IntrinsicsGuide/

【问题讨论】:

我编辑了编译器的编译器版本。简而言之:clang 10.0.0 和 gcc 9.3.0 您确实希望 _mm256_cmpeq_epi32,缺少的成分是 _mm256_castsi256_ps 将 0 / -1 元素的向量转换为 __m256 以使编译器对 C 类型感到满意。在 asm 中,这两种类型都可以存在于 YMM 寄存器中;如果比较结果中的“布尔”向量存在第三种类型,则 C 类型系统可能更明智,就像 Agner Fog 在他的 vectorclass C++ 包装器库中所做的那样。 【参考方案1】:

首先,编译器似乎将is 视为__m256i aka __vector(4) long long int,而它包含8 个整数。

__m128i 和更大的类似向量没有指定存储在其中的整数的实际大小(和数量)。您可以使用相同的 __m128i 类型来存储 16 个 uint8_ts 或 2 个 uint64_ts 或介于两者之间的任何内容。重要的部分是它用于存储整数。它是对__m128i 和更大的类似向量的操作,它指定将verctors 解释为一组给定宽度的整数。例如,_mm_add_epi16_mm_add_epi32 都接受 __m128i 参数,但第一个将其解释为 8 个uint16_ts 的向量,第二个 - 4 个uint32_ts。

其次,编译器的抱怨是正确的,因为 intel 内在函数指南 1 将参数显示为 __m256

我认为,编译器抱怨是正确的。它用-O0 编译代码似乎是一个编译器错误。在 gcc 中,__m128i 和其他向量是使用 __attribute__((vector_size)) 属性实现的,文档说应该使用 __builtin_convertvector 内在来在不同类型的向量之间进行转换。

英特尔软件开发人员手册第 3.1.1.10 节中__m128i 和其他向量类型的原始定义没有明确说明不同类型向量的可转换性,但确实这样说:

这些 SIMD 数据类型不是基本的标准 C 数据类型或 C++ 对象,因此它们只能与赋值运算符一起使用,作为函数参数传递,并从函数调用返回。

鉴于此,我认为这些向量类型不应该是隐式可转换的。你当然不能依赖转换,如果它确实编译,会有任何特定的行为。尤其是考虑到整数向量没有指定其元素的大小。因此,您应该始终使用内在函数来定义您想要的转换类型,例如_mm_cvtepi32_ps/_mm_cvtepi32_pd_mm_castsi128_ps/_mm_castsi128_pd

我不想使用返回__m256i_mm256_cmpeq_epi32 并且(似乎没有)没有接受这种掩码的blend_ps 指令。

_mm256_cmpeq_epi32 是 AVX2,在 AVX2 中有_mm256_blendv_epi8。如果您只限于 AVX,那么您必须对 128 位整数向量进行操作。

使用_mm256_cmp_ps 对整数向量进行操作是不正确的,因为它的行为不同于整数比较。特别是,如果至少有一个输入操作数与 NaN 位模式匹配(例如,使用 _CMP_EQ_OQ 操作数,您的比较将始终在结果向量元素中返回 0)。

【讨论】:

谢谢,这让我清楚地知道 __m128i 是一个容器,并且解释是由不同的函数完成的。我还将函数切换到_mm256_cmpeq_epi32,然后我用_mm256_castsi256_ps 进行转换,现在我可以在_mm256_blendv_ps 中使用掩码,它似乎可以工作。 @lyinch 为什么不使用_mm256_blendv_epi8?请注意,将 INT 向量指令的结果传递给 FP 向量指令(反之亦然)可能会导致跨域性能损失。 我想根据索引比较混合两个浮点向量,这就是我不使用 blend_epi8 的原因

以上是关于为啥 gcc -O3 处理 avx256 的内在比较与 gcc -O0 和 clang 不同?的主要内容,如果未能解决你的问题,请参考以下文章

发行版将 GCC 升级到 5.5.0 后,AVX512 内在函数头会产生许多错误

AVX 内在 _mm256_cmp_ps 是不是应该在为真时返回 NaN?

int64_t 指针转换为 AVX2 内在 _m256i

如何用 gcc 或 clang 模拟 _mm256_loadu_epi32?

防止 gcc 将我的 AVX2 内在函数复制到 REP MOVS

强制 AVX 内部函数改为使用 SSE 指令