C++ 将整数范围映射到另一个范围

C++ 将整数范围映射到另一个范围,c++,assembly,optimization,x86-64,simd,C++,Assembly,Optimization,X86 64,Simd,在运行时,我有两个范围,由它们的uint32\t边界a..b和c..d定义。第一个范围往往比第二个范围大得多:8=d-c时,保持比率尽可能接近理想值很重要,否则当[a;b]中的元素可以映射到[c;d]中的多个整数上时,可以返回这些整数中的任何一个 听起来像是一个简单的比率问题,已经在许多问题中得到了回答,如 但这里我需要一个非常快速的解决方案 此例程是专用排序算法的关键部分,对于已排序数组的每个元素至少调用一次 如果SIMD解决方案不会降低总体性能,那么它也是可以接受的。实际运行时划分(FP和

在运行时,我有两个范围,由它们的
uint32\t
边界
a..b
c..d
定义。第一个范围往往比第二个范围大得多:
8<(b-a)/(d-c)<64

精确限值:
a>=0
b=0
d四舍五入到32舍五入((浮动)(x-a)/(b-a)*(d-c)+c)

b-a>=d-c
时,保持比率尽可能接近理想值很重要,否则当
[a;b]
中的元素可以映射到
[c;d]
中的多个整数上时,可以返回这些整数中的任何一个

听起来像是一个简单的比率问题,已经在许多问题中得到了回答,如

但这里我需要一个非常快速的解决方案

此例程是专用排序算法的关键部分,对于已排序数组的每个元素至少调用一次

如果SIMD解决方案不会降低总体性能,那么它也是可以接受的。

实际运行时划分(FP和integer)非常慢,所以您一定要避免这种情况。您编写该表达式的方式可能编译为包含除法,因为FP-math不是关联的(没有
-ffast-math
);编译器无法为您将
x/foo*bar
转换为
x*(bar/foo)
,即使这对于循环不变
bar/foo
非常好。您确实需要浮点或64位整数来避免乘法中的溢出,但只有FP允许重用非整数循环不变除法结果

\u mm256\u fmadd\u ps
看起来是一条显而易见的道路,它为乘法器
(d-c)/(b-a)
预先计算了一个循环不变值。如果严格按顺序(先乘后除)舍入不是问题,那么在循环之外,首先进行这种不精确的除法可能是可以的。像
\u mm256\u set1\u ps((d-c)/(double)(b-a))
。在此计算中使用
double
,可以避免在除法操作数转换为FP时出现舍入错误

您正在对许多
x
重复使用相同的a、b、c、d,可能来自连续内存。不幸的是,您将结果用作内存地址的一部分,因此最终需要将结果从SIMD返回到整数寄存器中。(使用AVX512分散存储可能可以避免这种情况。)

现代x86 CPU具有2/时钟负载吞吐量,因此,让8x uint32_t返回整数寄存器的最佳选择可能是向量存储/整数重新加载,而不是每个元素花费2个UOP来进行ALU随机操作。这有一些延迟,所以我建议在循环通过该标量之前,将其转换为16或32整数(64或128字节)的tmp缓冲区,即2x或4x
\uuuu m256i

或者交替转换和存储一个向量,然后在之前转换的另一个向量的8个元素上循环。i、 e.软件流水线。无序执行可以隐藏延迟,但您已经在扩展它的延迟隐藏功能,以便在内存中进行任何缓存未命中操作

根据您的CPU(例如Haswell或某些Skylake),使用256位矢量指令可能会使您的max turbo稍低于其他情况。你可能会考虑只做4的向量,但是每次元素花费更多。

如果不是SIMD,那么即使标量C++<代码> fMAS-()/代码>仍然是好的,对于<代码> VFMADD213SD,但是使用内蕴是一种非常方便的方法,从FLUT-INT(C++代码> VCVTPS2DQ而不是<代码> VCVTPS2DQ < /COD>)获得舍入(而不是截断)。


请注意,在AVX512之前,
uint32\t
float
转换不直接可用。对于标量,您只需使用截断/零扩展将无符号低半部分转换为/从int64\t转换

非常方便的是(如注释中所讨论的),您的输入是范围有限的,因此如果您将它们解释为有符号整数,它们具有相同的值(有符号非负)。已知
x
x-a
(和
b-a
)都是正数,实际运行时除法(FP和integer)非常慢,因此您一定要避免这种情况。您编写该表达式的方式可能编译为包含除法,因为FP-math不是关联的(没有
-ffast-math
);编译器无法为您将
x/foo*bar
转换为
x*(bar/foo)
,即使这对于循环不变
bar/foo
非常好。您确实需要浮点或64位整数来避免乘法中的溢出,但只有FP允许重用非整数循环不变除法结果

\u mm256\u fmadd\u ps
看起来是一条显而易见的道路,它为乘法器
(d-c)/(b-a)
预先计算了一个循环不变值。如果严格按顺序(先乘后除)舍入不是问题,那么在循环之外,首先进行这种不精确的除法可能是可以的。像
\u mm256\u set1\u ps((d-c)/(double)(b-a))
。在此计算中使用
double
,可以避免在除法操作数转换为FP时出现舍入错误

您正在对许多
x
重复使用相同的a、b、c、d,可能来自连续内存。不幸的是,您将结果用作内存地址的一部分,因此最终需要将结果从SIMD返回到整数寄存器中。(使用AVX512分散存储可能可以避免这种情况。)

现代x86 CPU具有2/时钟负载吞吐量,因此,让8x uint32_t返回整数寄存器的最佳选择可能是向量存储/整数重新加载,而不是每个元素花费2个UOP来进行ALU随机操作。这会有一些延迟,因此我建议将其转换为16或32整数(64或128字节)的tmp缓冲区,即2x或4x
\uuuu m256i
b
// fastest but not safe if b-a is small and  a > 2^24
static inline
__m256i range_scale_fast_fma(__m256i data, uint32_t a, uint32_t b, uint32_t c, uint32_t d)
{
     // avoid rounding errors when computing the scale factor, but convert double->float on the final result
    double scale_scalar = (d - c) / (double)(b - a);
    const __m256 scale = _mm256_set1_ps(scale_scalar);
    const __m256 add = _m256_set1_ps(-a*scale_scalar + c);
    //    (x-a) * scale + c
    // =  x * scale + (-a*scale + c)   but with different rounding error from doing -a*scale + c

    __m256  in = _mm256_cvtepi32_ps(data);
    __m256  out = _mm256_fmadd_ps(in, scale, add);
    return _mm256_cvtps_epi32(out);   // convert back with round to nearest-even
                                   // _mm256_cvttps_epi32 truncates, matching C rounding; maybe good for scalar testing
}
static inline
__m256i range_scale_safe_fma(__m256i data, uint32_t a, uint32_t b, uint32_t c, uint32_t d)
{
     // avoid rounding errors when computing the scale factor, but convert double->float on the final result
    const __m256 scale = _mm256_set1_ps((d - c) / (double)(b - a));
    const __m256 cvec = _m256_set1_ps(c);

    __m256i in_offset = _mm256_add_epi32(data, _mm256_set1_epi32(-a));  // add can more easily fold a load of a memory operand than sub because it's commutative.  Only some compilers will do this for you.
    __m256  in_fp = _mm256_cvtepi32_ps(in_offset);
    __m256  out = _mm256_fmadd_ps(in_fp, scale, _mm256_set1_ps(c));  // in*scale + c
    return _mm256_cvtps_epi32(out);
}
void foo(uint32_t *arr, ptrdiff_t len)
{
    if (len < 24) special case;

    alignas(32) uint32_t tmpbuf[16];

    // peel half of first iteration for software pipelining / loop rotation
    __m256i arrdata = _mm256_loadu_si256((const __m256i*)&arr[0]);
    __m256i outrange = range_scale_safe_fma(arrdata);
    _mm256_store_si256((__m256i*)tmpbuf, outrange);

    // could have used an unsigned loop counter
    // since we probably just need an if() special case handler anyway for small len which could give len-23 < 0
    for (ptrdiff_t i = 0 ; i < len-(15+8) ; i+=16 ) {

        // prep next 8 elements
        arrdata = _mm256_loadu_si256((const __m256i*)&arr[i+8]);
        outrange = range_scale_safe_fma(arrdata);
        _mm256_store_si256((__m256i*)&tmpbuf[8], outrange);

        // use first 8 elements
        for (int j=0 ; j<8 ; j++) {
            use tmpbuf[j] which corresponds to arr[i+j]
        }

        // prep 8 more for next iteration
        arrdata = _mm256_loadu_si256((const __m256i*)&arr[i+16]);
        outrange = range_scale_safe_fma(arrdata);
        _mm256_store_si256((__m256i*)&tmpbuf[0], outrange);

        // use 2nd 8 elements
        for (int j=8 ; j<16 ; j++) {
            use tmpbuf[j] which corresponds to arr[i+j]
        }
    }

    // use tmpbuf[0..7]
    // then cleanup: one vector at a time until < 8 or < 4 with 128-bit vectors, then scalar
}