首页 > 解决方案 > GCC 不断抱怨 AVX512 函数 _mm512_cvt_roundpd_epi64 的“错误:不正确的舍入操作数”

问题描述

我正在使用 _mm512_cvt_roundpd_epi64 并不断收到编译器错误:

/dump/1/alicpp2/built/gcc-7.3.0-7u2/gcc-7.3.0/lib/gcc/x86_64-pc-linux-gnu/7.3.0/include/avx512dqintrin.h:1574:14:错误: 不正确的舍入操作数 __R);

这是我的代码:

    #include <iostream>
    #include <immintrin.h>

    void Date64Align(int64_t* dst, int64_t* src, size_t length) {
      constexpr int dop = 512 / 64;
      int64_t starting_epoch_milliseconds_ = 1513728000;
      int32_t granularity_milliseconds_ = 3600;

      __m512i start = _mm512_set1_epi64(starting_epoch_milliseconds_);
      __m512i granularity = _mm512_set1_epi64(granularity_milliseconds_);

      double temp = (double)granularity_milliseconds_;
      __m512d granularity_double = _mm512_set1_pd(temp);

      for (int i = 0; i < length / dop; ++i) {
        // load the src (load X into SIMD register
        __m512i data = _mm512_load_epi64(src);
        // X - starting_epoch_milliseconds_
        data = _mm512_sub_epi64(data, start);
        // convert X to double
        __m512d double_data;
        double_data = _mm512_cvt_roundepi64_pd(data, _MM_FROUND_TO_NEAREST_INT);

        // X = X / Y in double
        double_data = _mm512_div_pd(double_data, granularity_double);

        // Convert X to int64
        data = _mm512_cvt_roundpd_epi64(double_data, _MM_FROUND_NO_EXC);

        data = _mm512_mullo_epi64(data, granularity);

        // store X
        _mm512_store_epi64(dst, data);

        src += dop;
        dst += dop;
      }
    }

    int main() {
      return 0;
    }

还有我的 CMakeFileLists.txt:

    cmake_minimum_required(VERSION 3.11)
    project(untitled3)

    set(CMAKE_CXX_STANDARD 17)
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -ggdb -msse4.2 -mavx512f - 
    mavx512dq")


    add_executable(untitled3 main.cpp)

有人熟悉 AVX512 库并帮助回答我的问题吗?

标签: simdavx512

解决方案


仅供参考,您通常不需要明确的 round-to-nearest。默认模式是四舍五入,所有异常都被屏蔽。普通_mm512_cvtepi64_pd_mm512_cvtpd_epi64行为与您正在执行的操作相同,除非您使用 或 更改了此线程中的默认舍入模式或异常fenv掩码_MM_SET_ROUNDING_MODE

抑制异常仅意味着它们没有故障,但如果我正确阅读英特尔的手册,它不会阻止在 MXCSR 中设置相关的粘滞状态位的次正常或溢出。他们说这就像在 MXCSR 中设置屏蔽位一样,并不是说它根本不会在 MXCSR 状态位中记录异常。

一个更常见的用例是使用或舍入(朝向 -/+Infinity)_mm512_cvt_roundpd_epi64转换为整数,而不是在转换之前像使用 128 位或 256 位向量那样进行单独的舍入步骤。floorceil

但是,如果您正在运行一些未屏蔽的 FP 异常或可能是非默认舍入模式,那么显式舍入到最近确实是有意义的。


舍入模式覆盖必须始终包括_MM_FROUND_NO_EXC

如果编译器提供更好的错误消息告诉您这一点,那就太好了。(TODO:关于 gcc 和 clang 的文件功能请求错误报告)。

_MM_FROUND_CUR_DIRECTION不算,它的意思是“没有覆盖”,就像你使用了正常的非round版本的内在函数一样。)

英特尔的内在函数指南指出了这一点(在特定的条目中_mm512_cvt_roundepi64_pd,但您会在每个采用舍入模式覆盖 arg 的内在函数中找到相同的内容。)

舍入是根据舍入参数完成的,该参数可以是以下之一:

(_MM_FROUND_TO_NEAREST_INT |_MM_FROUND_NO_EXC) // round to nearest, and suppress exceptions
(_MM_FROUND_TO_NEG_INF |_MM_FROUND_NO_EXC)     // round down, and suppress exceptions
(_MM_FROUND_TO_POS_INF |_MM_FROUND_NO_EXC)     // round up, and suppress exceptions
(_MM_FROUND_TO_ZERO |_MM_FROUND_NO_EXC)        // truncate, and suppress exceptions
_MM_FROUND_CUR_DIRECTION // use MXCSR.RC; see _MM_SET_ROUNDING_MODE

请注意,_MM_FROUND_NO_EXC它本身恰好是有效的,因为_MM_FROUND_TO_NEAREST_INT恰好是0,与设置时的 2 位舍入模式字段的机器代码编码相同EVEX.b。但是你真的应该在你的_mm512_cvt_roundpd_epi64.

对于没有舍入控制的指令,例如(注意截断_mm512_cvtt_roundpd_epi64的额外部分),只允许(or ),因为行为不受 2 位字段的值的影响,无论是否舍入指定了覆盖。t_MM_FROUND_NO_EXC_MM_FROUND_CUR_DIRECTION


在 EVEX 前缀的机器编码中,设置舍入模式覆盖意味着 SAE(抑制所有异常)。_MM_FROUND_TO_NEAREST_INT如果不抑制异常,就无法对覆盖进行编码。

来自 Intel 的 vol.2 指令集参考手册

2.6.8 EVEX 中的静态舍入支持

嵌入在 EVEX 编码系统中的静态舍入控制仅适用于寄存器到寄存器风格的浮点指令,具有两个不同向量长度的舍入语义:(i) 标量,(ii) 512 位。在这两种情况下,如果设置,该字段EVEX.L’L表示舍入模式控制覆盖。设置时,暗示“抑制所有异常”。MXCSR.RCEVEX.bEVEX.b

请注意,舍入覆盖使编译器无法使用内存源操作数,因为该EVEX.b位在该上下文中表示广播与非广播。

在您的情况下不是问题;数据来自 a _mm512_sub_epi64,但通常值得指出的是,对已经是默认值的舍入模式的覆盖可能会因在某些情况下不需要额外的加载指令而产生轻微的性能损失。不过,静态舍入总是比额外的好_mm512_roundscale_pdAVX512 缺少 instrinsic _mm512_round_ps)。


顺便说一句,这些限制(仅适用于标量或 512 位向量,并且仅适用于非内存指令)是 AVX512 拥有vcvttpd2qq而不是仅仅使用_MM_FROUND_TO_ZERO|_MM_FROUND_NO_EXCfor 的原因_mm512_cvt_roundpd_epi64。因为没有_mm256_cvt_roundpd_epi64,如果编译器可以将负载折叠到vcvttpd2qq.

还有历史先例:自 SSE1cvttss2si和以来cvttps2dq,英特尔已经进行了截断转换,这使得实现 C 的 FP->int 强制转换语义更加高效,而无需像我们过去使用 x87(在 SSE3 之前fisttp)那样更改 MXCSR 舍入模式。

在 AVX512 之前,从未支持涉及 64 位整数的打包转换,因此该指令不存在现有的 128 位或 256 位版本。不过,提供一个是一个很好的设计决定。

舍入覆盖是 AVX512 中的新功能。在此之前,使用 SSE4.1 /可以使用显式模式打包舍入到整数(输入和输出均为__m128or ) 。__m128droundpsroundpd


提高效率的替代实现:

添加而不是 sub

__m512i minus_start = _mm512_set1_epi64(-starting_epoch_milliseconds_);

 for(){ 
    __m512i data = _mm512_add_epi64(data, minus_start);
 }

add 是可交换的,因此编译器可以将 load 折叠成 load+add 指令,例如vpaddq zmm0, zmm8, [rdi],而不是单独的 load+sub。clang 为你做了这个优化,但gcc 没有


看起来您想将输入整数四舍五入到最接近的 3600 倍数。

用乘法代替除法

1.0/3600四舍五入到最接近double的是 about 2.777777777777777775368439616699e-04,在 2^53 中最多只有 0.5 个部分错误(的有效精度double)。大约是 10^-16。对于小于该lrint(x * (1.0/3600))值的输入,在 1 以内lrint(x / 3600.0)。对于大多数合理大小的输入,它们完全相等。

乘法后,您仍然会得到3600精确倍数,但是“除法”中有一个微小的错误,您最终可能会偏离 3600 的一倍。

您可以编写一个测试程序来查找除法与乘法逆运算得到不同结果的情况。


您可以将其作为另一次数据传递的一部分吗?对于所有内存带宽来说,计算量并不大。或者,如果您不能将 替换div_pd为乘以倒数,那么它将完全成为 FP 除法的瓶颈,而不会让其他执行单元保持忙碌。

这里有三种策略:

  • 纯整数,使用乘法逆进行精确除法。 为什么 GCC 在实现整数除法时使用乘以一个奇怪的数字?. Evan AVX512DQ 没有整数乘法,可以为您提供 64x64 => 128 的半部分,只有vpmullq64x64 => 64 位(而且它是多个微指令)。

    如果没有 AVX512IFMA VPMADD52HUQ(52x52=>52 位乘法的高半部分),请参阅我可以使用 AVX FMA 单元进行位精确的 52 位整数乘法吗?. (或者,如果您实际上只关心输入的低 32 位,那么 32x32=>64 位乘法和 64 位移位应该可以工作,使用_mm512_mul_epu32, single-uop vpmuludq。)但这也需要额外的工作才能四舍五入到最近而不是截断。

  • 你现在在做什么:double除(或乘以倒数),转换为最接近int64_t的 64 位整数乘法。

    如果 > 2^53,输入可能会四舍五入到最接近的double值,但最终结果将始终是 3600 的精确倍数(除非乘法溢出int64_t)。

  • double除(或乘),舍入到最接近的整数(不转换),double乘,转换为整数。

    如果最后一次乘法的结果高于 2^(53+4) ,则可能会出现问题。3600 是 2^4 的倍数,但不是 2^5 的倍数。因此,对于非常大的输入,四舍五入到最接近的可表示double可能给出的数字不是 3600 的精确倍数。

    如果范围限制不是问题,您甚至可以使用fma(val, 3600, -3600.0*start).

    SIMD FP 乘法比整数乘法具有明显更好的吞吐量,因此总体而言它可能是一个胜利,即使 FP 舍入到最近指令的额外成本也是如此。

您有时可以通过添加然后减去一个大常数来避免显式舍入指令,就像 @Mysticial 在Can I use the AVX FMA units to do bit-exact 52 bit integer multiplications 中所做的那样?. 您使该值足够大,以使其最接近的可表示doubles 是整数。(如何使用 SSE/AVX 有效地执行 double/int64 转换?对于有限范围的输入,还展示了一些 FP 操作技巧。)

也许我们可以rounded=fma(v, 1.0/3600, round_constant),然后减去round_constant得到一个四舍五入到最接近整数的值_mm512_roundscale_pd。我们甚至可以fma(rounded, 3600, -3600*round_constant)将其折叠成按比例放大: 2^52 * 3600 =4503599627370496.0 * 3600完全可以表示为double.

可能存在双舍入问题:首先在从转换int64_t到最接近double的整数时(如果它太大以至于整数不能精确表示),然后在除法和四舍五入到最接近的整数时再次出现。


成本:我假设您可以用乘以替换 FP 除法1.0/3600

  • fp mul,convert,整数 mul:vcvtqq2pd(FMA 端口为1 uop)+( vmulpd1 uop)+ vcvtpd2qq(1 uop)+ vpmullq(FMA 端口为 3 uop)=2 个 FMA 端口为 6 uop。 vpsubq zmm也竞争相同的端口,所以真的是 7。SKX uop 来自Agner Fog 的测试

  • fp 一切:vcvtqq2pd(FMA 端口为 1 uop)+ vmulpd(1 uop)+ vrndscalepd(2 uop)+ vmulpd(1 uop)+ vcvtpd2qq(1 uop)=6 uop,但延迟可能更低。(vrndscale+vmulpd 是 8+4 延迟,比 vpmullq 15 周期延迟快)。但是,如果在数组上循环独立向量,OoO exec 应该很容易隐藏这种延迟,因此节省延迟并不是什么大问题。

我不确定你可以如何高效地进行“整数”乘法,或者使用 FP bithacks 来避免转换指令。如果这对性能至关重要,那可能值得研究。


推荐阅读