有没有办法将 8bitX32 ymm 寄存器右/左洗牌 N 个位置(c++)

Posted

技术标签:

【中文标题】有没有办法将 8bitX32 ymm 寄存器右/左洗牌 N 个位置(c++)【英文标题】:Is there a way to shuffle a 8bitX32 ymm register right/left by N positions (c++) 【发布时间】:2021-02-12 22:17:52 【问题描述】:

就像标题所说的那样,我需要一种方法将 256-avx-register 寄存器中所有元素的位置移动/洗牌 N 个位置。我发现的所有关于这使用 32 或 64 位值 (__builtin_ia32_permvarsf256) 等的帮助将不胜感激。

Example: 2,4,4,2,4,5,0,0,0,0,... shift right by 4 -> 0,0,0,0,2,4,4,2,4,5,...

【问题讨论】:

你需要VPSHUFB instruction(或对应的C++内在函数_mm256_shuffle_epi8)。 如果 N 是常数,您还可以查看 _mm256_bslli_epi128/_mm256_bsrli_epi128_mm256_permute2x128_si256+_mm256_or_si256 来组合移位的车道。 不容易(直到 AVX-512 或 N 是 4 的倍数时才使用单次随机播放),通常更容易从内存重新加载并让加载执行单元处理未对齐,如果您的数据想要的还在记忆中。 感谢您的所有回答,请查看_mm256_shuffle_epi8。 N 不可能是常数。这是我在这方面遇到这么多麻烦的原因之一,到目前为止我发现的所有其他解决方案都要求 N 是一个常数。 (Peter Cordes) 你的意思是我应该把寄存器元素打成一个指针,把它洗牌,然后把它重新加载进去吗?这将是一个可能的解决方案,但我宁愿避免这样做 @lunave:(确保您@要回复的人,以便他们收到通知)。不,我的意思是如果您的原始向量刚刚从内存中加载,请考虑从不同的偏移量进行另一个重叠加载。如果不是,存储然后重新加载几乎没有那么好,需要更多的前端微指令来提高吞吐量。 (对于延迟,存储转发停止。)尽管对于非常量字节移位计数实际上值得考虑;这是延迟与吞吐量的权衡,最佳选择取决于周围的代码。 【参考方案1】:

如果在编译时知道移位距离,则相对容易且相当快。唯一需要注意的是,32 字节字节移位指令对 16 字节通道独立执行此操作,因为移位小于 16 字节需要跨通道传播这几个字节。这是左移:

// Move 16-byte vector to higher half of the output, and zero out the lower half
inline __m256i setHigh( __m128i v16 )

    const __m256i v = _mm256_castsi128_si256( v16 );
    return _mm256_permute2x128_si256( v, v, 8 );


template<int i>
inline __m256i shiftLeftBytes( __m256i src )

    static_assert( i >= 0 && i < 32 );
    if constexpr( i == 0 )
        return src;
    if constexpr( i == 16 )
        return setHigh( _mm256_castsi256_si128( src ) );
    if constexpr( 0 == ( i % 8 ) )
    
        // Shifting by multiples of 8 bytes is faster with shuffle + blend
        constexpr int lanes64 = i / 8;
        constexpr int shuffleIndices = ( _MM_SHUFFLE( 3, 2, 1, 0 ) << ( lanes64 * 2 ) ) & 0xFF;
        src = _mm256_permute4x64_epi64( src, shuffleIndices );
        constexpr int blendMask = ( 0xFF << ( lanes64 * 2 ) ) & 0xFF;
        return _mm256_blend_epi32( _mm256_setzero_si256(), src, blendMask );
    
    if constexpr( i > 16 )
    
        // Shifting by more than half of the register
        // Shift low half by ( i - 16 ) bytes to the left, and place into the higher half of the result.
        __m128i low = _mm256_castsi256_si128( src );
        low = _mm_slli_si128( low, i - 16 );
        return setHigh( low );
    
    else
    
        // Shifting by less than half of the register, using vpalignr to shift.
        __m256i low = setHigh( _mm256_castsi256_si128( src ) );
        return _mm256_alignr_epi8( src, low, 16 - i );
    

但是,如果在编译时不知道移位距离,这是相当棘手的。这是一种方法。它使用了很多 shuffle,但我希望它仍然比两个 32 字节存储(其中之一是写入零)后跟 32 字节加载的明显方式快一些。

// 16 bytes of 0xFF (which makes `vpshufb` output zeros), followed by 16 bytes of identity shuffle [ 0 .. 15 ], followed by another 16 bytes of 0xFF
// That data allows to shift 16-byte vectors by runtime-variable count of bytes in [ -16 .. +16 ] range
inline std::array<uint8_t, 48> makeShuffleConstants()

    std::array<uint8_t, 48> res;
    std::fill_n( res.begin(), 16, 0xFF );
    for( uint8_t i = 0; i < 16; i++ )
        res[ (size_t)16 + i ] = i;
    std::fill_n( res.begin() + 32, 16, 0xFF );
    return res;

// Align by 64 bytes so the complete array stays within cache line
static const alignas( 64 ) std::array<uint8_t, 48> shuffleConstants = makeShuffleConstants();

// Load shuffle constant with offset in bytes. Counterintuitively, positive offset shifts output of to the right.
inline __m128i loadShuffleConstant( int offset )

    assert( offset >= -16 && offset <= 16 );
    return _mm_loadu_si128( ( const __m128i * )( shuffleConstants.data() + 16 + offset ) );


// Move 16-byte vector to higher half of the output, and zero out the lower half
inline __m256i setHigh( __m128i v16 )

    const __m256i v = _mm256_castsi128_si256( v16 );
    return _mm256_permute2x128_si256( v, v, 8 );


inline __m256i shiftLeftBytes( __m256i src, int i )

    assert( i >= 0 && i < 32 );
    if( i >= 16 )
    
        // Shifting by more than half of the register
        // Shift low half by ( i - 16 ) bytes to the left, and place into the higher half of the result.
        __m128i low = _mm256_castsi256_si128( src );
        low = _mm_shuffle_epi8( low, loadShuffleConstant( 16 - i ) );
        return setHigh( low );
    
    else
    
        // Shifting by less than half of the register
        // Just like _mm256_slli_si256, _mm_shuffle_epi8 can't move data across 16-byte lanes, need to propagate shifted bytes manually.
        __m128i low = _mm256_castsi256_si128( src );
        low = _mm_shuffle_epi8( low, loadShuffleConstant( 16 - i ) );
        const __m256i cv = _mm256_broadcastsi128_si256( loadShuffleConstant( -i ) );
        const __m256i high = setHigh( low );
        src = _mm256_shuffle_epi8( src, cv );
        return _mm256_or_si256( high, src );
    

【讨论】:

第二种方法是我需要的!谢谢..虽然看起来很复杂 比 [两次存储 + 重新加载] 稍快 - 是的延迟,不一定是吞吐量。存储转发停顿会花费大约 10c 的延迟,但如果 OoO exec 可以隐藏它,那么它可能不会花费太多吞吐量。 (在阻止其他东西并行运行的意义上,这并不是真正的“停顿”。) 请注意,vperm2i128 (_mm256_permute2x128_si256) 可以使用立即数的第 3 位或第 7 位将任一通道归零,因此您可以将其用于常量 i &gt;= 16 情况,以及 256 位 @987654326 @。在除 Zen1 之外的所有 CPU 上,应该比您预期的 vpslldq xmm / vpxor zero / vinsertf128 更好。对于非常量的情况,我想你可以广播加载vpshufb 控制向量,这样洗牌就发生在上车道。 (希望_mm256_set1_si128( loadShuffleConstant( 16 - i ) ) 编译为 VBROADCASTI128) @PeterCordes 好主意,已更新。我的 Visual C++ 中没有 _mm256_set1_si128,但我已经确认 _mm256_broadcastsi128_si256 在发布版本中做了正确的事情,即 vbroadcasti128 ymm1,oword ptr [r8] 对此有了另一个想法:对于常量i 的情况,而不是&lt;&lt; / &gt;&gt; / OR,使用vperm2i128 来设置vpalignr。后者具有从相应通道移入字节的非常不便的行为,但是vperm2i128 到通道交换和零创建一个向量,其中包含正确的数据以为每个通道移入(零进入低通道,低通道进入高车道)。

以上是关于有没有办法将 8bitX32 ymm 寄存器右/左洗牌 N 个位置(c++)的主要内容,如果未能解决你的问题,请参考以下文章

测试 256 位 YMM AVX 寄存器为零的最有效/惯用方法

ASM x86_64 AVX:xmm 和 ymm 寄存器差异

有效地将 YMM 寄存器的低 64 位设置为常数

用于灰度到 ARGB 转换的 C++ SSE2 或 AVX2 内在函数

有没有办法将 128 位从内存直接加载到寄存器?

用 YMM 向量寄存器求和 vec4[idx[i]] * scalar[i]