GCC keeps complaining "error: incorrect rounding operand" for a AVX512 functions _mm512_cvt_roundpd_epi64

GCC keeps complaining "error: incorrect rounding operand" for a AVX512 functions _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: error: incorrect rounding operand __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库的朋友帮忙解答一下吗?

仅供参考,您通常不需要显式舍入到最近。默认模式是舍入到最近的,所有异常都被屏蔽。除非您使用 fenv_MM_SET_ROUNDING_MODE 更改了此线程中的默认舍入模式或异常掩码,否则纯 _mm512_cvtepi64_pd_mm512_cvtpd_epi64 的行为将与您正在做的相同。

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

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

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


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

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

_MM_FROUND_CUR_DIRECTION 不算数,它意味着 "no override",就好像你使用了正常的非 round 版本的内部函数一样。)

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

Rounding is done according to the rounding parameter, which can be one of:

(_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(注意截断的额外t),只有_MM_FROUND_NO_EXC(或_MM_FROUND_CUR_DIRECTION)是允许,因为该行为不受 2 位字段值的影响,只受是否指定舍入覆盖的影响。


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

From Intel's vol.2 instruction set reference manual:

2.6.8 Static Rounding Support in EVEX

Static rounding control embedded in the EVEX encoding system applies only to register-to-register flavor of floating-point instructions with rounding semantic at two distinct vector lengths: (i) scalar, (ii) 512-bit. In both cases, the field EVEX.L’L expresses rounding mode control overriding MXCSR.RC if EVEX.b is set. When EVEX.b is set, “suppress all exceptions” is implied.

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

你的情况不是问题;数据来自 _mm512_sub_epi64,但一般来说值得指出的是,覆盖已经默认的舍入模式可能会产生轻微的性能损失,因为在某些情况下需要额外的加载指令,否则它不会被需要。尽管 ().

,静态舍入总是比额外的 _mm512_roundscale_pd 更好

顺便说一句,这些限制(仅适用于标量或 512 位向量,并且仅适用于非内存指令)是 AVX512 具有 vcvttpd2qq 而不是仅使用 [=41] 的原因=] 对于 _mm512_cvt_roundpd_epi64。因为没有 _mm256_cvt_roundpd_epi64,如果编译器可以将负载折叠到 vcvttpd2qq 的内存操作数中,偶尔会很好。

还有历史先例:自 SSE1 cvttss2sicvttps2dq 以来,Intel 已经进行了截断转换,这使得 更有效地实现 C 的 FP- >int 转换语义而不改变 MXCSR 舍入模式,就像我们以前必须使用 x87 的方式(在 SSE3 fisttp 之前)。

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

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


提高效率的替代实现:

添加而不是子

__m512i minus_start = _mm512_set1_epi64(-starting_epoch_milliseconds_);

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

add 是可交换的,因此编译器可以像 vpaddq zmm0, zmm8, [rdi] 一样将加载折叠成加载+添加指令,而不是单独的加载+子指令。 clang 为您做了这个优化,但是 gcc doesn't


您似乎想将输入的整数四舍五入为最接近的 3600 的倍数。

用乘法代替除法

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

相乘后你仍然总是会得到 3600 的精确倍数,但如果 "division" 中有一个小错误,你最终可能会偏离 3600 的一倍.

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


你能把这作为另一次数据传递的一部分吗?对于所有内存带宽来说,计算量并不大。或者,如果您不能将 div_pd 替换为乘以倒数,则在不让其他执行单元忙碌的情况下,它完全是 FP 除法的瓶颈。

这里有三种策略:

  • 纯整数,使用乘法逆进行精确除法。 Why does GCC use multiplication by a strange number in implementing integer division?。 Evan AVX512DQ 没有整数乘法,它给你 high 64x64 => 128 的一半,只有 vpmullq 64x64 => 64 位(而且它是多个 uops) .

    没有 AVX512IFMA VPMADD52HUQ (the high half of a 52x52=>52-bit multiply), see 。 (或者如果你实际上只关心输入的低 32 位,那么 32x32=>64 位乘法和 64 位移位应该可以工作,使用 _mm512_mul_epu32,单 uop vpmuludq。)但这会还需要额外的工作来四舍五入而不是截断。

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

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

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

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

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

    折叠减法

    SIMD FP 乘法的吞吐量明显优于整数乘法,因此它可能是总体上的胜利,即使有 FP 舍入到最近指令的额外成本。

您有时可以通过添加然后减去一个大常数来避免显式舍入指令,就像@Mysticial 在 . You make the value big enough that it the nearest representable doubles are whole integers. ( 中所做的那样,对于有限范围的输入,也展示了一些 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 时(如果它太大以至于整数不能完全表示),然后在除法时再次并四舍五入到最接近的整数。


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

  • fp mul,转换,整数 mul:vcvtqq2pd(FMA 端口为 1 uop)+ vmulpd(1 uop)+ vcvtpd2qq(1 uop) + vpmullq(FMA 端口为 3 微指令)= 2 个 FMA 端口为 6 微指令。 vpsubq zmm 也竞争相同的端口,所以真的有 7 个。 SKX uop 从 Agner Fog's testing.

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

我不确定 "integer" 乘法或使用 FP bithacks 来避免转换指令的效率如何。如果这是性能关键,那可能值得研究。