C 从基于源的索引转换为基于目标的索引

C 从基于源的索引转换为基于目标的索引,c,math,sse,simd,avx2,C,Math,Sse,Simd,Avx2,我在一些C代码中使用AVX2指令 指令获取两个8整数向量a和idx,并通过基于idx排列a生成第三个8整数向量dst。对于0..7中的i,这似乎相当于dst[i]=a[idx[i]]。我称之为基于源的,因为移动是基于源的索引 然而,我的计算指数是基于目的地的形式。这对于设置数组来说很自然,相当于0..7中i的dst[idx[i]]=a[i] 如何从基于源的表单转换为基于目标的表单?一个示例测试用例是: {2 1 0 5 3 4 6 7} source-based form. {2 1 0

我在一些C代码中使用AVX2指令

指令获取两个8整数向量
a
idx
,并通过基于
idx
排列
a
生成第三个8整数向量
dst
。对于0..7中的i,这似乎相当于
dst[i]=a[idx[i]]。我称之为基于源的,因为移动是基于源的索引

然而,我的计算指数是基于目的地的形式。这对于设置数组来说很自然,相当于0..7中i的dst[idx[i]]=a[i]

如何从基于源的表单转换为基于目标的表单?一个示例测试用例是:

{2 1 0 5 3 4 6 7}    source-based form. 
{2 1 0 4 5 3 6 7}    destination-based equivalent

对于这个转换,我将留在ymm寄存器中,这意味着基于目标的解决方案不起作用。即使我分别插入每一个,因为它只对常量索引进行操作,您也不能只设置它们。

我猜您是在含蓄地说,您首先不能修改代码来计算基于源代码的索引?除了采用基于dst索引的AVX512分散指令外,我想不出可以用x86 SIMD做什么。(但在当前CPU上,即使与收集负载相比,这些速度也不是很快。)

存储到内存、反转和重新加载向量实际上可能是最好的。(或者直接传输到整数寄存器,而不是通过内存,可能在VEXTRACT128/packusdw之后,所以您只需要从矢量到整数寄存器的两个64位传输:movq和pextrq)


但无论如何,然后使用它们作为索引将计数器存储到内存中的数组中,并将其作为向量重新加载。这仍然是缓慢和丑陋的,包括存储转发失败延迟。因此,如果可能的话,更改索引生成代码以生成基于源代码的洗牌向量可能是值得的。

我想你是在暗示你不能修改代码来计算基于源代码的索引?除了采用基于dst索引的AVX512分散指令外,我想不出可以用x86 SIMD做什么。(但在当前CPU上,即使与收集负载相比,这些速度也不是很快。)

存储到内存、反转和重新加载向量实际上可能是最好的。(或者直接传输到整数寄存器,而不是通过内存,可能在VEXTRACT128/packusdw之后,所以您只需要从矢量到整数寄存器的两个64位传输:movq和pextrq)


但无论如何,然后使用它们作为索引将计数器存储到内存中的数组中,并将其作为向量重新加载。这仍然是缓慢和丑陋的,包括存储转发失败延迟。因此,如果可能的话,更改索引生成代码以生成基于源代码的洗牌向量可能是值得的。

我也有同样的问题,但方向相反:目标索引很容易计算,但应用SIMD排列指令需要源索引。这里有一个AVX-512的解决方案,使用Peter Cordes建议的分散指令;它还应适用于相反方向:

__m512i ident = _mm512_set_epi32(15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0);
__m512i perm  = _mm512_set_epi32(7,9,3,0,5,8,13,11,4,2,15,1,12,6,10,14);  
uint32_t id[16], in[16], out[16];
_mm512_storeu_si512(id, ident);
for (int i = 0; i < 16; i++) printf("%2d ", id[i]); puts("");
_mm512_storeu_si512(in, perm);
for (int i = 0; i < 16; i++) printf("%2d ", in[i]); puts("");
_mm512_i32scatter_epi32(out, perm, ident, 4);
for (int i = 0; i < 16; i++) printf("%2d ", out[i]); puts("");
注意,我有数学意义上的排列(没有重复)。对于重复项,
out
存储需要初始化,因为某些元素可能会保持未写入状态


我也认为在寄存器中实现这一点并不容易。我考虑通过反复应用排列指令来循环给定的排列。一旦达到身份模式,之前的模式就是反向排列(这可以追溯到EOF on的想法)。然而,周期可能很长。16个元件可能需要的最大循环次数为140,见下表。我可以证明,如果单个排列子循环在与标识元素重合时立即冻结,则可以将其缩短到最多16个。将随机排列模式测试的平均排列指令从28条缩短为9条。但是,它仍然不是一个有效的解决方案(比我在另一个答案中描述的吞吐量基准中的分散指令慢得多)。

我遇到了相同的问题,但方向相反:目标索引很容易计算,但应用SIMD排列指令需要源索引。这里有一个AVX-512的解决方案,使用Peter Cordes建议的分散指令;它还应适用于相反方向:

__m512i ident = _mm512_set_epi32(15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0);
__m512i perm  = _mm512_set_epi32(7,9,3,0,5,8,13,11,4,2,15,1,12,6,10,14);  
uint32_t id[16], in[16], out[16];
_mm512_storeu_si512(id, ident);
for (int i = 0; i < 16; i++) printf("%2d ", id[i]); puts("");
_mm512_storeu_si512(in, perm);
for (int i = 0; i < 16; i++) printf("%2d ", in[i]); puts("");
_mm512_i32scatter_epi32(out, perm, ident, 4);
for (int i = 0; i < 16; i++) printf("%2d ", out[i]); puts("");
注意,我有数学意义上的排列(没有重复)。对于重复项,
out
存储需要初始化,因为某些元素可能会保持未写入状态


我也认为在寄存器中实现这一点并不容易。我考虑通过反复应用排列指令来循环给定的排列。一旦达到身份模式,之前的模式就是反向排列(这可以追溯到EOF on的想法)。然而,周期可能很长。16个元件可能需要的最大循环次数为140,见下表。我可以证明,如果单个排列子循环在与标识元素重合时立即冻结,则可以将其缩短到最多16个。将随机排列模式测试的平均排列指令从28条缩短为9条。但是,它仍然不是一个有效的解决方案(比我的另一个答案中描述的吞吐量基准中的分散指令慢得多)。

为了基准化解决方案,我修改了我的另一个答案中的代码,以比较分散指令(
使用分散
定义)与存储和顺序排列的性能(
USE_SCATTER
undefined)。我必须将结果复制回置换模式
perm
,以防止编译器优化循环体:

#ifdef TEST_SCATTER
  #define REPEATS 1000000001
  #define USE_SCATTER
  
  __m512i ident = _mm512_set_epi32(15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0);
  __m512i perm  = _mm512_set_epi32(7,9,3,0,5,8,13,11,4,2,15,1,12,6,10,14);  
  uint32_t outA[16] __attribute__ ((aligned(64)));
  uint32_t id[16], in[16];
  _mm512_storeu_si512(id, ident);
  for (int i = 0; i < 16; i++) printf("%2d ", id[i]); puts("");
  _mm512_storeu_si512(in, perm);
  for (int i = 0; i < 16; i++) printf("%2d ", in[i]); puts("");
#ifdef USE_SCATTER
  puts("scatter");
  for (long t = 0; t < REPEATS; t++) {
    _mm512_i32scatter_epi32(outA, perm, ident, 4);
    perm = _mm512_load_si512(outA);
  }
#else
  puts("store & permute");
  uint32_t permA[16] __attribute__ ((aligned(64)));
  for (long t = 0; t < REPEATS; t++) {
    _mm512_store_si512(permA, perm);
    for (int i = 0; i < 16; i++) outA[permA[i]] = i;
    perm = _mm512_load_si512(outA);    
  }
#endif
  for (int i = 0; i < 16; i++) printf("%2d ", outA[i]); puts("");

#endif
运行时大致相同(英特尔(R)至强(R)W-2125 CPU@4.0
 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 
14 10  6 12  1 15  2  4 11 13  8  5  0  3  9  7 
store & permute
12  4  6 13  7 11  2 15 10 14  1  8  3  9  0  5 
10.765u 0.001s 0:11.22 95.9%    0+0k 0+0io 0pf+0w

 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 
14 10  6 12  1 15  2  4 11 13  8  5  0  3  9  7 
scatter
12  4  6 13  7 11  2 15 10 14  1  8  3  9  0  5 
10.740u 0.000s 0:11.19 95.9%    0+0k 40+0io 0pf+0w
  #define REPEATS 1000000
  #define ARRAYSIZE 1000
  #define USE_SCATTER
  
  std::srand(unsigned(std::time(0)));
  // build array with random permutations
  uint32_t permA[ARRAYSIZE][16] __attribute__ ((aligned(64)));
  for (int i = 0; i < ARRAYSIZE; i++)
    _mm512_store_si512(permA[i], randPermZMM());
  // vector register
  __m512i perm;
#ifdef USE_SCATTER
  puts("scatter");
  __m512i ident = _mm512_set_epi32(15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0);
  for (long t = 0; t < REPEATS; t++)
    for (long i = 0; i < ARRAYSIZE; i++) {
      perm = _mm512_load_si512(permA[i]);
      _mm512_i32scatter_epi32(permA[i], perm, ident, 4);
    }
#else
  uint32_t permAsingle[16] __attribute__ ((aligned(64)));
  puts("store & permute");
  for (long t = 0; t < REPEATS; t++)
    for (long i = 0; i < ARRAYSIZE; i++) {
      perm = _mm512_load_si512(permA[i]);
      _mm512_store_si512(permAsingle, perm);
      uint32_t *permAVec = permA[i];
      for (int e = 0; e < 16; e++)
    permAVec[permAsingle[e]] = e;
    }
#endif
  FILE *f = fopen("testperm.dat", "w");
  fwrite(permA, ARRAYSIZE, 64, f);
  fclose(f);
scatter
4.241u 0.002s 0:04.26 99.5% 0+0k 80+128io 0pf+0w

store & permute
5.956u 0.002s 0:05.97 99.6% 0+0k 80+128io 0pf+0w