只保留 16 位字中的 10 个有用位

Keep only the 10 useful bits in 16-bit words

我有 _m256i 向量,在 16 位整数中包含 10 位字(因此 16*16 位仅包含 16*10 有用位)。 best/fastest 仅提取那些 10 位并将它们打包以生成 10 位值的输出比特流的方法是什么?

这是我的尝试。

尚未进行基准测试,但我认为它总体上应该运行得相当快:指令不多,所有指令在现代处理器上都有 1 个周期的延迟。存储也很高效,20 字节数据的 2 条存储指令。

该代码仅使用了 3 个常量。如果你在循环中调用这个函数,好的编译器应该在循环之外加载所有三个并将它们保存在寄存器中。

// bitwise blend according to a mask
inline void combineHigh( __m256i& vec, __m256i high, const __m256i lowMask )
{
    vec = _mm256_and_si256( vec, lowMask );
    high = _mm256_andnot_si256( lowMask, high );
    vec = _mm256_or_si256( vec, high );
}

// Store 10-bit pieces from each of the 16-bit lanes of the AVX2 vector.
// The function writes 20 bytes to the pointer.
inline void store_10x16_avx2( __m256i v, uint8_t* rdi )
{
    // Pack pairs of 10 bits into 20, into 32-bit lanes
    __m256i high = _mm256_srli_epi32( v, 16 - 10 );
    const __m256i low10 = _mm256_set1_epi32( ( 1 << 10 ) - 1 ); // Bitmask of 10 lowest bits in 32-bit lanes
    combineHigh( v, high, low10 );

    // Now the vector contains 32-bit lanes with 20 payload bits / each
    // Pack pairs of 20 bits into 40, into 64-bit lanes
    high = _mm256_srli_epi64( v, 32 - 20 );
    const __m256i low20 = _mm256_set1_epi64x( ( 1 << 20 ) - 1 ); // Bitmask of 20 lowest bits in 64-bit lanes
    combineHigh( v, high, low20 );

    // Now the vector contains 64-bit lanes with 40 payload bits / each
    // 40 bits = 5 bytes, store initial 4 bytes of the result
    _mm_storeu_si32( rdi, _mm256_castsi256_si128( v ) );

    // Shuffle the remaining 16 bytes of payload into correct positions.
    // The indices of the payload bytes are [ 0 .. 4 ] and [ 8 .. 12 ]
    // _mm256_shuffle_epi8 can only move data within 16-byte lanes
    const __m256i shuffleIndices = _mm256_setr_epi8(
        // 6 remaining payload bytes from the lower half of the vector
        4, 8, 9, 10, 11, 12,
        // 10 bytes gap, will be zeros
        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
        // 6 bytes gap, will be zeros
        -1, -1, -1, -1, -1, -1,
        // 10 payload bytes from the higher half of the vector
        0, 1, 2, 3, 4,
        8, 9, 10, 11, 12
    );
    v = _mm256_shuffle_epi8( v, shuffleIndices );

    // Combine and store the final 16 bytes of payload
    const __m128i low16 = _mm256_castsi256_si128( v );
    const __m128i high16 = _mm256_extracti128_si256( v, 1 );
    const __m128i result = _mm_or_si128( low16, high16 );
    _mm_storeu_si128( ( __m128i* )( rdi + 4 ), result );
}

此代码截断值的未使用的高 6 位。


如果你想饱和,你还需要一条指令,_mm256_min_epu16

此外,如果您这样做,函数的第一步可以使用 pmaddwd。这是使源数字饱和的完整函数,并进行了一些额外的调整。

// Store 10-bit pieces from 16-bit lanes of the AVX2 vector, with saturation.
// The function writes 20 bytes to the pointer.
inline void store_10x16_avx2( __m256i v, uint8_t* rdi )
{
    const __m256i low10 = _mm256_set1_epi16( ( 1 << 10 ) - 1 );
#if 0
    // Truncate higher 6 bits; pmaddwd won't truncate, it needs zeroes in the unused higher bits.
    v = _mm256_and_si256( v, low10 );
#else
    // Saturate numbers into the range instead of truncating
    v = _mm256_min_epu16( v, low10 );
#endif

    // Pack pairs of 10 bits into 20, into 32-bit lanes
    // pmaddwd computes a[ 0 ] * b[ 0 ] + a[ 1 ] * b[ 1 ] for pairs of 16-bit lanes, making a single 32-bit number out of two pairs.
    // Initializing multiplier with pairs of [ 1, 2^10 ] to implement bit shifts + packing
    const __m256i multiplier = _mm256_set1_epi32( 1 | ( 1 << ( 10 + 16 ) ) );
    v = _mm256_madd_epi16( v, multiplier );

    // Now the vector contains 32-bit lanes with 20 payload bits / each
    // Pack pairs of 20 bits into 40 in 64-bit lanes
    __m256i low = _mm256_slli_epi32( v, 12 );
    v = _mm256_blend_epi32( v, low, 0b01010101 );
    v = _mm256_srli_epi64( v, 12 );

    // Now the vector contains 64-bit lanes with 40 payload bits / each
    // 40 bits = 5 bytes, store initial 4 bytes of the result
    _mm_storeu_si32( rdi, _mm256_castsi256_si128( v ) );

    // Shuffle the remaining 16 bytes of payload into correct positions.
    const __m256i shuffleIndices = _mm256_setr_epi8(
        // Lower half
        4, 8, 9, 10, 11, 12,
        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
        // Higher half
        -1, -1, -1, -1, -1, -1,
        0, 1, 2, 3, 4,
        8, 9, 10, 11, 12
    );
    v = _mm256_shuffle_epi8( v, shuffleIndices );

    // Combine and store the final 16 bytes of payload
    const __m128i low16 = _mm256_castsi256_si128( v );
    const __m128i high16 = _mm256_extracti128_si256( v, 1 );
    const __m128i result = _mm_or_si128( low16, high16 );
    _mm_storeu_si128( ( __m128i* )( rdi + 4 ), result );
}

根据处理器、编译器和调用该函数的代码,这在总体上可能会稍快或稍慢,但绝对有助于减少代码大小。没有人再关心二进制大小,但 CPU 的 L1I 和 µop 缓存有限。


为了完整起见,这是另一个使用 SSE2 和可选的 SSSE3 而不是 AVX2 的方法,实际上速度稍慢。

// Compute v = ( v & lowMask ) | ( high & ( ~lowMask ) ), for 256 bits of data in two registers
inline void combineHigh( __m128i& v1, __m128i& v2, __m128i h1, __m128i h2, const __m128i lowMask )
{
    v1 = _mm_and_si128( v1, lowMask );
    v2 = _mm_and_si128( v2, lowMask );
    h1 = _mm_andnot_si128( lowMask, h1 );
    h2 = _mm_andnot_si128( lowMask, h2 );
    v1 = _mm_or_si128( v1, h1 );
    v2 = _mm_or_si128( v2, h2 );
}

inline void store_10x16_sse( __m128i v1, __m128i v2, uint8_t* rdi )
{
    // Pack pairs of 10 bits into 20, in 32-bit lanes
    __m128i h1 = _mm_srli_epi32( v1, 16 - 10 );
    __m128i h2 = _mm_srli_epi32( v2, 16 - 10 );
    const __m128i low10 = _mm_set1_epi32( ( 1 << 10 ) - 1 );
    combineHigh( v1, v2, h1, h2, low10 );

    // Pack pairs of 20 bits into 40, in 64-bit lanes
    h1 = _mm_srli_epi64( v1, 32 - 20 );
    h2 = _mm_srli_epi64( v2, 32 - 20 );
    const __m128i low20 = _mm_set1_epi64x( ( 1 << 20 ) - 1 );
    combineHigh( v1, v2, h1, h2, low20 );

#if 1
    // 40 bits is 5 bytes, for the final shuffle we use pshufb instruction from SSSE3 set
    // If you don't have SSSE3, below under `#else` there's SSE2-only workaround.
    const __m128i shuffleIndices = _mm_setr_epi8(
        0, 1, 2, 3, 4,
        8, 9, 10, 11, 12,
        -1, -1, -1, -1, -1, -1 );
    v1 = _mm_shuffle_epi8( v1, shuffleIndices );
    v2 = _mm_shuffle_epi8( v2, shuffleIndices );
#else
    // SSE2-only version of the above, uses 8 instructions + 2 constants to emulate 2 instructions + 1 constant
    // Need two constants because after this step we want zeros in the unused higher 6 bytes.
    h1 = _mm_srli_si128( v1, 3 );
    h2 = _mm_srli_si128( v2, 3 );
    const __m128i low40 = _mm_setr_epi8( -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 );
    const __m128i high40 = _mm_setr_epi8( 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0 );
    const __m128i l1 = _mm_and_si128( v1, low40 );
    const __m128i l2 = _mm_and_si128( v2, low40 );
    h1 = _mm_and_si128( h1, high40 );
    h2 = _mm_and_si128( h2, high40 );
    v1 = _mm_or_si128( h1, l1 );
    v2 = _mm_or_si128( h2, l2 );
#endif

    // Now v1 and v2 vectors contain densely packed 10 bytes / each.
    // Produce final result: 16 bytes in the low part, 4 bytes in the high part
    __m128i low16 = _mm_or_si128( v1, _mm_slli_si128( v2, 10 ) );
    __m128i high16 = _mm_srli_si128( v2, 6 );
    // Store these 20 bytes with 2 instructions
    _mm_storeu_si128( ( __m128i* )rdi, low16 );
    _mm_storeu_si32( rdi + 16, high16 );
}

在循环中,您可能希望使用部分重叠的存储,这些存储会写入每个源数据向量的 20 字节目标的末尾之后。这节省了跨 16 字节边界混洗数据以设置 16 + 4 字节存储的工作。

(@Soont 的更新答案有一个 vmovd 和一个 vmovdqu 存储非常好,总共只有 2 个洗牌 uops,包括 vpshufbvextracti128。当我最初写这个的时候,我们还没有想到一个好的方法来避免存储在 20 字节之外而不花费更多的 shuffle uops,这会造成比前端更糟糕的瓶颈。但是 vmovdqu + vextracti128 mem, ymm, 1 (2 uops 不是微融合)仍然稍微便宜一些:vpshufb 之后是 3 uops 而不是 4。)

或者展开可能适用于大型数组,LCM(20,16) = 80,因此对于大型展开(以及其中每个位置的不同洗牌控制向量),您可能只对齐 16 字节商店。但这可能需要大量的改组,包括在可能带有 palignr.

的源块之间

两个重叠的 16 字节存储示例

将其用作循环体,可以覆盖超过 20 个字节。

#include <immintrin.h>
#include <stdint.h>

// Store 10-bit pieces from each of the 16-bit lanes of the AVX2 vector.
// The function writes 20 useful bytes to the pointer
// but actually steps on data out to 26 bytes from dst
void pack10bit_avx2_store26( __m256i v, uint8_t* dst)
{
    // clear high garbage if elements aren't already zero-extended   
    //v = _mm256_and_si256(v, _mm256_set1_epi16( (1<<10)-1) );

    ... prep data somehow; pmaddwd + a couple shifts is good for throughput

    // Now the vector contains 64-bit lanes with 40 payload bits / each; 40 bits = 5 bytes.
    // Shuffle these bytes into a very special order.
    // Note _mm256_shuffle_epi8 can only move data within 16-byte lanes.
    const __m256i shuffleIndices = _mm256_setr_epi8(
        // 6 bytes gap with zeros
        // Pack the two 5-byte chunks into the bottom of each 16-byte lane
        0, 1, 2, 3, 4,
        8, 9, 10, 11, 12,
        -1, -1, -1, -1, -1, -1,

        0, 1, 2, 3, 4,
        8, 9, 10, 11, 12,
        -1, -1, -1, -1, -1, -1);
    v = _mm256_shuffle_epi8(v, shuffleIndices );

    // Split the vector into halves
    __m128i low16 = _mm256_castsi256_si128( v );
    _mm_storeu_si128( ( __m128i* )dst, low16 );        // vmovdqu      mem, xmm

    __m128i high16 = _mm256_extracti128_si256( v, 1 );
    _mm_storeu_si128( ( __m128i* )(dst+10), high16 );   // vextracti128 mem, ymm, 1

    // An AVX-512 masked store could avoid writing past the end
}

我们可以通过将其编译为独立函数 (https://godbolt.org/z/8T7KhT) 来了解它如何内联到循环中。

# clang -O3 -march=skylake
pack10bit_avx2(long long __vector(4), unsigned char*):
       # vpand  commented out
        vpmaddwd        ymm0, ymm0, ymmword ptr [rip + .LCPI0_0]
         ... # work in progress, original PMADDWD idea ignored some limitations!  See Soonts' answer

        vpshufb ymm0, ymm0, ymmword ptr [rip + .LCPI0_1] # ymm0 = ymm0[0,1,2,3,4,8,9,10,11,12],zero,zero,zero,zero,zero,zero,ymm0[16,17,18,19,20,24,25,26,27,28],zero,zero,zero,zero,zero,zero
        vmovdqu xmmword ptr [rdi], xmm0
        vextracti128    xmmword ptr [rdi + 10], ymm0, 1

        vzeroupper               # overhead that goes away when inlining into a loop
        ret

在循环中,编译器会将这 2 个向量常量加载到寄存器中,希望使用广播加载。

与一些更宽的整数乘法或水平加法不同,vpmaddwd 作为具有 5 个周期延迟的单个 uop 进行有效处理。 https://uops.info/

vextracti128 存储无法在 Intel 上进行微融合,但与 vpextrd 不同的是,它不涉及 shuffle uop。只是存储地址和存储数据。 Zen2 也 运行 将其设置为 2 微指令,不幸的是每 2 个周期吞吐量为 1。 (比Zen1还差)。

在 Ice Lake 之前,Intel 和 AMD 都可以 运行 每个时钟存储 1 个。


如果您确实想要将打包的数据放回寄存器中,您可能需要使用 palignr 进行@Soont 的原始随机播放,或者您可以先执行此操作,然后重新加载。延迟会更高(特别是因为存储转发在重新加载时停滞),但如果您的块是几个寄存器的数据,那么它应该重叠甚至隐藏延迟,也许给存储时间甚至提交到 L1d 而不是导致重新加载时停顿。


BMI2 pext

uint64_t packed = _pext_u64(x, 0x03FF03FF03FF03FF);

可能适用于标量清理或一小块 4 像素或其他内容。这给您带来了进行 5 字节存储(或带有尾随零的 8 字节存储)的问题。如果使用它,请注意严格别名和对齐,例如使用 memcpy 将未对齐的 may-alias 数据放入 uint64_t,或制作 __attribute__((aligned(1),may_alias)) typedef。

pext 在 Intel 上非常有效(1 uop,3c 延迟),但 在 AMD 上非常糟糕 ,比只使用一个 SIMD 的低部分更糟糕步骤。


AVX-512

AVX512VBMI(冰湖)会给你 vpermb(车道交叉口)而不是 vpshufb。 (Skylake-X / Cascade Lake 上 vpermw 的 AVX512BW 要求您已经组合成偶数个字节,即使在 vpermb 为 1 的 Ice Lake 上也是 2 微码,所以非常糟糕.) vpermb 可以设置一个未对齐的 32 字节存储(有 20 个有用字节),您可以在循环中重叠它。

AVX-512 存储可以被有效地屏蔽以不实际上覆盖结尾,例如使用双字掩码。 vmovdqu32 [rdi]{k}, ymm0 在 Skylake-X 上是 1 uop。但是 AVX2 vmaskmovd 即使在 Intel 上也是几个微指令,而在 AMD 上非常昂贵,所以你不想那样做。双字掩码只有在您为一个存储准备好所有 20 个字节时才有效,否则您至少需要 16 位粒度。

其他 AVX-512 指令:VBMI vpmultishiftqb,一种并行位域提取,似乎很有用,但它只能从未对齐但连续的源块写入对齐的 8 位目标块。我不认为这比我们可以用可变移位和旋转做的更好。 vpmultishiftqb 会让我们 解压 这种格式(这个函数的反函数) 可能需要 2 条指令: 1 条随机播放(例如 vpexpandbvpermb) 将所需的数据放入向量中的每个 qword,并进行一次多移位以获取每个单词底部的正确 10 位字段。

AVX-512 具有可变计数移位和旋转,包括字(16 位)粒度,因此这将是第一步而不是 vpmaddwd 的一个选项。 使用轮班免费忽略高垃圾。它具有较低的延迟,并且直接版本的合并屏蔽可以取代对控制向量的需要。 (但是你需要一个掩码常量)。

使用屏蔽时,延迟为 3 个周期,而没有屏蔽时为 1 个周期,并且 AVX-512 使得从即时广播控制向量与 mov reg,imm / kmov kreg, reg 一样高效。例如mov reg,imm / vpbroadcastd ymm, reg(1 微指令)。合并屏蔽还限制优化器覆盖目标寄存器而不是复制和移位,尽管这在这里无关紧要如果优化器很聪明。两种方式都不允许将数据的加载折叠到内存源操作数中进行移位:sllvw 只能从内存中获取计数,而 sllw 需要合并到寄存器中的原始操作数。

Shifts 可以 运行 在 Intel 的端口 0 或 1 上(AMD 不支持 AVX-512)。或者仅用于 512 位微指令的端口 0,在任何 512 位微指令运行时关闭用于任何矢量 ALU 微指令的端口 1。因此,对于此的 __m512i 版本,端口 0 存在潜在的吞吐量瓶颈,但对于 256 位,有足够的其他微指令(洗牌和存储,如果对数据数组执行此操作,可能会产生循环开销),这应该分布相当均匀。

这个移位部分(在_mm256_permutexvar_epi8之前)只需要AVX-512BW(+VL),并且可以在Skylake-X上工作。它将数据留在与其他方法相同的地方,因此是一个直接替代品,您可以混合搭配各种策略。

// Ice Lake.  Could work on __m512i but then shifts could only run on p0, not p0/p1,
  //  and almost every store would be a cache line split.
inline void store_10x16_avx512vbmi( __m256i v, uint8_t* dst )
{
// no _mm256_and_si256 needed, we safely ignore high bits
   // v = [ ?(6) ... B[9:0] | ?(6) ... A[9:0] ] repeated
   v = _mm256_sllv_epi16(v, _mm256_set1_epi32((0<<16) | 6));  // alternative: simple repeated-pattern control vector
      // v =  _mm256_mask_slli_epi16(v, 0x5555, v, 6);   // merge-masking, updating only elements 0,2, etc.
   // v = [ ?(6) ... B[9:0] | A[9:0] ... 0(6) ] repeated
   v = _mm256_rolv_epi32(v, _mm256_set1_epi64x(((32ULL-6)<<32) | 6));  // top half right, bottom half left
   // v = [ 0(6) .. ?(6) .. D[9:0] | C[9:0] | B[9:0] | A[9:0] ... 0(12) ] repeated
   v = _mm256_srli_epi64(v, 12);    // 40 bit chunks at the bottom of each qword

   const __m256i permb = _mm256_setr_epi8( 0, 1, 2, 3, 4,   8, 9,10,11,12,
                                          16,17,18,19,20,  24,25,26,27,28,
                                          28,28,28,28,28,28,28,28,28,28,28,28 );
    // repeat last byte as filler.  vpermb can't zero (except by maskz) but we can do a masked store
   v = _mm256_permutexvar_epi8(v, permb);  // AVX512_VBMI
   _mm256_mask_storeu_epi32( dst, 0x1F, v);  // 32-bit masking granularity in case that's cheaper for HW.  20 bytes = 5 dwords.
}

这样编译 (Godbolt):

# clang -O3 -march=icelake-client.  GCC is essentially the same.
store_10x16_avx512vbmi(long long __vector(4), unsigned char*):
        vpsllvw ymm0, ymm0, ymmword ptr [rip + .LCPI0_0]
        vprolvd ymm0, ymm0, ymmword ptr [rip + .LCPI0_1]
        vpsrlq  ymm0, ymm0, 12
        vpermb  ymm0, ymm0, ymmword ptr [rip + .LCPI0_2]
        mov     al, 31           # what the heck, clang? partial register false dependency for no reason!
        kmovd   k1, eax
        vmovdqu32       ymmword ptr [rdi] {k1}, ymm0
      # vzeroupper not needed because the caller was using __m256i args.  GCC omits it.
        ret

即使您两次使用相同的移位常量向量使编译器将其保存在寄存器中(而不是直接从内存源操作数中使用),它仍然选择从内存中加载它而不是 mov eax,6 / vpbroadcast ymm1, eax 之类的。这以需要 .rodata 中的常量为代价节省了 1 uop。公平地说,我们确实可能需要在同一个缓存行中使用其他常量,但是 GCC 浪费的方式 space 它们并不都适合一个缓存行! clang 注意到该模式并使用 vpbroadcastdq 加载,gcc 浪费地加载了完整的 32 个字节。 (kmov k1, [mem] 是 3 个前端微指令,因此它不会保存一个微指令来从内存中加载掩码常量。)

使用 _mm256_mask_slli_epi16(v, 0x5555, v, 6),clang 将其优化回 vpsllvw ymm0, ymm0, ymmword ptr [rip + .LCPI0_0],具有相同的 6,0 重复常数。所以我想这是一个好兆头,我做对了。但是 GCC 编译如写:

store_10x16_avx512vbmi(long long __vector(4), unsigned char*):
        mov     eax, 21845
        kmovw   k1, eax
        vpsllw  ymm0{k1}, ymm0, 6
        vprolvd ymm0, ymm0, YMMWORD PTR .LC0[rip]
        mov     eax, 31
        kmovb   k2, eax
        vpsrlq  ymm0, ymm0, 12
        vpermb  ymm0, ymm0, YMMWORD PTR .LC1[rip]
        vmovdqu32       YMMWORD PTR [rdi]{k2}, ymm0
        ret

_mm256_sllv_epi16 需要 AVX-512BW 和 AVX-512VL。 rolv_epi32 只需要 AVX-512VL。 (或者仅适用于 512 位版本的 AVX-512F。)旋转只有 32 和 64 个元素大小,而不是 16 个,但 AVX-512 确实将可变移位粒度扩展到 16(从 AVX2 中的 32 或 64)。

vpcompressb [rdi]{k1}, ymm0(AVX512VBMI = Ice Lake 及更高版本)将替代 vpermb + store 以在寄存器底部打包字节(如 BMI2 pext 但用于矢量元素而不是位在标量寄存器中)。但它实际上更昂贵:在 Ice Lake 上 6 uops,每 6c 吞吐量 1。 (vpcompressd 还不错)。

即使 vpcompressb 进入向量寄存器也是 2 微指令,因此对于常量洗牌控制,最好为 vpermb 加载向量常量,除非控制向量的缓存未命中是一个问题,例如如果你只是经常这样做一次,那么让硬件处理一个 k 掩码而不是一个负载。


不带 VBMI 的 AVX-512:2x 16 字节存储,不超过 20 字节范围

   ...  // same setup as usual, leaving 40-bit chunks at the bottom of each qword
 
    const __m256i shuffleIndices = _mm256_setr_epi8(
        // 6 bytes gap with zeros
        // Pack the two 5-byte chunks into the bottom of each 16-byte lane
        0, 1, 2, 3, 4,
        8, 9, 10, 11, 12,
        -1, -1, -1, -1, -1, -1,

        0, 1, 2, 3, 4,
        8, 9, 10, 11, 12,
        -1, -1, -1, -1, -1, -1);
    v = _mm256_shuffle_epi8(v, shuffleIndices );

    // Split the vector into halves
    __m128i low16 = _mm256_castsi256_si128( v );
    _mm_storeu_si128( ( __m128i* )dst, low16 );        // vmovdqu      mem, xmm  no masking

    // An AVX-512BW masked store avoiding writing past the end costs more instructions (and back-end uops), same front-end uops
    __m128i high16 = _mm256_extracti128_si256( v, 1 );  // vextracti128 xmm, ymm, 1
    _mm_mask_storeu_epi8( dst+10, 0x3FF, high16 );      // vmovdqu8 [mem]{k}, xmm

这需要 vextracti128 xmm, ymm, 1vmovdqu8 设置。与写入 26 个字节不同,我们不能直接提取到内存中。没有 vextracti8x16,只有 vextracti32x464x2(以及 32x8 / 64x4 256 位提取)。我们需要字节粒度掩码,但无法通过直接提取到内存的指令获得它,只能通过洗牌(vextract 到寄存器)然后 vmovdqu8.

所以我们得到的asm是

# clang
...        vpshufb result in YMM0
        vmovdqu      [rdi], xmm0             # same as before
        vextracti128    xmm0, ymm0, 1        # 1 shuffle uop
        mov     ax, 1023
        kmovd   k1, eax                         # will be hoisted
        vmovdqu8     [rdi + 10] {k1}, xmm0   # 1 micro-fused uop

因为 vextracti128 [mem], ymm, 1 无论如何都是 2 个前端微指令,这不会影响前端吞吐量。 (由于 shuffle uop,它确实对后端执行端口造成了更大的压力)。