【问题标题】:A better 8x8 bytes matrix transpose with SSE?使用 SSE 的更好的 8x8 字节矩阵转置?
【发布时间】:2017-06-28 23:19:45
【问题描述】:

我找到了this post,它解释了如何用 24 个操作转置一个 8x8 字节矩阵,然后滚动几卷后,the code 实现了转置。但是,这种方法没有利用我们可以阻止将 8x8 转置为四个 4x4 转置,并且每个转置只能在一个 shuffle 指令中完成(this post 是参考)。所以我想出了这个解决方案:

__m128i transpose4x4mask = _mm_set_epi8(15, 11, 7, 3, 14, 10, 6, 2, 13,  9, 5, 1, 12,  8, 4, 0);
__m128i shuffle8x8Mask = _mm_setr_epi8(0, 1, 2, 3, 8, 9, 10, 11, 4,  5, 6, 7, 12,  13, 14, 15);

void TransposeBlock8x8(uint8_t *src, uint8_t *dst, int srcStride, int dstStride) {
    __m128i load0 = _mm_set_epi64x(*(uint64_t*)(src + 1 * srcStride), *(uint64_t*)(src + 0 * srcStride));
    __m128i load1 = _mm_set_epi64x(*(uint64_t*)(src + 3 * srcStride), *(uint64_t*)(src + 2 * srcStride));
    __m128i load2 = _mm_set_epi64x(*(uint64_t*)(src + 5 * srcStride), *(uint64_t*)(src + 4 * srcStride));
    __m128i load3 = _mm_set_epi64x(*(uint64_t*)(src + 7 * srcStride), *(uint64_t*)(src + 6 * srcStride));

    __m128i shuffle0 = _mm_shuffle_epi8(load0, shuffle8x8Mask);
    __m128i shuffle1 = _mm_shuffle_epi8(load1, shuffle8x8Mask);
    __m128i shuffle2 = _mm_shuffle_epi8(load2, shuffle8x8Mask);
    __m128i shuffle3 = _mm_shuffle_epi8(load3, shuffle8x8Mask);

    __m128i block0 = _mm_unpacklo_epi64(shuffle0, shuffle1);
    __m128i block1 = _mm_unpackhi_epi64(shuffle0, shuffle1);
    __m128i block2 = _mm_unpacklo_epi64(shuffle2, shuffle3);
    __m128i block3 = _mm_unpackhi_epi64(shuffle2, shuffle3);

    __m128i transposed0 = _mm_shuffle_epi8(block0, transpose4x4mask);   
    __m128i transposed1 = _mm_shuffle_epi8(block1, transpose4x4mask);   
    __m128i transposed2 = _mm_shuffle_epi8(block2, transpose4x4mask);   
    __m128i transposed3 = _mm_shuffle_epi8(block3, transpose4x4mask);   

    __m128i store0 = _mm_unpacklo_epi32(transposed0, transposed2);
    __m128i store1 = _mm_unpackhi_epi32(transposed0, transposed2);
    __m128i store2 = _mm_unpacklo_epi32(transposed1, transposed3);
    __m128i store3 = _mm_unpackhi_epi32(transposed1, transposed3);

    *((uint64_t*)(dst + 0 * dstStride)) = _mm_extract_epi64(store0, 0);
    *((uint64_t*)(dst + 1 * dstStride)) = _mm_extract_epi64(store0, 1);
    *((uint64_t*)(dst + 2 * dstStride)) = _mm_extract_epi64(store1, 0);
    *((uint64_t*)(dst + 3 * dstStride)) = _mm_extract_epi64(store1, 1);
    *((uint64_t*)(dst + 4 * dstStride)) = _mm_extract_epi64(store2, 0);
    *((uint64_t*)(dst + 5 * dstStride)) = _mm_extract_epi64(store2, 1);
    *((uint64_t*)(dst + 6 * dstStride)) = _mm_extract_epi64(store3, 0);
    *((uint64_t*)(dst + 7 * dstStride)) = _mm_extract_epi64(store3, 1);
}

排除加载/存储操作,此过程仅包含 16 条指令,而不是 24 条。

我错过了什么?

【问题讨论】:

  • 您错过了加载向量的 4 128 位操作和存储向量的 4 128 位操作。
  • __m128i load0 = _mm_set_epi64x((uint64_t)(src + 1 * srcStride), (uint64_t)(src + 0 * srcStride));它等于 __m128i load0 = _mm_loadu_si128((__m128i*)(src + 0 * srcStride));
  • ((uint64_t)(dst + 0 * dstStride)) = _mm_extract_epi64(store0, 0); ((uint64_t)(dst + 1 * dstStride)) = _mm_extract_epi64(store0, 1);等于 _mm_storeu_si128((__m128i*)(src + 0 * srcStride), store0);
  • @ErmIg:加载/存储应该从操作中排除。我链接的包含 24 个操作的帖子没有考虑到它们。无论如何,您必须加载数据,不是吗?
  • @ErmIg:只有当srcStride8 并且dstStride8 时,它们才等效,也就是说,如果 src 和 dst 都是每个 8x8 字节的矩阵。否则它们是不同的,例如 128x128 字节的矩阵,但我实际上只是“放大”到 8x8 块,在这种情况下,scrStridedstStride 将都是 128

标签: c matrix optimization sse simd


【解决方案1】:

除了读取和写入内存的加载、存储和pinsrq-s,步长可能不等于8字节, 只需 12 条指令即可进行转置(此代码可以轻松地与 Z boson 的测试代码结合使用):

void tran8x8b_SSE_v2(char *A, char *B) {
  __m128i pshufbcnst = _mm_set_epi8(15,11,7,3, 14,10,6,2, 13,9,5,1, 12,8,4,0);

  __m128i B0, B1, B2, B3, T0, T1, T2, T3;
  B0 = _mm_loadu_si128((__m128i*)&A[ 0]);
  B1 = _mm_loadu_si128((__m128i*)&A[16]);
  B2 = _mm_loadu_si128((__m128i*)&A[32]);
  B3 = _mm_loadu_si128((__m128i*)&A[48]);


  T0 = _mm_castps_si128(_mm_shuffle_ps(_mm_castsi128_ps(B0),_mm_castsi128_ps(B1),0b10001000));
  T1 = _mm_castps_si128(_mm_shuffle_ps(_mm_castsi128_ps(B2),_mm_castsi128_ps(B3),0b10001000));
  T2 = _mm_castps_si128(_mm_shuffle_ps(_mm_castsi128_ps(B0),_mm_castsi128_ps(B1),0b11011101));
  T3 = _mm_castps_si128(_mm_shuffle_ps(_mm_castsi128_ps(B2),_mm_castsi128_ps(B3),0b11011101));

  B0 = _mm_shuffle_epi8(T0,pshufbcnst);
  B1 = _mm_shuffle_epi8(T1,pshufbcnst);
  B2 = _mm_shuffle_epi8(T2,pshufbcnst);
  B3 = _mm_shuffle_epi8(T3,pshufbcnst);

  T0 = _mm_unpacklo_epi32(B0,B1);
  T1 = _mm_unpackhi_epi32(B0,B1);
  T2 = _mm_unpacklo_epi32(B2,B3);
  T3 = _mm_unpackhi_epi32(B2,B3);

  _mm_storeu_si128((__m128i*)&B[ 0], T0);
  _mm_storeu_si128((__m128i*)&B[16], T1);
  _mm_storeu_si128((__m128i*)&B[32], T2);
  _mm_storeu_si128((__m128i*)&B[48], T3);
}


这里我们使用 32 位浮点 shuffle,它比 epi32 shuffle 更灵活。 强制转换不会生成额外的指令(使用 gcc 5.4 生成的代码):

tran8x8b_SSE_v2:
.LFB4885:
    .cfi_startproc
    vmovdqu 48(%rdi), %xmm5
    vmovdqu 32(%rdi), %xmm2
    vmovdqu 16(%rdi), %xmm0
    vmovdqu (%rdi), %xmm1
    vshufps $136, %xmm5, %xmm2, %xmm4
    vshufps $221, %xmm5, %xmm2, %xmm2
    vmovdqa .LC6(%rip), %xmm5
    vshufps $136, %xmm0, %xmm1, %xmm3
    vshufps $221, %xmm0, %xmm1, %xmm1
    vpshufb %xmm5, %xmm3, %xmm3
    vpshufb %xmm5, %xmm1, %xmm0
    vpshufb %xmm5, %xmm4, %xmm4
    vpshufb %xmm5, %xmm2, %xmm1
    vpunpckldq  %xmm4, %xmm3, %xmm5
    vpunpckldq  %xmm1, %xmm0, %xmm2
    vpunpckhdq  %xmm4, %xmm3, %xmm3
    vpunpckhdq  %xmm1, %xmm0, %xmm0
    vmovups %xmm5, (%rsi)
    vmovups %xmm3, 16(%rsi)
    vmovups %xmm2, 32(%rsi)
    vmovups %xmm0, 48(%rsi)
    ret
    .cfi_endproc



在某些(但不是全部)较旧的 cpu 上,在 整数和浮点单位。这会增加函数的延迟,但不一定会影响 代码的吞吐量。

一个简单的 1e9 转置延迟测试:

  for (int i=0;i<500000000;i++){
     tran8x8b_SSE(A,C);
     tran8x8b_SSE(C,A);
  }
  print8x8b(A);

使用 tran8x8b_SSE 大约需要 5.5 秒(19.7e9 个周期),使用 tran8x8b_SSE_v2(英特尔酷睿 i5-6500)大约需要 4.5 秒(16.0e9 个周期)。注意 尽管函数在 for 循环中内联,但编译器并未消除加载和存储。


更新:AVX2-128 / SSE 4.1 混合解决方案。

“洗牌”(解包、洗牌)由端口 5 处理,在现代 cpu 上每个 cpu 周期有 1 条指令。 有时用两种混合来代替一个“洗牌”是值得的。在 Skylake 上,32 位混合指令可以在端口 0、1 或 5 上运行。

很遗憾,_mm_blend_epi32 只是 AVX2-128。一个有效的 SSE 4.1 替代方案是 _mm_blend_ps 的组合 有一些演员表(通常是免费的)。 12个“洗牌”被替换为 8 种 shuffle 和 8 种混合。

现在,简单延迟测试的运行时间约为 3.6 秒(13e9 个 CPU 周期),比 tran8x8b_SSE_v2 的结果快 18%。

代码:

/* AVX2-128 version, sse 4.1 version see ---------------->       SSE 4.1 version of tran8x8b_AVX2_128()                                                              */
void tran8x8b_AVX2_128(char *A, char *B) {                   /*  void tran8x8b_SSE4_1(char *A, char *B) {                                                            */                                    
  __m128i pshufbcnst_0 = _mm_set_epi8(15, 7,11, 3,  
               13, 5, 9, 1,  14, 6,10, 2,  12, 4, 8, 0);     /*    __m128i pshufbcnst_0 = _mm_set_epi8(15, 7,11, 3,  13, 5, 9, 1,  14, 6,10, 2,  12, 4, 8, 0);       */                                    
  __m128i pshufbcnst_1 = _mm_set_epi8(13, 5, 9, 1,  
               15, 7,11, 3,  12, 4, 8, 0,  14, 6,10, 2);     /*    __m128i pshufbcnst_1 = _mm_set_epi8(13, 5, 9, 1,  15, 7,11, 3,  12, 4, 8, 0,  14, 6,10, 2);       */                                    
  __m128i pshufbcnst_2 = _mm_set_epi8(11, 3,15, 7,  
                9, 1,13, 5,  10, 2,14, 6,   8, 0,12, 4);     /*    __m128i pshufbcnst_2 = _mm_set_epi8(11, 3,15, 7,   9, 1,13, 5,  10, 2,14, 6,   8, 0,12, 4);       */                                    
  __m128i pshufbcnst_3 = _mm_set_epi8( 9, 1,13, 5,  
               11, 3,15, 7,   8, 0,12, 4,  10, 2,14, 6);     /*    __m128i pshufbcnst_3 = _mm_set_epi8( 9, 1,13, 5,  11, 3,15, 7,   8, 0,12, 4,  10, 2,14, 6);       */                                    
  __m128i B0, B1, B2, B3, T0, T1, T2, T3;                    /*    __m128 B0, B1, B2, B3, T0, T1, T2, T3;                                                            */                                    
                                                             /*                                                                                                      */                                    
  B0 = _mm_loadu_si128((__m128i*)&A[ 0]);                    /*    B0 = _mm_loadu_ps((float*)&A[ 0]);                                                                */                                    
  B1 = _mm_loadu_si128((__m128i*)&A[16]);                    /*    B1 = _mm_loadu_ps((float*)&A[16]);                                                                */                                    
  B2 = _mm_loadu_si128((__m128i*)&A[32]);                    /*    B2 = _mm_loadu_ps((float*)&A[32]);                                                                */                                    
  B3 = _mm_loadu_si128((__m128i*)&A[48]);                    /*    B3 = _mm_loadu_ps((float*)&A[48]);                                                                */                                    
                                                             /*                                                                                                      */                                    
  B1 = _mm_shuffle_epi32(B1,0b10110001);                     /*    B1 = _mm_shuffle_ps(B1,B1,0b10110001);                                                            */                                    
  B3 = _mm_shuffle_epi32(B3,0b10110001);                     /*    B3 = _mm_shuffle_ps(B3,B3,0b10110001);                                                            */                                    
  T0 = _mm_blend_epi32(B0,B1,0b1010);                        /*    T0 = _mm_blend_ps(B0,B1,0b1010);                                                                  */                                    
  T1 = _mm_blend_epi32(B2,B3,0b1010);                        /*    T1 = _mm_blend_ps(B2,B3,0b1010);                                                                  */                                    
  T2 = _mm_blend_epi32(B0,B1,0b0101);                        /*    T2 = _mm_blend_ps(B0,B1,0b0101);                                                                  */                                    
  T3 = _mm_blend_epi32(B2,B3,0b0101);                        /*    T3 = _mm_blend_ps(B2,B3,0b0101);                                                                  */                                    
                                                             /*                                                                                                      */                                    
  B0 = _mm_shuffle_epi8(T0,pshufbcnst_0);                    /*    B0 = _mm_castsi128_ps(_mm_shuffle_epi8(_mm_castps_si128(T0),pshufbcnst_0));                       */                                    
  B1 = _mm_shuffle_epi8(T1,pshufbcnst_1);                    /*    B1 = _mm_castsi128_ps(_mm_shuffle_epi8(_mm_castps_si128(T1),pshufbcnst_1));                       */                                    
  B2 = _mm_shuffle_epi8(T2,pshufbcnst_2);                    /*    B2 = _mm_castsi128_ps(_mm_shuffle_epi8(_mm_castps_si128(T2),pshufbcnst_2));                       */                                    
  B3 = _mm_shuffle_epi8(T3,pshufbcnst_3);                    /*    B3 = _mm_castsi128_ps(_mm_shuffle_epi8(_mm_castps_si128(T3),pshufbcnst_3));                       */                                    
                                                             /*                                                                                                      */                                    
  T0 = _mm_blend_epi32(B0,B1,0b1010);                        /*    T0 = _mm_blend_ps(B0,B1,0b1010);                                                                  */                                    
  T1 = _mm_blend_epi32(B0,B1,0b0101);                        /*    T1 = _mm_blend_ps(B0,B1,0b0101);                                                                  */                                    
  T2 = _mm_blend_epi32(B2,B3,0b1010);                        /*    T2 = _mm_blend_ps(B2,B3,0b1010);                                                                  */                                    
  T3 = _mm_blend_epi32(B2,B3,0b0101);                        /*    T3 = _mm_blend_ps(B2,B3,0b0101);                                                                  */                                    
  T1 = _mm_shuffle_epi32(T1,0b10110001);                     /*    T1 = _mm_shuffle_ps(T1,T1,0b10110001);                                                            */                                    
  T3 = _mm_shuffle_epi32(T3,0b10110001);                     /*    T3 = _mm_shuffle_ps(T3,T3,0b10110001);                                                            */                                    
                                                             /*                                                                                                      */                                    
  _mm_storeu_si128((__m128i*)&B[ 0], T0);                    /*    _mm_storeu_ps((float*)&B[ 0], T0);                                                                */                                    
  _mm_storeu_si128((__m128i*)&B[16], T1);                    /*    _mm_storeu_ps((float*)&B[16], T1);                                                                */                                    
  _mm_storeu_si128((__m128i*)&B[32], T2);                    /*    _mm_storeu_ps((float*)&B[32], T2);                                                                */                                    
  _mm_storeu_si128((__m128i*)&B[48], T3);                    /*    _mm_storeu_ps((float*)&B[48], T3);                                                                */                                    
}                                                            /*  }                                                                                                   */                                    

【讨论】:

  • 很棒的工作!我喜欢你表明如果每个寄存器存储两行,你需要一半的操作。这是否意味着每个寄存器有四行,例如使用 AVX2,您可以在 6 次操作中做到这一点?很高兴您提到了旁路延迟。我有避免混合更改域的代码的坏习惯,因为我害怕延迟,而且我永远记不起延迟何时重要。所以无论如何,首先你期望 24 个操作,然后找到 16 个(我也找到了),现在你得到了 12 个。
  • 我的猜测是,对于 AVX,由于跨 128 位通道操作的限制,您将无法在 6 次操作中执行此操作,但理论上使用 256 位/32 字节两个操作数我认为操作可以在 6 次操作中做到这一点。
  • 同意,我认为使用 6 条 AVX2“shuffle”(即置换、解包等)指令是不可能的,但应该少于 12 条。然而,我的第一次尝试没有成功。
  • @wim:这是用 -mavx 还是 -msse4.2 编译的?我在加载时得到了一些不同的程序集,最后使用 -msse4.2.godbolt.org/g/VAGXGD 解包。
  • 我的微软评论指的是this
【解决方案2】:

将此作为答案发布。由于到目前为止收到了一些答案和 cmets,我还将把问题的标题从“... with SSE”更改为“... with SIMD”。

我仅在 8 条指令中成功地用 AVX2 转置了矩阵,其中 10 条包括加载/存储(不包括掩码加载)。 编辑:我找到了一个较短的版本。见下文。这是矩阵在内存中都是连续的情况,因此可以使用直接加载/存储。

这是 C 代码:

void tran8x8b_AVX2(char *src, char *dst) {
    __m256i perm = _mm256_set_epi8(
        0, 0, 0, 7,
        0, 0, 0, 5,
        0, 0, 0, 3,
        0, 0, 0, 1,

        0, 0, 0, 6,
        0, 0, 0, 4,
        0, 0, 0, 2,
        0, 0, 0, 0
    );

    __m256i tm = _mm256_set_epi8(
        15, 11, 7, 3,
        14, 10, 6, 2,
        13,  9, 5, 1,
        12,  8, 4, 0,

        15, 11, 7, 3,
        14, 10, 6, 2,
        13,  9, 5, 1,
        12,  8, 4, 0
    );

    __m256i load0 = _mm256_loadu_si256((__m256i*)&src[ 0]);
    __m256i load1 = _mm256_loadu_si256((__m256i*)&src[32]);  

    __m256i perm0 = _mm256_permutevar8x32_epi32(load0, perm);   
    __m256i perm1 = _mm256_permutevar8x32_epi32(load1, perm);   

    __m256i transpose0 = _mm256_shuffle_epi8(perm0, tm);    
    __m256i transpose1 = _mm256_shuffle_epi8(perm1, tm);    

    __m256i unpack0 = _mm256_unpacklo_epi32(transpose0, transpose1);    
    __m256i unpack1 = _mm256_unpackhi_epi32(transpose0, transpose1);

    perm0 = _mm256_castps_si256(_mm256_permute2f128_ps(_mm256_castsi256_ps(unpack0), _mm256_castsi256_ps(unpack1), 32));    
    perm1 = _mm256_castps_si256(_mm256_permute2f128_ps(_mm256_castsi256_ps(unpack0), _mm256_castsi256_ps(unpack1), 49));    

    _mm256_storeu_si256((__m256i*)&dst[ 0], perm0);
    _mm256_storeu_si256((__m256i*)&dst[32], perm1);
}

GCC 足够聪明,可以在 AVX 加载期间执行置换,从而节省了两条指令。这是编译器的输出:

tran8x8b_AVX2(char*, char*):
        vmovdqa ymm1, YMMWORD PTR .LC0[rip]
        vmovdqa ymm2, YMMWORD PTR .LC1[rip]
        vpermd  ymm0, ymm1, YMMWORD PTR [rdi]
        vpermd  ymm1, ymm1, YMMWORD PTR [rdi+32]
        vpshufb ymm0, ymm0, ymm2
        vpshufb ymm1, ymm1, ymm2
        vpunpckldq      ymm2, ymm0, ymm1
        vpunpckhdq      ymm0, ymm0, ymm1
        vinsertf128     ymm1, ymm2, xmm0, 1
        vperm2f128      ymm0, ymm2, ymm0, 49
        vmovdqu YMMWORD PTR [rsi], ymm1
        vmovdqu YMMWORD PTR [rsi+32], ymm0
        vzeroupper
        ret

它发出带有 -O3 的 vzerupper 指令,但向下到 -O1 会删除它。

如果是我最初的问题(一个大矩阵,我正在放大它的 8x8 部分),处理步幅会以一种非常糟糕的方式破坏输出:

void tran8x8b_AVX2(char *src, char *dst, int srcStride, int dstStride) {
    __m256i load0 = _mm256_set_epi64x(*(uint64_t*)(src + 3 * srcStride), *(uint64_t*)(src + 2 * srcStride), *(uint64_t*)(src + 1 * srcStride), *(uint64_t*)(src + 0 * srcStride));
    __m256i load1 = _mm256_set_epi64x(*(uint64_t*)(src + 7 * srcStride), *(uint64_t*)(src + 6 * srcStride), *(uint64_t*)(src + 5 * srcStride), *(uint64_t*)(src + 4 * srcStride));

    // ... the same as before, however we can skip the final permutations because we need to handle the destination stride...

    *((uint64_t*)(dst + 0 * dstStride)) = _mm256_extract_epi64(unpack0, 0);
    *((uint64_t*)(dst + 1 * dstStride)) = _mm256_extract_epi64(unpack0, 1);
    *((uint64_t*)(dst + 2 * dstStride)) = _mm256_extract_epi64(unpack1, 0);
    *((uint64_t*)(dst + 3 * dstStride)) = _mm256_extract_epi64(unpack1, 1);
    *((uint64_t*)(dst + 4 * dstStride)) = _mm256_extract_epi64(unpack0, 2);
    *((uint64_t*)(dst + 5 * dstStride)) = _mm256_extract_epi64(unpack0, 3);
    *((uint64_t*)(dst + 6 * dstStride)) = _mm256_extract_epi64(unpack1, 2);
    *((uint64_t*)(dst + 7 * dstStride)) = _mm256_extract_epi64(unpack1, 3);
}

这是编译器的输出:

tran8x8b_AVX2(char*, char*, int, int):
        movsx   rdx, edx
        vmovq   xmm5, QWORD PTR [rdi]
        lea     r9, [rdi+rdx]
        vmovdqa ymm3, YMMWORD PTR .LC0[rip]
        movsx   rcx, ecx
        lea     r11, [r9+rdx]
        vpinsrq xmm0, xmm5, QWORD PTR [r9], 1
        lea     r10, [r11+rdx]
        vmovq   xmm4, QWORD PTR [r11]
        vpinsrq xmm1, xmm4, QWORD PTR [r10], 1
        lea     r8, [r10+rdx]
        lea     rax, [r8+rdx]
        vmovq   xmm7, QWORD PTR [r8]
        vmovq   xmm6, QWORD PTR [rax+rdx]
        vpinsrq xmm2, xmm7, QWORD PTR [rax], 1
        vinserti128     ymm1, ymm0, xmm1, 0x1
        vpinsrq xmm0, xmm6, QWORD PTR [rax+rdx*2], 1
        lea     rax, [rsi+rcx]
        vpermd  ymm1, ymm3, ymm1
        vinserti128     ymm0, ymm2, xmm0, 0x1
        vmovdqa ymm2, YMMWORD PTR .LC1[rip]
        vpshufb ymm1, ymm1, ymm2
        vpermd  ymm0, ymm3, ymm0
        vpshufb ymm0, ymm0, ymm2
        vpunpckldq      ymm2, ymm1, ymm0
        vpunpckhdq      ymm0, ymm1, ymm0
        vmovdqa xmm1, xmm2
        vmovq   QWORD PTR [rsi], xmm1
        vpextrq QWORD PTR [rax], xmm1, 1
        vmovdqa xmm1, xmm0
        add     rax, rcx
        vextracti128    xmm0, ymm0, 0x1
        vmovq   QWORD PTR [rax], xmm1
        add     rax, rcx
        vpextrq QWORD PTR [rax], xmm1, 1
        add     rax, rcx
        vextracti128    xmm1, ymm2, 0x1
        vmovq   QWORD PTR [rax], xmm1
        add     rax, rcx
        vpextrq QWORD PTR [rax], xmm1, 1
        vmovq   QWORD PTR [rax+rcx], xmm0
        vpextrq QWORD PTR [rax+rcx*2], xmm0, 1
        vzeroupper
        ret

但是,如果与我的原始代码的输出相比,这似乎没什么大不了的。


编辑:我找到了一个较短的版本。总共 4 条指令,8 条同时计算加载/存储。 这是可能的,因为我以不同的方式读取矩阵,在加载期间在“收集”指令中隐藏了一些“洗牌”。另外,请注意,执行存储需要最终排列,因为 AVX2 没有“分散”指令。拥有分散指令只会将所有内容减少到 2 条指令。另外,请注意,我可以通过更改 vindex 向量的内容来轻松处理 src 步幅。

不幸的是,这个 AVX_v2 似乎比前一个慢。代码如下:

void tran8x8b_AVX2_v2(char *src1, char *dst1) {
    __m256i tm = _mm256_set_epi8(
        15, 11, 7, 3,
        14, 10, 6, 2,
        13,  9, 5, 1,
        12,  8, 4, 0,

        15, 11, 7, 3,
        14, 10, 6, 2,
        13,  9, 5, 1,
        12,  8, 4, 0
    );

    __m256i vindex = _mm256_setr_epi32(0, 8, 16, 24, 32, 40, 48, 56);
    __m256i perm = _mm256_setr_epi32(0, 4, 1, 5, 2, 6, 3, 7);

     __m256i load0 = _mm256_i32gather_epi32((int*)src1, vindex, 1);
    __m256i load1 = _mm256_i32gather_epi32((int*)(src1 + 4), vindex, 1); 

    __m256i transpose0 = _mm256_shuffle_epi8(load0, tm);    
    __m256i transpose1 = _mm256_shuffle_epi8(load1, tm);    

    __m256i final0 = _mm256_permutevar8x32_epi32(transpose0, perm);    
    __m256i final1 = _mm256_permutevar8x32_epi32(transpose1, perm);    

    _mm256_storeu_si256((__m256i*)&dst1[ 0], final0);
    _mm256_storeu_si256((__m256i*)&dst1[32], final1);
}

这是编译器的输出:

tran8x8b_AVX2_v2(char*, char*):
        vpcmpeqd        ymm3, ymm3, ymm3
        vmovdqa ymm2, YMMWORD PTR .LC0[rip]
        vmovdqa ymm4, ymm3
        vpgatherdd      ymm0, DWORD PTR [rdi+4+ymm2*8], ymm3
        vpgatherdd      ymm1, DWORD PTR [rdi+ymm2*8], ymm4
        vmovdqa ymm2, YMMWORD PTR .LC1[rip]
        vpshufb ymm1, ymm1, ymm2
        vpshufb ymm0, ymm0, ymm2
        vmovdqa ymm2, YMMWORD PTR .LC2[rip]
        vpermd  ymm1, ymm2, ymm1
        vpermd  ymm0, ymm2, ymm0
        vmovdqu YMMWORD PTR [rsi], ymm1
        vmovdqu YMMWORD PTR [rsi+32], ymm0
        vzeroupper
        ret

【讨论】:

  • 别担心vzeroupper。这仅针对具有外部链接的功能发出。如果你在同一个源文件中调用你的函数,你的编译器可能会内联,在这种情况下它不会生成vzeroupper。我会做static void tran8x8b_AVX2
  • 我们专注于指令数量,但另一个需要考虑的重要事项是端口压力。如果我记得排列和洗牌都去一个端口,而混合去两个端口。在某些情况下,如果可以降低端口压力,则使用更多指令实际上是有意义的。例如。 in some cases two shuffles can be replaced by one shuffle and two blends.
  • @Zboson:确实。我刚刚对 8 instr 函数进行了基准测试,它比我的 16 instr(6.4s)快一点(6.2s),但比 wim 的版本(5.3s)慢。
  • 关于寄存器转置你的AVX2转置很棒。它是 8 次(端口 5)洗牌而不是 12 次,因此吞吐量可能比我的解决方案高 1.5 倍。对于延迟,由于交叉通道洗牌速度较慢(正常洗牌时延迟为 3c 而不是 1c),我预计不会有太大差异。请注意,我的测试循环是一个简单的延迟测试,而在您的应用程序中,您可能对吞吐量更感兴趣!
  • @wim 太棒了!很高兴看到这么多人在这方面有所改进。您的新代码与this new answer 相比如何?可悲的是,我现在的回答是最糟糕的。但如果我不回答这个问题会引起这么大的兴趣吗?
【解决方案3】:

一个简化的

void tp128_8x8(char *A, char *B) {
  __m128i sv = _mm_set_epi8(15, 7, 14, 6, 13, 5, 12, 4, 11, 3, 10, 2, 9, 1, 8,  0);
  __m128i iv[4], ov[4];

  ov[0] = _mm_shuffle_epi8(_mm_loadu_si128((__m128i*)A), sv);
  ov[1] = _mm_shuffle_epi8(_mm_loadu_si128((__m128i*)(A+16)), sv);
  ov[2] = _mm_shuffle_epi8(_mm_loadu_si128((__m128i*)(A+32)), sv);
  ov[3] = _mm_shuffle_epi8(_mm_loadu_si128((__m128i*)(A+48)), sv);

  iv[0] = _mm_unpacklo_epi16(ov[0], ov[1]); 
  iv[1] = _mm_unpackhi_epi16(ov[0], ov[1]); 
  iv[2] = _mm_unpacklo_epi16(ov[2], ov[3]); 
  iv[3] = _mm_unpackhi_epi16(ov[2], ov[3]); 

  _mm_storeu_si128((__m128i*)B,      _mm_unpacklo_epi32(iv[0], iv[2]));
  _mm_storeu_si128((__m128i*)(B+16), _mm_unpackhi_epi32(iv[0], iv[2]));
  _mm_storeu_si128((__m128i*)(B+32), _mm_unpacklo_epi32(iv[1], iv[3]));
  _mm_storeu_si128((__m128i*)(B+48), _mm_unpackhi_epi32(iv[1], iv[3]));
}



Benchmark:i5-5300U 2.3GHz (cycles per byte)
tran8x8b           : 2.140
tran8x8b_SSE       : 1.602
tran8x8b_SSE_v2    : 1.551
tp128_8x8          : 1.535
tran8x8b_AVX2      : 1.563
tran8x8b_AVX2_v2   : 1.731

【讨论】:

  • 这只是一些噪音吗?还是这段代码去掉了其他代码中存在的一些指令依赖?
【解决方案4】:

通常不计算加载和存储指令时,这是因为代码正在使用寄存器中的矩阵,例如除了在循环中进行转置之外还进行多项操作。这种情况下的加载和存储不计算在内,因为它们不是主循环的一部分。

但是在您的代码中,加载和存储(或者更确切地说是集合和提取)是转置的一部分。

GCC 在您的代码中使用_mm_insert_epi64_mm_loadl_epi64 为SSE4.1 实现_mm_set_epi64x。插入指令正在执行转置的一部分,即转置从load0,1,2,3 开始,而不是shuffle0,1,2,3。然后你最终的store0,1,2,3 值也不包含转置。您必须使用八条_mm_extract_epi64 指令来完成内存中的转置。所以不计算集合并提取内在函数是没有意义的。

在任何情况下,事实证明,您可以只使用 SSSE3 仅使用 16 条指令从寄存器进行转置,如下所示:

//__m128i B0, __m128i B1, __m128i B2, __m128i B3
__m128i mask = _mm_setr_epi8(0x0,0x04,0x01,0x05, 0x02,0x06,0x03,0x07, 0x08,0x0c,0x09,0x0d, 0x0a,0x0e,0x0b,0x0f);

__m128i T0, T1, T2, T3;
T0 = _mm_unpacklo_epi8(B0,B1);
T1 = _mm_unpackhi_epi8(B0,B1);
T2 = _mm_unpacklo_epi8(B2,B3);
T3 = _mm_unpackhi_epi8(B2,B3);

B0 = _mm_unpacklo_epi16(T0,T2);
B1 = _mm_unpackhi_epi16(T0,T2);
B2 = _mm_unpacklo_epi16(T1,T3);
B3 = _mm_unpackhi_epi16(T1,T3);

T0 = _mm_unpacklo_epi32(B0,B2);
T1 = _mm_unpackhi_epi32(B0,B2);
T2 = _mm_unpacklo_epi32(B1,B3);
T3 = _mm_unpackhi_epi32(B1,B3);

B0 = _mm_shuffle_epi8(T0,mask);
B1 = _mm_shuffle_epi8(T1,mask);
B2 = _mm_shuffle_epi8(T2,mask);
B3 = _mm_shuffle_epi8(T3,mask);

我不确定排除负载并在此处存储是否有意义,因为我不确定在四个 128 位寄存器中使用 8x8 字节矩阵是否方便。

这是测试这个的代码:

#include <stdio.h>
#include <x86intrin.h>

void print8x8b(char *A) {
  for(int i=0; i<8; i++) {
    for(int j=0; j<8; j++) {
      printf("%2d ", A[i*8+j]);
    } puts("");
  } puts("");
}

void tran8x8b(char *A, char *B) {
  for(int i=0; i<8; i++) {
    for(int j=0; j<8; j++) {
      B[j*8+i] = A[i*8+j];
    }
  }
}

void tran8x8b_SSE(char *A, char *B) {
  __m128i mask = _mm_setr_epi8(0x0,0x04,0x01,0x05, 0x02,0x06,0x03,0x07, 0x08,0x0c,0x09,0x0d, 0x0a,0x0e,0x0b,0x0f);

  __m128i B0, B1, B2, B3, T0, T1, T2, T3;
  B0 = _mm_loadu_si128((__m128i*)&A[ 0]);
  B1 = _mm_loadu_si128((__m128i*)&A[16]);
  B2 = _mm_loadu_si128((__m128i*)&A[32]);
  B3 = _mm_loadu_si128((__m128i*)&A[48]);

  T0 = _mm_unpacklo_epi8(B0,B1);
  T1 = _mm_unpackhi_epi8(B0,B1);
  T2 = _mm_unpacklo_epi8(B2,B3);
  T3 = _mm_unpackhi_epi8(B2,B3);

  B0 = _mm_unpacklo_epi16(T0,T2);
  B1 = _mm_unpackhi_epi16(T0,T2);
  B2 = _mm_unpacklo_epi16(T1,T3);
  B3 = _mm_unpackhi_epi16(T1,T3);

  T0 = _mm_unpacklo_epi32(B0,B2);
  T1 = _mm_unpackhi_epi32(B0,B2);
  T2 = _mm_unpacklo_epi32(B1,B3);
  T3 = _mm_unpackhi_epi32(B1,B3);

  B0 = _mm_shuffle_epi8(T0,mask);
  B1 = _mm_shuffle_epi8(T1,mask);
  B2 = _mm_shuffle_epi8(T2,mask);
  B3 = _mm_shuffle_epi8(T3,mask);

  _mm_storeu_si128((__m128i*)&B[ 0], B0);
  _mm_storeu_si128((__m128i*)&B[16], B1);
  _mm_storeu_si128((__m128i*)&B[32], B2);
  _mm_storeu_si128((__m128i*)&B[48], B3);
}

int main(void) {
  char A[64], B[64], C[64];
  for(int i=0; i<64; i++) A[i] = i;
  print8x8b(A);
  tran8x8b(A,B);
  print8x8b(B);
  tran8x8b_SSE(A,C);
  print8x8b(C);
}

【讨论】:

  • 我明白了。当与 8x8 矩阵一起使用时(例如A[64]),我的代码“退化”成类似的东西。但是,我的代码也可以处理更大的矩阵,其中行不是 8 字节的倍数(例如A[32 * 32]),您不能使用_mm_load。你是怎么处理这种情况的? (+1 顺便说一句)
  • @xmas79,哦,我明白你的意思了,我没有充分考虑你为什么对跨步感兴趣。在这种情况下,使用插入和提取是有意义的。无论如何,这强化了为什么不计算负载和存储是没有意义的。但无论如何,问题在于您指向的链接不讨论 8x8 字节转置,而是讨论 8x8 短转置(和 4x4 字符转置),因此您认为需要 24 次操作的假设是错误的。只需要 16 次操作。我们都用不同的方法展示。
  • @xmas79,也许我的假设是错误的,即store0,1,2,3 不包含转置。您只使用 extract 来处理步幅。我现在明白了。那我的回答并不完全正确。问题再次是您假设需要 24 次操作,它只有 16 次。
【解决方案5】:

AVX512VBMI (Cascade Lake / Ice Lake)

AVX512VBMI 引入了vpermb,这是一种具有字节粒度的 64 字节车道交叉洗牌。
_mm512_permutexvar_epi8( __m512i idx, __m512i a);

支持它的现有 CPU 将其作为单个 uop 运行,吞吐量为 1/时钟。 (https://www.uops.info/html-tp/CNL/VPERMB_ZMM_ZMM_M512-Measurements.html)

这使问题变得微不足道,只需 1 条指令就可以实现(至少对于整个 8x8 块是连续的 stride=8 的情况)。否则,您应该查看 vpermt2b 以将来自 2 个源的字节混在一起。但这是 CannonLake 上的 3 微秒。

// TODO: strided loads / stores somehow for stride != 8
// AVX512VBMI
void TransposeBlock8x8_contiguous(uint8_t *src, uint8_t *dst)
{
    const __m512i trans8x8shuf = _mm512_set_epi8(
        63, 63-8*1, 63-8*2, 63-8*3, 63-8*4, ...
        ...
        57, 49, 41, 33, 25, 17, 9, 1,
        56, 48, 40, 32, 24, 16, 8, 0
   );

    __m512i vsrc = _mm512_loadu_si512(src);
    __m512i shuffled = _mm512_permutexvar_epi8(trans8x8shuf, vsrc);
    _mm512_storeu_si512(dst, shuffled);
}

https://godbolt.org/z/wrfyy3

显然_mm512_setr_epi8 不存在于 gcc/clang(仅 256 和 128 版本),因此您必须按照与 C 数组初始值设定项顺序相反的从后到前的顺序定义常量。


vpermb 甚至可以将数据用作内存源操作数,因此它可以在单个指令中加载+随机播放。但根据https://uops.info/,它不会在 CannonLake 上进行微融合:不像 vpermd zmm, zmm, [r14] 解码为 1 个融合域 uop(注意“retire_slots:1.0”)
vpermd zmm, zmm, [r14] 解码为 2 个单独的 uops前端/融合域:“retire_slots:2.0”)。这是在真正的 CannonLake CPU 上使用性能计数器进行的实验测试。 uops.info 还没有 Cascade Lake 或 Ice Lake 可用,所以它可能会在那里更有效率。

uops.info 表无用地计算未融合域 uops 的总数,因此您必须单击一条指令以查看它是否微融合。


源或 dst 跨步的、不连续的数据

我猜您可能希望将 qword(8 字节)加载到 XMM 寄存器中并将输入对混洗在一起,或者将它们与 movhpspinsrq 连接。使用带跨步索引的 qword-gather 加载可能是值得的,但这通常不值得。

我不确定是否值得结合 YMM 寄存器,更不用说 ZMM,或者最好只获得与 XMM 寄存器一样宽,以便我们可以使用 vmovq 和 @ 手动有效地将 qwords 分散回内存987654340@(在英特尔 CPU 上不需要 shuffle uop,只需一个商店)。如果 dst 是连续的,那么合并一个不连续的跨步 src 会更有意义。

AVX512VBMI vpermt2b ymm 看起来对 shuffle+merge 很有用,就像车道交叉 punpcklbw,从其他两个 32 字节 YMM 寄存器的串联中选择任意 32 字节。 (或 ZMM 版本的 2x 64 字节 regs 中的 64 个)。但不幸的是,在 CannonLake 上它需要 3 微秒,例如 Skylake-X 和 Cannon Lake 上的 vpermt2w

如果我们以后可以担心字节,vpermt2d 在支持它的 CPU 上是有效的(单 uop)! Skylake-X 及更高版本。

Ice Lake 对于vpermt2b (instlat) 有一个每 2 周期的吞吐量,这可能是因为它有一个额外的 shuffle 单元可以运行一些(但不是全部)shuffle uop。注意例如vpshufb xmmymm 是0.5c 吞吐量,但vpshufb zmm 是1c 吞吐量。但是vpermb 总是只有 1c 的吞吐量。

我想知道我们是否可以利用合并屏蔽?就像vpmovzxbq 将输入字节零扩展为 qwords 一样。 (一个 8 字节行 -> 64 字节 ZMM 寄存器)。那么也许 dword 左移与合并屏蔽到另一个寄存器?不,这无济于事,有用的数据在两个输入的相同 dword 元素中,除非您先对一个寄存器执行某些操作,否则无法达到目的。


vpmovzxbq 加载结果的

重叠字节屏蔽存储 (vmovdqu8 [rdi + 0..7]{k1}, zmm0..7) 也是可能的,但可能效率不高。充其量,除了其中一个之外,所有这些都会错位。不过,存储缓冲区和/或缓存硬件可能能够有效地提交 8 次屏蔽存储。

混合策略在寄存器和一些掩码存储中进行一些移动可能会很有趣,以平衡连续的dst 的随机播放/混合与存储工作。尤其是如果所有商店都可以对齐,这将需要在每个向量中移动数据以使其位于正确的位置。

Ice Lake 有 2 个商店执行单元。 (IDK 如果 L1d 缓存提交可以跟上,或者如果在存储缓冲区中合并通常有帮助,或者如果这只是有助于工作的爆发。)

【讨论】:

    【解决方案6】:

    这对我来说真的很有趣,我一直希望这样做,但由于各种原因,我最终需要在 Go 中而不是 C 中进行,而且我没有向量内在函数,所以我想“好吧,我就写点东西看看效果如何”。

    我报告的时间,在 ~3.6GHz CPU 上,对于一个幼稚的实现,每 64 字节块转置大约 28ns,使用位移位完成一个大约 19ns。我使用 perf 来确认数字,这对我来说似乎有点不可能,而且它们似乎加起来了。花哨的移位实现有点超过 250 条指令,每个周期大约有 3.6 条指令,因此每个操作大约需要 69-70 个周期。

    这是 Go,但老实说,实现起来应该很简单;它只是将 64 字节的输入数组视为 8 个 uint64_t。

    通过将其中一些东西声明为新变量来提示寄存器分配器,您可以获得另一个纳秒左右的时间。

    
    import (
    "unsafe"
    )
    
    const (
        hi16 = uint64(0xFFFF0000FFFF0000)
        lo16 = uint64(0x0000FFFF0000FFFF)
        hi8  = uint64(0xFF00FF00FF00FF00)
        lo8  = uint64(0x00FF00FF00FF00FF)
    )
    
    // Okay, this might take some explaining. We are working on a logical
    // 8x8 matrix of bytes, which we get as a 64-byte array. We want to transpose
    // it (row/column).
    //
    // start:
    // [[00 08 16 24 32 40 48 56]
    //  [01 09 17 25 33 41 49 57]
    //  [02 10 18 26 34 42 50 58]
    //  [03 11 19 27 35 43 51 59]
    //  [04 12 20 28 36 44 52 60]
    //  [05 13 21 29 37 45 53 61]
    //  [06 14 22 30 38 46 54 62]
    //  [07 15 23 31 39 47 55 63]]
    //
    // First, let's make sure everything under 32 is in the top four rows,
    // and everything over 32 is in the bottom four rows. We do this by
    // swapping pairs of 32-bit words.
    // swap32:
    // [[00 08 16 24 04 12 20 28]
    //  [01 09 17 25 05 13 21 29]
    //  [02 10 18 26 06 14 22 30]
    //  [03 11 19 27 07 15 23 31]
    //  [32 40 48 56 36 44 52 60]
    //  [33 41 49 57 37 45 53 61]
    //  [34 42 50 58 38 46 54 62]
    //  [35 43 51 59 39 47 55 63]]
    //
    // Next, let's make sure everything over 16 or 48 is in the bottom two
    // rows of the two four-row sections, and everything under 16 or 48 is
    // in the top two rows of the section. We do this by swapping masked
    // pairs in much the same way:
    // swap16:
    // [[00 08 02 10 04 12 06 14]
    //  [01 09 03 11 05 13 07 15]
    //  [16 24 18 26 20 28 22 30]
    //  [17 25 19 27 21 29 23 31]
    //  [32 40 34 42 36 44 38 46]
    //  [33 41 35 43 37 45 39 47]
    //  [48 56 50 58 52 60 54 62]
    //  [49 57 51 59 53 61 55 63]]
    //
    // Now, we will do the same thing to each pair -- but because of
    // clever choices in the specific arrange ment leading up to this, that's
    // just one more byte swap, where each 2x2 block has its upper right
    // and lower left corners swapped, and that turns out to be an easy
    // shift and mask.
    func UnswizzleLazy(m *[64]uint8) {
        // m32 treats the 8x8 array as a 2x8 array, because
        // it turns out we only need to swap a handful of the
        // bits...
        m32 := (*[16]uint32)(unsafe.Pointer(&m[0]))
        m32[1], m32[8] = m32[8], m32[1]
        m32[3], m32[10] = m32[10], m32[3]
        m32[5], m32[12] = m32[12], m32[5]
        m32[7], m32[14] = m32[14], m32[7]
        m64 := (*[8]uint64)(unsafe.Pointer(&m[0]))
        // we're now at the state described above as "swap32"
        tmp0, tmp1, tmp2, tmp3 :=
            (m64[0]&lo16)|(m64[2]&lo16)<<16,
            (m64[1]&lo16)|(m64[3]&lo16)<<16,
            (m64[0]&hi16)>>16|(m64[2]&hi16),
            (m64[1]&hi16)>>16|(m64[3]&hi16)
        tmp4, tmp5, tmp6, tmp7 :=
            (m64[4]&lo16)|(m64[6]&lo16)<<16,
            (m64[5]&lo16)|(m64[7]&lo16)<<16,
            (m64[4]&hi16)>>16|(m64[6]&hi16),
            (m64[5]&hi16)>>16|(m64[7]&hi16)
        // now we're at "swap16".
        lo8 := lo8
        hi8 := hi8
        m64[0], m64[1] = (tmp0&lo8)|(tmp1&lo8)<<8, (tmp0&hi8)>>8|tmp1&hi8
        m64[2], m64[3] = (tmp2&lo8)|(tmp3&lo8)<<8, (tmp2&hi8)>>8|tmp3&hi8
        m64[4], m64[5] = (tmp4&lo8)|(tmp5&lo8)<<8, (tmp4&hi8)>>8|tmp5&hi8
        m64[6], m64[7] = (tmp6&lo8)|(tmp7&lo8)<<8, (tmp6&hi8)>>8|tmp7&hi8
    }
    

    我希望这样做是相当明显的:将半个单词打乱,所以前四个单词具有属于它们的所有值,而后四个具有属于它们的所有值。然后对每组四个单词做类似的事情,这样你就得到了前两个单词中属于前两个单词的东西,等等。

    直到我意识到如果上面的周期/字节数是正确的,这实际上胜过 shuffle/unpack 解决方案,我才会发表评论。

    (请注意,这是一个就地转置,但很容易将 temps 用于中间步骤并在其他地方进行最终存储。实际上可能更快。)

    更新:我最初描述我的算法有点不正确,然后我意识到我实际上可以做我描述的事情。这个每 64 位运行大约 65.7 个周期。

    编辑 #2:在这台机器上尝试了上述 AVX 版本之一。在我的硬件(Xeon E3-1505M,名义上为 3GHz)上,每个 64 字节块有 10 个多一点的周期,因此,每个周期大约 6 个字节。这对我来说似乎比每字节 1.5 个周期更合理。

    编辑#3:更进一步,每 64 位大约 45 个周期,只需将第一部分写为 uint64 上的移位和掩码,而不是试图变得“聪明”并只移动我关心的 32 位。

    【讨论】:

    • 为了在 Sandybridge(?) CPU 上获得最佳吞吐量,您可能需要调整 wim 的 AVX2 _mm_blend_epi32 以使用 AVX1 _mm_blend_ps。或者实际上不是,因为 Sandybridge 具有 2/时钟整数 shuffle 吞吐量,这与 Haswell 以及后来的 AVX2 不同。所以 12 整数 shuffle 版本应该很棒。在 SnB 上,FP shuffle 具有 1/clock 的吞吐量,因为它们存在 256 位版本,并且它们仅扩大了支持这些 shuffle 或其他东西的 shuffle 单元。
    • 我很好奇它编译成什么 asm。但我不太了解 Go,因此将其复制/粘贴到 godbolt.org/z/sF0p23 不起作用:GCCGo 阻塞了 unsafe 关键字。
    • 我很好,没有最好的吞吐量,我只是想看看我是否可以让它“足够快”而不必进行组装。这应该可以直接转换为朴素的 C,只需将传入的 64 字节数组类型转换为 uint64_t 数组,然后都是移位操作等。如果您添加了我忘记包含在其中的缺少的import ("unsafe")(我现在正在添加),您可以在 godbolt.org 上编译它。
    • 将 shift 更改为 c = b ^ (a&gt;&gt;16)) &amp; 0x0000ffff0000ffffull; a,c = a ^ (c &lt;&lt;16), b^c;,内核中的指令数减少到 48(在 arm64 中)。
    【解决方案7】:

    这里的大多数答案使用_mm_shuffle_epi8 结合使用不同大小的随机播放和排列,这仅在 SSSE3 及更高版本中可用。

    具有 12* 指令内核的纯 SSE2 实现可以通过连续三次将前 32 个元素与后 32 个元素交错形成:

    void systolic_kernel(__m128i a[4]) {
        __m128i a0 = _mm_unpacklo_epi8(a[0], a[2]);
        __m128i a1 = _mm_unpackhi_epi8(a[0], a[2]);
        __m128i a2 = _mm_unpacklo_epi8(a[1], a[3]);
        __m128i a3 = _mm_unpackhi_epi8(a[1], a[3]);
        a[0] = a0;
        a[1] = a1;
        a[2] = a2;
        a[3] = a3;
    }
    
    void transpose(__m128i a[4]) {
        systolic_kernel(a);
        systolic_kernel(a);
        systolic_kernel(a);
    }
    

    *如果没有 VEX 编码(对于三个操作数指令),将添加 6 条潜在的零成本 movdqa 指令。

    相同的策略可以更容易地应用于 4x4、16x16 转置等,因为要置换的索引的计算和块大小已从等式中扣除。

    【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-02-22
    • 2016-10-31
    • 1970-01-01
    • 2017-03-11
    • 2020-09-25
    • 2014-09-09
    相关资源
    最近更新 更多