最快的 stride-3 收集指令序列是啥?

Posted

技术标签:

【中文标题】最快的 stride-3 收集指令序列是啥?【英文标题】:What's the fastest stride-3 gather instruction sequence?最快的 stride-3 收集指令序列是什么? 【发布时间】:2017-07-08 09:39:54 【问题描述】:

问题:

从内存中生成 32 位元素的 stride-3 集合的最有效序列是什么? 如果内存排列为:

MEM = R0 G0 B0 R1 G1 B1 R2 G2 B2 R3 G3 B3 ...

我们要获得三个 YMM 寄存器,其中:

YMM0 = R0 R1 R2 R3 R4 R5 R6 R7
YMM1 = G0 G1 G2 G3 G4 G5 G6 G7
YMM2 = B0 B1 B2 B3 B4 B5 B6 B7

动机和讨论

标量 C 代码类似于

template <typename T>
T Process(const T* Input) 
  T Result = 0;
  for (int i=0; i < 4096; ++i) 
    T R = Input[3*i];
    T G = Input[3*i+1];
    T B = Input[3*i+2];
    Result += some_parallelizable_algorithm<T>(R, G, B);  
  
  return Result;

假设 some_parallelizable_algorithm 是用内在函数编写的,并已调整为尽可能快的实现:

template <typename T>
__m256i some_parallelizable_algorithm(__m256i R, __m256i G, __m256i B);

所以 T=int32_t 的向量实现可以是这样的:

    template <>
    int32_t Process<int32_t>(const int32_t* Input) 
     __m256i Step = _mm256_set_epi32(0, 1, 2, 3, 4, 5, 6, 7);
     __m256i Result = _mm256_setzero_si256(); 
     for (int i=0; i < 4096; i+=8) 
       // R = R0 R1 R2 R3 R4 R5 R6 R7
       __m256i R = _mm256_i32gather_epi32 (Input+3*i, Step, 3);
       // G = G0 G1 G2 G3 G4 G5 G6 G7
       __m256i G = _mm256_i32gather_epi32 (Input+3*i+1, Step, 3);
       // B = B0 B1 B2 B3 B4 B5 B6 B7
       __m256i B = _mm256_i32gather_epi32 (Input+3*i+2, Step, 3);
       Result = _mm256_add_epi32 (Result, 
                                  some_parallelizable_algorithm<int32_t>(R, G, B));
     
   // Here should be the less interesting part:
   // Perform a reduction on Result and return the result

首先,可以这样做,因为有针对 32 位元素的收集指令,但没有针对 16 位元素或 8 位元素的指令。 其次,更重要的是,出于性能原因,应完全避免使用上述收集指令。使用连续的宽负载并将加载的值打乱以获得 R、G 和 B 向量可能更有效。

    template <>
    int32_t Process<int32_t>(const int32_t* Input) 
     __m256i Result = _mm256_setzero_si256(); 
     for (int i=0; i < 4096; i+=3) 
       __m256i Ld0 = _mm256_lddqu_si256((__m256i*)Input+3*i));
       __m256i Ld1 = _mm256_lddqu_si256((__m256i*)Input+3*i+1));
       __m256i Ld2 = _mm256_lddqu_si256((__m256i*)Input+3*i+2));
       __m256i R = ???
       __m256i G = ???
       __m256i B = ???
       Result = _mm256_add_epi32 (Result, 
                                  some_parallelizable_algorithm<int32_t>(R, G, B));
     
   // Here should be the less interesting part:
   // Perform a reduction on Result and return the result

似乎对于 power-2 stride (2, 4, ...) 有使用 UNKPCKL/UNKPCKH 的已知方法,但对于 stride-3 访问,我找不到任何参考。

我有兴趣解决 T=int32_t、T=int16_t 和 T=int8_t 的问题,但为了保持专注,我们只讨论第一种情况。

【问题讨论】:

使用连续的宽负载和随机播放可能更有效。是的,当然,但你是对的,如何做好这件事并不明显。 >.pshufb 和 por。也许我们可以以某种方式屏蔽和使用packuswb,并对混合了 R、​​G 和 B 的向量做一些事情?可能不会,因为屏蔽输入以消除饱和是昂贵的,并且合并可能需要vpblendvb(与vpblendd相比更昂贵) 也许pshufbpackuswb 设置两个输入(因为pshufb 将设置控制向量高位的元素归零),并在第三个与另一个pshufb 合并+ por? 在某些情况下,如果可以进行自动矢量化,英特尔编译器可能会为您完成 - codeproject.com/Articles/1182515/… @PeterCordes 这些“奇数”交错有一种通用的方法来维护O(N*log(SIMD-width)) 指令计数(如果您使用标量指令,则为O(N * SIMD-width))。因为我需要它们高达 stride-19。但我面前没有它,因为它很复杂。 @zr。它们不存在。这是我几年前遇到同样问题时得出的,但我还没有在任何地方发表过。因此,除非其他人重新发明了它(或者如果他们先来,我重新发明了它),否则您将找不到参考。下班回家后我会发布 3-stride 的解决方案,因为我无法在此处访问它。 【参考方案1】:

This article from Intel 描述了如何精确地完成您想要的 3x8 大小写。

那篇文章涵盖了float 案例。如果你想要int32,你需要转换输出,因为_mm256_shuffle_ps() 没有整数版本。

逐字复制他们的解决方案:

float *p;  // address of first vector
__m128 *m = (__m128*) p;
__m256 m03;
__m256 m14; 
__m256 m25; 
m03  = _mm256_castps128_ps256(m[0]); // load lower halves
m14  = _mm256_castps128_ps256(m[1]);
m25  = _mm256_castps128_ps256(m[2]);
m03  = _mm256_insertf128_ps(m03 ,m[3],1);  // load upper halves
m14  = _mm256_insertf128_ps(m14 ,m[4],1);
m25  = _mm256_insertf128_ps(m25 ,m[5],1);

__m256 xy = _mm256_shuffle_ps(m14, m25, _MM_SHUFFLE( 2,1,3,2)); // upper x's and y's 
__m256 yz = _mm256_shuffle_ps(m03, m14, _MM_SHUFFLE( 1,0,2,1)); // lower y's and z's
__m256 x  = _mm256_shuffle_ps(m03, xy , _MM_SHUFFLE( 2,0,3,0)); 
__m256 y  = _mm256_shuffle_ps(yz , xy , _MM_SHUFFLE( 3,1,2,0)); 
__m256 z  = _mm256_shuffle_ps(yz , m25, _MM_SHUFFLE( 3,0,3,1)); 

所以这是 11 条指令。 (6 次加载,5 次随机播放)


在一般情况下,可以在O(S*log(W)) 指令中执行S x W 转置。地点:

S 是大步 W 是 SIMD 宽度

假设存在2-vector permutes和half-vector insert-loads,则公式变为:

(S x W load-permute)  <=  S * (lg(W) + 1) instructions

忽略 reg-reg 移动。对于像3 x 4 这样的退化案例,可能会做得更好。

这是使用 AVX512 的 3 x 16 加载转置:(6 次加载,3 次随机播放,6 次混合)

FORCE_INLINE void transpose_f32_16x3_forward_AVX512(
    const float T[48],
    __m512& r0, __m512& r1, __m512& r2
)
    __m512 a0, a1, a2;

    //   0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15
    //  16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
    //  32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47

    a0 = _mm512_castps256_ps512(_mm256_loadu_ps(T +  0));
    a1 = _mm512_castps256_ps512(_mm256_loadu_ps(T +  8));
    a2 = _mm512_castps256_ps512(_mm256_loadu_ps(T + 16));
    a0 = _mm512_insertf32x8(a0, ((const __m256*)T)[3], 1);
    a1 = _mm512_insertf32x8(a1, ((const __m256*)T)[4], 1);
    a2 = _mm512_insertf32x8(a2, ((const __m256*)T)[5], 1);

    //   0  1  2  3  4  5  6  7 24 25 26 27 28 29 30 31
    //   8  9 10 11 12 13 14 15 32 33 34 35 36 37 38 39
    //  16 17 18 19 20 21 22 23 40 41 42 43 44 45 46 47

    r0 = _mm512_mask_blend_ps(0xf0f0, a0, a1);
    r1 = _mm512_permutex2var_ps(a0, _mm512_setr_epi32(  4,  5,  6,  7, 16, 17, 18, 19, 12, 13, 14, 15, 24, 25, 26, 27), a2);
    r2 = _mm512_mask_blend_ps(0xf0f0, a1, a2);

    //   0  1  2  3 12 13 14 15 24 25 26 27 36 37 38 39
    //   4  5  6  7 16 17 18 19 28 29 30 31 40 41 42 43
    //   8  9 10 11 20 21 22 23 32 33 34 35 44 45 46 47

    a0 = _mm512_mask_blend_ps(0xcccc, r0, r1);
    a1 = _mm512_shuffle_ps(r0, r2, 78);
    a2 = _mm512_mask_blend_ps(0xcccc, r1, r2);

    //   0  1  6  7 12 13 18 19 24 25 30 31 36 37 42 43
    //   2  3  8  9 14 15 20 21 26 27 32 33 38 39 44 45
    //   4  5 10 11 16 17 22 23 28 29 34 35 40 41 46 47

    r0 = _mm512_mask_blend_ps(0xaaaa, a0, a1);
    r1 = _mm512_permutex2var_ps(a0, _mm512_setr_epi32(  1,  16,  3, 18,  5, 20,  7, 22,  9, 24, 11, 26, 13, 28, 15, 30), a2);
    r2 = _mm512_mask_blend_ps(0xaaaa, a1, a2);

    //   0  3  6  9 12 15 18 21 24 27 30 33 36 39 42 45
    //   1  4  7 10 13 16 19 22 25 28 31 34 37 40 43 46
    //   2  5  8 11 14 17 20 23 26 29 32 35 38 41 44 47

逆向3 x 16 transpose-store 将作为练习留给读者。

因为S = 3 有点退化,所以这种模式一点也不简单。但是,如果您能看到这种模式,您就可以将其推广到任何奇数 S 以及任何二次幂 W

【讨论】:

请注意,vinsertf128 y,y,m,imm8 是 Intel/AMD CPU 上的 2-uop 加载+混合指令。所以我可能会说成本是“3 次加载,3 次加载+混合,5 次洗牌”。 (在 Sandybridge 上,来自内存源的 vinsertf128 需要端口 5,但在 HSW/SKL 和 Ryzen 上,ALU uop 可以在任何矢量 ALU 端口上运行。) 这仅回答了int32_t 部分问题,对吧?对于更窄的元素来说,看起来工作量要大得多,因为没有shufps 的更小的元素版本。就像输入中的 pshufb 一样,在输入到 shufps 之前将元素组合在一起? @PeterCordes 您还可以用 3 个半向量加载 + 3 个半向量插入换取 3 个全向量加载 + 3 个置换。由于您正在增加端口 5 压力,因此在当前 CPU 上这不是一个很好的权衡。 @PeterCordes 只要您提供一种进行适当置换的方法,就可以扩展到任何粒度(包括字节)。在较小的粒度上,您会注意到 2 向量置换实际上并没有穿过任何车道。所以它们可以用 shift + blend-masking 来模拟 - 从而减轻端口 5 的压力。我在 AVX512 示例上的 3 x 16 具有比随机播放更多的混合,因此它甚至可能不受端口 5 限制。对于3 x 16 情况,通用S x W 算法需要9 x 2-vector shuffle。碰巧其中 6 次洗牌减少到混合。 @PeterCordes 嗯... 3 x 16 AVX512 机箱足够小,可以使用 2-vector permutes 进行暴力破解。 3 个 512 位加载和 6 个 _mm512_permutex2var_ps()。有 3 个输出,每个只有 3 个输入。 2 个排列可以让你从 3 个向量中提取任何你想要的东西。所以3 x 2 = 6 置换。如果我今晚有时间,我可能会拼凑出那个解决方案。我的解决方案中的一个只是与通用S x W 方法的直接偏差。这种通用方法实际上只对 S &gt;= 5 有用。【参考方案2】:

8 位整数大小写。

上面的cmets中已经提到,两个输入shuffle指令,比如vshufps,不要 存在 8 位粒度。因此,8 位解决方案与 32 位解决方案略有不同。下面介绍两种不同的解决方案。


一种简单的方法是将 8 位整数 'color by color (R G B)' 与 6 vpblendvb-s 分组,然后 通过vpshufb 排列:

#include <stdio.h>
#include <x86intrin.h>
/*  gcc -O3 -Wall -m64 -march=broadwell stride_3.c   */
int __attribute__ ((noinline)) print_vec_char(__m256i x);

int main() 

    char *m;
    int i;
    __m256i blnd1 = _mm256_set_epi8(0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,     0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0);
    __m256i blnd2 = _mm256_set_epi8(0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,     0,-1,0,0,-1,0,0,-1,0,0,-1,0,0,-1,0,0);
    __m256i p0    = _mm256_set_epi8(13,10,7,4,1, 14,11,8,5,2, 15,12,9,6,3,0,    13,10,7,4,1, 14,11,8,5,2, 15,12,9,6,3,0);
    __m256i p1    = _mm256_set_epi8(14,11,8,5,2, 15,12,9,6,3,0, 13,10,7,4,1,    14,11,8,5,2, 15,12,9,6,3,0, 13,10,7,4,1);
    __m256i p2    = _mm256_set_epi8(15,12,9,6,3,0, 13,10,7,4,1, 14,11,8,5,2,    15,12,9,6,3,0, 13,10,7,4,1, 14,11,8,5,2);
            m     = _mm_malloc(96,32);
    for(i = 0; i < 96; i++) m[i] = i;

//        printf("m_lo  ");print_vec_char(_mm256_load_si256((__m256i*)&m[0]));printf("m_mid ");print_vec_char(_mm256_load_si256((__m256i*)&m[32]));printf("m_hi  ");print_vec_char(_mm256_load_si256((__m256i*)&m[64]));printf("\n");
//        m_lo   31  30  29  28 |  27  26  25  24 |  23  22  21  20 |  19  18  17  16 ||  15  14  13  12 |  11  10   9   8 |   7   6   5   4 |   3   2   1   0 
//        m_mid  63  62  61  60 |  59  58  57  56 |  55  54  53  52 |  51  50  49  48 ||  47  46  45  44 |  43  42  41  40 |  39  38  37  36 |  35  34  33  32 
//        m_hi   95  94  93  92 |  91  90  89  88 |  87  86  85  84 |  83  82  81  80 ||  79  78  77  76 |  75  74  73  72 |  71  70  69  68 |  67  66  65  64 

    __m256i t0 = _mm256_castsi128_si256(_mm_loadu_si128((__m128i*)&m[0]));
    __m256i t1 = _mm256_castsi128_si256(_mm_loadu_si128((__m128i*)&m[16]));
    __m256i t2 = _mm256_castsi128_si256(_mm_loadu_si128((__m128i*)&m[32]));

            t0 = _mm256_inserti128_si256(t0,_mm_loadu_si128((__m128i*)&m[48]),1);
            t1 = _mm256_inserti128_si256(t1,_mm_loadu_si128((__m128i*)&m[64]),1);
            t2 = _mm256_inserti128_si256(t2,_mm_loadu_si128((__m128i*)&m[80]),1);

//        printf("t0  ");print_vec_char(t0);printf("t1  ");print_vec_char(t1);printf("t2  ");print_vec_char(t2);printf("\n");
//        t0   63  62  61  60 |  59  58  57  56 |  55  54  53  52 |  51  50  49  48 ||  15  14  13  12 |  11  10   9   8 |   7   6   5   4 |   3   2   1   0 
//        t1   79  78  77  76 |  75  74  73  72 |  71  70  69  68 |  67  66  65  64 ||  31  30  29  28 |  27  26  25  24 |  23  22  21  20 |  19  18  17  16 
//        t2   95  94  93  92 |  91  90  89  88 |  87  86  85  84 |  83  82  81  80 ||  47  46  45  44 |  43  42  41  40 |  39  38  37  36 |  35  34  33  32 

    __m256i u0 = _mm256_blendv_epi8( _mm256_blendv_epi8(t0,t1,blnd2), t2,blnd1);
    __m256i u1 = _mm256_blendv_epi8( _mm256_blendv_epi8(t1,t2,blnd2), t0,blnd1);
    __m256i u2 = _mm256_blendv_epi8( _mm256_blendv_epi8(t2,t0,blnd2), t1,blnd1);

//        printf("u0  ");print_vec_char(u0);printf("u1  ");print_vec_char(u1);printf("u2  ");print_vec_char(u2);printf("\n");
//        u0   63  78  93  60 |  75  90  57  72 |  87  54  69  84 |  51  66  81  48 ||  15  30  45  12 |  27  42   9  24 |  39   6  21  36 |   3  18  33   0 
//        u1   79  94  61  76 |  91  58  73  88 |  55  70  85  52 |  67  82  49  64 ||  31  46  13  28 |  43  10  25  40 |   7  22  37   4 |  19  34   1  16 
//        u2   95  62  77  92 |  59  74  89  56 |  71  86  53  68 |  83  50  65  80 ||  47  14  29  44 |  11  26  41   8 |  23  38   5  20 |  35   2  17  32 

            t0 = _mm256_shuffle_epi8(u0,p0);
            t1 = _mm256_shuffle_epi8(u1,p1);
            t2 = _mm256_shuffle_epi8(u2,p2);

          printf("t0  ");print_vec_char(t0);printf("t1  ");print_vec_char(t1);printf("t2  ");print_vec_char(t2);printf("\n");
//        t0   93  90  87  84 |  81  78  75  72 |  69  66  63  60 |  57  54  51  48 ||  45  42  39  36 |  33  30  27  24 |  21  18  15  12 |   9   6   3   0 
//        t1   94  91  88  85 |  82  79  76  73 |  70  67  64  61 |  58  55  52  49 ||  46  43  40  37 |  34  31  28  25 |  22  19  16  13 |  10   7   4   1 
//        t2   95  92  89  86 |  83  80  77  74 |  71  68  65  62 |  59  56  53  50 ||  47  44  41  38 |  35  32  29  26 |  23  20  17  14 |  11   8   5   2 

    return 0;




int __attribute__ ((noinline)) print_vec_char(__m256i x)
    char v[32];
    _mm256_storeu_si256((__m256i *)v,x);
    printf("%3hhi %3hhi %3hhi %3hhi | %3hhi %3hhi %3hhi %3hhi | %3hhi %3hhi %3hhi %3hhi | %3hhi %3hhi %3hhi %3hhi || ",
           v[31],v[30],v[29],v[28],v[27],v[26],v[25],v[24],v[23],v[22],v[21],v[20],v[19],v[18],v[17],v[16]);
    printf("%3hhi %3hhi %3hhi %3hhi | %3hhi %3hhi %3hhi %3hhi | %3hhi %3hhi %3hhi %3hhi | %3hhi %3hhi %3hhi %3hhi \n",
           v[15],v[14],v[13],v[12],v[11],v[10],v[9],v[8],v[7],v[6],v[5],v[4],v[3],v[2],v[1],v[0]);
    return 0;

说明摘要:

    3 vmovdqu
    3 vinserti128-load
    6 vpblendvb   
    3 vpshufb

不幸的是,vpblendvb 指令通常比较慢: 在 Intel Skylake vpblendvb 上,每个周期的吞吐量为 1,并且 在 AMD Ryzen 和 Intel Haswell 上,吞吐量仅为每两个周期一个。 Skylake-X 具有快速字节混合vpblendmb(每个周期吞吐量三个(256 位)),尽管在 Skylake-X 上可能更多 对使用 512 位向量而不是 256 位的解决方案感兴趣。


另一种方法是将vpshufbvshufps 结合起来,正如上面@Peter Cordes 的cmets 中所建议的那样。 在下面的代码中,数据被加载为 12 字节的块。总共需要比第一个解决方案更多的指令。 尽管如此,第二个解决方案的性能可能比第一个解决方案更好,具体取决于周围的代码 和微架构。

#include <stdio.h>
#include <x86intrin.h>
/*  gcc -O3 -Wall -m64 -march=broadwell stride_3.c   */
int __attribute__ ((noinline)) print_vec_char(__m256i x);

inline __m256i _mm256_shufps_epi32(__m256i a,__m256i b,int imm)return _mm256_castps_si256(_mm256_shuffle_ps(_mm256_castsi256_ps(a),_mm256_castsi256_ps(b),imm));

int main() 

    char *m;
    int i;
    __m256i p0    = _mm256_set_epi8(-1,-1,-1,-1, 11,8,5,2, 10,7,4,1, 9,6,3,0,     -1,-1,-1,-1, 11,8,5,2, 10,7,4,1, 9,6,3,0);
    __m256i p1    = _mm256_set_epi8(11,8,5,2, 10,7,4,1, 9,6,3,0, -1,-1,-1,-1,     11,8,5,2, 10,7,4,1, 9,6,3,0, -1,-1,-1,-1);
    __m256i p2    = _mm256_set_epi8(10,7,4,1, 9,6,3,0, -1,-1,-1,-1, 11,8,5,2,     10,7,4,1, 9,6,3,0,-1, -1,-1,-1, 11,8,5,2);
    __m256i p3    = _mm256_set_epi8(9,6,3,0, -1,-1,-1,-1, 11,8,5,2, 10,7,4,1,     9,6,3,0, -1,-1,-1,-1, 11,8,5,2, 10,7,4,1);

            m     = _mm_malloc(96+4,32);   /* 4 extra dummy bytes to avoid errors with _mm_loadu_si128((__m128i*)&m[84]) . Otherwise use maskload instead of standard load */
    for(i = 0; i < 96; i++) m[i] = i;

//        printf("m_lo  ");print_vec_char(_mm256_load_si256((__m256i*)&m[0]));printf("m_mid ");print_vec_char(_mm256_load_si256((__m256i*)&m[32]));printf("m_hi  ");print_vec_char(_mm256_load_si256((__m256i*)&m[64]));printf("\n");
//        m_lo   31  30  29  28 |  27  26  25  24 |  23  22  21  20 |  19  18  17  16 ||  15  14  13  12 |  11  10   9   8 |   7   6   5   4 |   3   2   1   0 
//        m_mid  63  62  61  60 |  59  58  57  56 |  55  54  53  52 |  51  50  49  48 ||  47  46  45  44 |  43  42  41  40 |  39  38  37  36 |  35  34  33  32 
//        m_hi   95  94  93  92 |  91  90  89  88 |  87  86  85  84 |  83  82  81  80 ||  79  78  77  76 |  75  74  73  72 |  71  70  69  68 |  67  66  65  64 

    __m256i t0 = _mm256_castsi128_si256(_mm_loadu_si128((__m128i*)&m[0]));
    __m256i t1 = _mm256_castsi128_si256(_mm_loadu_si128((__m128i*)&m[12]));
    __m256i t2 = _mm256_castsi128_si256(_mm_loadu_si128((__m128i*)&m[24]));
    __m256i t3 = _mm256_castsi128_si256(_mm_loadu_si128((__m128i*)&m[36]));

            t0 = _mm256_inserti128_si256(t0,_mm_loadu_si128((__m128i*)&m[48]),1);
            t1 = _mm256_inserti128_si256(t1,_mm_loadu_si128((__m128i*)&m[60]),1);
            t2 = _mm256_inserti128_si256(t2,_mm_loadu_si128((__m128i*)&m[72]),1);
            t3 = _mm256_inserti128_si256(t3,_mm_loadu_si128((__m128i*)&m[84]),1);   /* Use a masked load (_mm_maskload_epi32) here if m[99] is not a valid address */

//        printf("t0  ");print_vec_char(t0);printf("t1  ");print_vec_char(t1);printf("t2  ");print_vec_char(t2);printf("t3  ");print_vec_char(t3);printf("\n");
//        t0   63  62  61  60 |  59  58  57  56 |  55  54  53  52 |  51  50  49  48 ||  15  14  13  12 |  11  10   9   8 |   7   6   5   4 |   3   2   1   0 
//        t1   75  74  73  72 |  71  70  69  68 |  67  66  65  64 |  63  62  61  60 ||  27  26  25  24 |  23  22  21  20 |  19  18  17  16 |  15  14  13  12 
//        t2   87  86  85  84 |  83  82  81  80 |  79  78  77  76 |  75  74  73  72 ||  39  38  37  36 |  35  34  33  32 |  31  30  29  28 |  27  26  25  24 
//        t3    0   0   0   0 |  95  94  93  92 |  91  90  89  88 |  87  86  85  84 ||  51  50  49  48 |  47  46  45  44 |  43  42  41  40 |  39  38  37  36 

            t0 = _mm256_shuffle_epi8(t0,p0);
            t1 = _mm256_shuffle_epi8(t1,p1);
            t2 = _mm256_shuffle_epi8(t2,p2);
            t3 = _mm256_shuffle_epi8(t3,p3);

//        printf("t0  ");print_vec_char(t0);printf("t1  ");print_vec_char(t1);printf("t2  ");print_vec_char(t2);printf("t3  ");print_vec_char(t3);printf("\n");
//        t0    0   0   0   0 |  59  56  53  50 |  58  55  52  49 |  57  54  51  48 ||   0   0   0   0 |  11   8   5   2 |  10   7   4   1 |   9   6   3   0 
//        t1   71  68  65  62 |  70  67  64  61 |  69  66  63  60 |   0   0   0   0 ||  23  20  17  14 |  22  19  16  13 |  21  18  15  12 |   0   0   0   0 
//        t2   82  79  76  73 |  81  78  75  72 |   0   0   0   0 |  83  80  77  74 ||  34  31  28  25 |  33  30  27  24 |   0   0   0   0 |  35  32  29  26 
//        t3   93  90  87  84 |   0   0   0   0 |  95  92  89  86 |  94  91  88  85 ||  45  42  39  36 |   0   0   0   0 |  47  44  41  38 |  46  43  40  37 

    __m256i u0 = _mm256_blend_epi32(t0,t1,0b10101010);
    __m256i u1 = _mm256_blend_epi32(t2,t3,0b10101010);
    __m256i u2 = _mm256_blend_epi32(t0,t1,0b01010101);
    __m256i u3 = _mm256_blend_epi32(t2,t3,0b01010101);

//        printf("u0  ");print_vec_char(u0);printf("u1  ");print_vec_char(u1);printf("u2  ");print_vec_char(u2);printf("u3  ");print_vec_char(u3);printf("\n");
//        u0   71  68  65  62 |  59  56  53  50 |  69  66  63  60 |  57  54  51  48 ||  23  20  17  14 |  11   8   5   2 |  21  18  15  12 |   9   6   3   0 
//        u1   93  90  87  84 |  81  78  75  72 |  95  92  89  86 |  83  80  77  74 ||  45  42  39  36 |  33  30  27  24 |  47  44  41  38 |  35  32  29  26 
//        u2    0   0   0   0 |  70  67  64  61 |  58  55  52  49 |   0   0   0   0 ||   0   0   0   0 |  22  19  16  13 |  10   7   4   1 |   0   0   0   0 
//        u3   82  79  76  73 |   0   0   0   0 |   0   0   0   0 |  94  91  88  85 ||  34  31  28  25 |   0   0   0   0 |   0   0   0   0 |  46  43  40  37 

            t0 = _mm256_blend_epi32(u0,u1,0b11001100);
            t1 = _mm256_shufps_epi32(u2,u3,0b00111001);
            t2 = _mm256_shufps_epi32(u0,u1,0b01001110);

        printf("t0  ");print_vec_char(t0);printf("t1  ");print_vec_char(t1);printf("t2  ");print_vec_char(t2);printf("\n");
//        t0   93  90  87  84 |  81  78  75  72 |  69  66  63  60 |  57  54  51  48 ||  45  42  39  36 |  33  30  27  24 |  21  18  15  12 |   9   6   3   0 
//        t1   94  91  88  85 |  82  79  76  73 |  70  67  64  61 |  58  55  52  49 ||  46  43  40  37 |  34  31  28  25 |  22  19  16  13 |  10   7   4   1 
//        t2   95  92  89  86 |  83  80  77  74 |  71  68  65  62 |  59  56  53  50 ||  47  44  41  38 |  35  32  29  26 |  23  20  17  14 |  11   8   5   2 

    return 0;




int __attribute__ ((noinline)) print_vec_char(__m256i x)
    char v[32];
    _mm256_storeu_si256((__m256i *)v,x);
    printf("%3hhi %3hhi %3hhi %3hhi | %3hhi %3hhi %3hhi %3hhi | %3hhi %3hhi %3hhi %3hhi | %3hhi %3hhi %3hhi %3hhi || ",
           v[31],v[30],v[29],v[28],v[27],v[26],v[25],v[24],v[23],v[22],v[21],v[20],v[19],v[18],v[17],v[16]);
    printf("%3hhi %3hhi %3hhi %3hhi | %3hhi %3hhi %3hhi %3hhi | %3hhi %3hhi %3hhi %3hhi | %3hhi %3hhi %3hhi %3hhi \n",
           v[15],v[14],v[13],v[12],v[11],v[10],v[9],v[8],v[7],v[6],v[5],v[4],v[3],v[2],v[1],v[0]);
    return 0;

说明摘要:

    4 vmovdqu
    4 vinserti128-load
    4 vpshufb
    5 vpblendd    (vpblendd is much faster than vpblendvb on most cpu architectures)
    2 vshufps

这些方法的思想很容易适应 16 位的情况。

【讨论】:

酷,有趣的是,看看我的想法是如何通过完整的实现实现的:)(顺便说一句,Haswell 上的vpblendvb 在这里特别糟糕,因为它是端口 5 的 2 微指令,所以它直接竞争vpshufb。Agner 说在 SKL 上 p015 是 2 微指令,但奇怪的是每个时钟仍然只有 1 个。)无论如何,在 Haswell 上,shufps 版本有 6 个随机端口微指令,而 vblendvb 版本有 15 个。使用未对齐的负载来替换/简化 shuffle 绝对是一个有用的技巧。 @PeterCordes 我做了一些简单的测试,周围的代码不相关,但即使在 Skylake 上,vpblendvb 版本似乎也比vpshufb + vshufps 版本慢。尽管如此,如果_mm512_permutexvar_epi8(idx, a) 指令集提供了一个快速通道穿越AVX512VBMI,那么字节混合的想法可能会很有趣。

以上是关于最快的 stride-3 收集指令序列是啥?的主要内容,如果未能解决你的问题,请参考以下文章

压缩+编码+检查+数组序列化的最快组合是啥?

显式多线程 SIMD 操作的最快方法是啥?

完成这些任务的最快方法是啥?

进行水平 SSE 向量求和(或其他缩减)的最快方法

找到素数最快的算法是啥?

大型数据集最快的分类算法是啥? [关闭]