C 从基于源的索引转换为基于目标的索引
我在一些C代码中使用AVX2指令 指令获取两个8整数向量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
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