从基于来源的指数转换为基于目的地的指数

时间:2016-08-31 23:35:08

标签: c math sse simd avx2

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

VPERMD指令采用两个8整数向量aidx,并通过基于{{置换dst来生成第三个a。 1}}。这似乎相当于idx。我基于这个来源调用此源,因为该移动是根据源进行索引的。

但是,我有基于目的地形式的计算索引。这对于设置数组很自然,相当于dst[i] = a[idx[i]] for i in 0..7

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

dst[idx[i]] = a[i] for i in 0..7

对于这种转换,我会留在ymm寄存器中,这意味着基于目标的解决方案无法正常工作。即使我要单独插入每个,因为它只对常量索引进行操作,所以你不能只设置它们。

3 个答案:

答案 0 :(得分:2)

我猜你是否暗中说你不能修改你的代码来计算基于源的索引?我不能想到你可以用x86 SIMD做什么,除了采用基于dst索引的AVX512分散指令。

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

但无论如何,然后使用它们作为索引将计数器存储到内存中的数组中,并将其作为向量重新加载。这仍然是缓慢而丑陋的,并且包括存储转发故障延迟。因此,改变生成索引的代码以生成基于源的随机向量可能值得您花些时间。

答案 1 :(得分:1)

为了对解决方案进行基准测试,我从另一个答案中修改了代码,以比较分散指令(定义为USE_SCATTER与存储和顺序排列(USE_SCATTER未定义)的性能。我必须将结果复制回排列模式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

以下是这两种情况的输出(使用time的内置tcsh命令,u的输出是用户空间时间,以秒为单位):

 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

运行时间大致相同(Intel®Xeon®W-2125 CPU @ 4.00GHz,clang ++-6.0,-O3 -funroll-loops -march=native)。我检查了生成的汇编代码。定义为USE_SCATTER时,编译器将生成vpscatterdd条指令,而无需使用vpextrdvpextrqvpextracti32x4生成复杂的代码。


编辑:我担心编译器可能为我使用的固定排列模式找到了一种特定的解决方案。因此,我用std::random_shuffe()中随机生成的模式替换了它,但是时间测量值大致相同。


编辑:根据彼得·科德斯(Peter Cordes)的评论,我写了一个经过修改的基准测试,希望可以测量诸如吞吐量之类的数据:

  #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

因此,使用scatter命令时吞吐量会更好。

答案 2 :(得分:0)

我遇到了同样的问题,但方向相反:目标索引易于计算,但应用SIMD置换指令需要源索引。这是Peter Cordes建议的使用分散指令的AVX-512解决方案;它也应适用于相反的方向:

__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("");

根据索引模式ident,将身份映射out分配到perm数组。这个想法基本上与针对inverting a permutation所描述的想法相同。这是输出:

 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 
12  4  6 13  7 11  2 15 10 14  1  8  3  9  0  5 

请注意,我具有数学意义上的排列(无重复)。对于重复项,out存储区需要初始化,因为某些元素可能会保持未写入状态。

我也看不到在寄存器中实现此目的的简便方法。我考虑过通过重复应用置换指令循环遍历给定的置换。一旦达到身份模式,前面的一个就是逆排列(这可以追溯到EOF在unzip operations上的想法)。但是,周期可能很长。 Landau's function给出了可能需要的最大循环数,对于16个元素为140,请参见此table。我可以证明,如果将单个置换子周期与标识元素重合时将其冻结,则可以将其最多缩短到16个。对于随机排列模式的测试,该缩短将平均置换指令从28条缩短为9条。但是,它仍然不是一种有效的解决方案(比我在其他答案中描述的吞吐量基准中的分散指令要慢得多。)