【问题标题】:how to convert uint32 to uint8 using simd but not avx512?如何使用 simd 而不是 avx512 将 uint32 转换为 uint8?
【发布时间】:2020-09-07 09:14:20
【问题描述】:

假设对齐内存uint32 *p中有很多uint32s存储,如何用simd将它们转换为uint8s?

我看到有_mm256_cvtepi32_epi8/vpmovdb 但是它属于avx512,我的cpu不支持它????

【问题讨论】:

  • 您究竟想如何转换它们?饱和或截断? 32 位值的范围是多少?
  • 将它们截断为 255
  • 您最好从vpshufb 开始。所有vpack... 指令都将它们的输入 视为有符号,即使它们对输出进行无符号饱和(如vpackusdw),所以0xFFFFFFFF 将有符号饱和到0 (-1到 0) 而不是 0xFFFF (UINT_MAX -> USHORT_MAX)
  • > 将它们截断为 255——这并不能说明问题。 256的值转换后应该是什么结果?
  • 我的意思是选择最低的 8 位,0x87654321 应该是 0x21

标签: sse simd avx avx2


【解决方案1】:

如果你真的有很多,我会做这样的事情(未经测试)。

主循环每次迭代读取 64 个字节,包含 16 个 uint32_t 值,围绕实现截断的字节进行混洗,将结果合并到单个寄存器中,并使用向量存储指令写入 16 个字节。

void convertToBytes( const uint32_t* source, uint8_t* dest, size_t count )
{
    // 4 bytes of the shuffle mask to fetch bytes 0, 4, 8 and 12 from a 16-bytes source vector
    constexpr int shuffleScalar = 0x0C080400;
    // Mask to shuffle first 8 values of the batch, making first 8 bytes of the result
    const __m256i shuffMaskLow = _mm256_setr_epi32( shuffleScalar, -1, -1, -1, -1, shuffleScalar, -1, -1 );
    // Mask to shuffle last 8 values of the batch, making last 8 bytes of the result
    const __m256i shuffMaskHigh = _mm256_setr_epi32( -1, -1, shuffleScalar, -1, -1, -1, -1, shuffleScalar );
    // Indices for the final _mm256_permutevar8x32_epi32
    const __m256i finalPermute = _mm256_setr_epi32( 0, 5, 2, 7, 0, 5, 2, 7 );

    const uint32_t* const sourceEnd = source + count;
    // Vectorized portion, each iteration handles 16 values.
    // Round down the count making it a multiple of 16.
    const size_t countRounded = count & ~( (size_t)15 );
    const uint32_t* const sourceEndAligned = source + countRounded;
    while( source < sourceEndAligned )
    {
        // Load 16 inputs into 2 vector registers
        const __m256i s1 = _mm256_load_si256( ( const __m256i* )source );
        const __m256i s2 = _mm256_load_si256( ( const __m256i* )( source + 8 ) );
        source += 16;
        // Shuffle bytes into correct positions; this zeroes out the rest of the bytes.
        const __m256i low = _mm256_shuffle_epi8( s1, shuffMaskLow );
        const __m256i high = _mm256_shuffle_epi8( s2, shuffMaskHigh );
        // Unused bytes were zeroed out, using bitwise OR to merge, very fast.
        const __m256i res32 = _mm256_or_si256( low, high );
        // Final shuffle of the 32-bit values into correct positions
        const __m256i res16 = _mm256_permutevar8x32_epi32( res32, finalPermute );
        // Store lower 16 bytes of the result
        _mm_storeu_si128( ( __m128i* )dest, _mm256_castsi256_si128( res16 ) );
        dest += 16;
    }

    // Deal with the remainder
    while( source < sourceEnd )
    {
        *dest = (uint8_t)( *source );
        source++;
        dest++;
    }
}

【讨论】:

  • 如果你正确地安排了你的 Epi8 洗牌,你应该能够用一个vpermd(或者甚至可能是vpermq)而不是@ 987654325@ + vpor。除非您正在为 Zen1 进行调整(其中车道提取非常便宜),否则只需 1 次 shuffle 就比 shuffle+or 更好。
  • 嗯,另一种选择是不同对齐的负载来提供字节混合 + vpshufb + vpermd。 IDK 如果这更好的话,虽然 Skylake 运行 vpblendvb 作为任何 ALU 端口的 2 微指令。使用 64 字节对齐的源,您可以对其进行排列,使所有负载都不是缓存行拆分。
  • @PeterCordes 我不会弄乱负载的。顺序 RAM 加载速度快的唯一原因是 CPU 中的预取器,密集对齐的顺序访问是该硬件的最佳情况。一旦你开始引入偏移量,你就会受到实施的支配,可能会也可能不会在性能方面做得很好。
  • 有趣的一点,这可能会引发 L1d 预取。但是主要的预取器位于 L2 中,它们只能看到来自 L1 的完整缓存行的请求流。但我想即使是 L1d 预取也可能还可以;您有一个展开的循环,其中每个负载自上次迭代以来都会看到 64 个字节的偏移量;负载彼此偏移 31 个字节的事实并不重要。我认为还有另一个问答,其中有人针对类似的问题实施了类似的交替对稍微重叠的负载 + 混合,并取得了良好的效果。
猜你喜欢
  • 1970-01-01
  • 2017-02-20
  • 2011-11-14
  • 2017-03-11
  • 2011-09-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多