【问题标题】:Gather AVX2&512 intrinsic for 16-bit integers?收集 16 位整数的 AVX2&512 内在函数?
【发布时间】:2020-04-07 21:53:44
【问题描述】:

想象一下这段代码:

void Function(int16 *src, int *indices, float *dst, int cnt, float mul)
{
    for (int i=0; i<cnt; i++) dst[i] = float(src[indices[i]]) * mul;
};

这确实需要收集内在函数,例如_mm_i32gather_epi32。在加载浮点数时,我在这些方面取得了巨大的成功,但是有 16 位整数吗?这里的另一个问题是我需要从输入的 16 位转换为输出的 32 位(浮点数)。

【问题讨论】:

    标签: optimization avx2 avx512


    【解决方案1】:

    确实没有收集 16 位整数的指令,但是(假设没有违反内存访问的风险)您可以从相应地址开始加载 32 位整数,并屏蔽每个值的上半部分。 对于uint16_t,这将是一个简单的位,对于有符号整数,您可以将值向左移动,以便将符号位置于最重要的位置。然后,您可以(算术)在将值转换为浮点数之前将它们移回,或者,因为无论如何您都将它们相乘,所以只需相应地缩放乘法因子。 或者,您可以提前从两个字节加载并以算术方式向右移动。无论哪种方式,您的瓶颈都可能是加载端口(@98​​7654324@ 需要 8 个加载 uops。连同索引的负载,您有 9 个负载分布在两个端口上,这应该导致 8 个元素的 4.5 个周期) .

    未经测试的可能 AVX2 实现(不处理最后一个元素,如果 cnt 不是 8 的倍数,则在最后执行原始循环):

    void Function(int16_t const *src, int const *indices, float *dst, size_t cnt, float mul_)
    {
        __m256 mul = _mm256_set1_ps(mul_*float(1.0f/0x10000));
        for (size_t i=0; i+8<=cnt; i+=8){ // todo handle last elements
            // load indicies:
            __m256i idx = _mm256_loadu_si256(reinterpret_cast<__m256i const*>(indices + i));
            // load 16bit integers in the lower halves + garbage in the upper halves:
            __m256i values = _mm256_i32gather_epi32(reinterpret_cast<int const*>(src), idx, 2);
            // shift each value to upper half (removes garbage, makes sure sign is at the right place)
            // values are too large by a factor of 0x10000
            values = _mm256_slli_epi32(values, 16);
            // convert to float, scale and multiply:
            __m256 fvalues = _mm256_mul_ps(_mm256_cvtepi32_ps(values), mul);
            // store result
            _mm256_storeu_ps(dst, fvalues);
        } 
    }
    

    将其移植到 AVX-512 应该很简单。

    【讨论】:

    • AVX2 vpgatherdd ymm 在 Skylake 上仅花费 4 uop(5c 吞吐量),显然每个 uop 执行多个缓存访问。所以瓶颈不是执行 ports 或前端,并且可以更好地与周围的工作重叠,而不是 vpinrsw 手动收集。但是是的,它在缓存读取端口或类似的东西上确实存在瓶颈,吞吐量略低于 2 次缓存读取/时钟。 SKX 上的 AVX512 版本对于 ZMM 是 9c,因此与 16 次缓存访问可能希望的 8 相比,再多 1 个周期。对于未对齐的双字,偶尔的缓存行拆分可能会使情况变得更糟。
    • 将移位校正比例因子烘焙到浮点 mul OP 无论如何都要使用的常量中的好主意。
    • 感谢@PeterCordes 的评论。我想我误解了uops.info/html-instr/VPGATHERDD_YMM_VSIB_YMM_YMM.html 中的+8*p23 - 或者它只是一个术语(即加载在技术上不是微操作?)该页面确实说它只需要 5uops(在 Skylake 或稍后),而不是 11 岁。
    • 哦,我只是在看 Agner 的指南,他没有完全分解后端端口的使用情况,只有哪些端口和 前端 的总数哎呀。是的,8*p23 确实为每个缓存访问使用后端加载端口微指令。这更有意义。但不知何故,它设法让这些微指令在后端执行,而无需通过前端发送那么多。这很酷。我想知道它是否让他们从后端的 RS 重播或其他什么; the same mechanism 用于处理缓存未命中和缓存行拆分。
    • (前端uop还不够微融合来解释。)不过不管怎样,是的,还是可以通过前端获取其他ALU uop来获取ALU工作与后端的聚集并行发生。加上不需要任何洗牌来组合双字块,这使得聚集在某些情况下很有用。
    猜你喜欢
    • 1970-01-01
    • 2015-04-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-06-12
    • 1970-01-01
    • 2014-03-29
    • 2012-10-21
    相关资源
    最近更新 更多