选择性地使用AVX2指令对列表中的元素进行排序

时间:2018-05-29 11:52:18

标签: optimization x86 simd avx avx2

我想用AVX2指令加快以下操作,但我找不到办法。

我获得了一个大型数组uint64_t data[100000]的uint64_t和一个数组unsigned char indices[100000]的字节。我想输出一个数组uint64_t Out[256],其中第i个值是所有data[j]的xor,index[j]=i

直接实现我想要的是:

uint64_t Out[256] = {0};     // initialize output array
for (i = 0; i < 100000 ; i++) {
    Out[Indices[i]] ^= data[i];
}

我们可以使用AVX2指令更有效地实现这一点吗?

编辑:这就是我的代码现在的样子

uint64_t Out[256][4] = {0};   // initialize output array
for (i = 0; i < 100000 ; i+=4) {
    Out[Indices[i  ]][0] ^= data[i];
    Out[Indices[i+1]][1] ^= data[i+1];
    Out[Indices[i+2]][2] ^= data[i+2];
    Out[Indices[i+3]][3] ^= data[i+3];
}

2 个答案:

答案 0 :(得分:6)

基于对Haswell / Skylake的静态分析,我提出了一个版本,当由gcc编译时,每4 i个值运行约5个周期,而不是8个周期。大尺寸的平均值,不包括组合Out[]的多个副本的时间,并假设随机分布的指数不会导致任何存储/重新加载dep链运行足够长的时间。

如果您关心Ryzen或挖掘机(其他2个主流AVX2微架构),请使用IDK。

我还没有手工仔细分析,但IACA对于HSW / SKL来说是错误的,并且认为某些说明实际上并没有微融合(在i7上测试过) -6700k with perf counter),因此它认为前端瓶颈比实际更严重。例如movhps加载+合并微型保险丝,但IACA认为即使使用简单的寻址模式也不行。

我们应该忽略任何缓存未命中,因为uint64_t Out[4][256]只有8kiB。因此,我们的缓存占用空间仅为最新CPU上L1d大小的1/4,即使超线程在两个逻辑线程之间共享L1d,也应该大致正常。循环播放data[]Indices[]应该预取好,并且希望不会逐渐驱逐Out[]。因此,静态分析很有可能在某种程度上准确,并且比仔细的微基准测试更快,更重要的是告诉您瓶颈的确切含义。

但当然,我们在很大程度上依赖于无序执行和不完美的调度或其他意外的瓶颈很容易发生。不过,如果我没有得到报酬,我并不觉得实际上是微缩位标记。

  

我们可以使用AVX2指令更有效地实现这一点吗?

这基本上是一个直方图问题。使用多个表并在最后组合的常用直方图优化适用。 SIMD XOR对于端到端组合非常有用(只要您使用Out[4][256],而不是Out[256][4]。后者也需要按8*4进行缩放而不是缩放索引8(可以使用缩放索引寻址模式中的单个LEA完成)。

但是与普通直方图不同,您可以对内存中的某些数据进行异或,而不是添加常量1.因此代码必须加载1而不是立即data[i]。注册为xor的来源。 (或加载,然后xor reg, data[i] / store)。这比直方图更多的是总内存操作。

我们从&#34;手册&#34;收集/分散到SIMD向量中(使用movq / movhps加载/存储),允许我们使用SIMD进行data[i]加载和异或。这减少了负载操作的总数,从而降低了负载端口压力,而不会增加额外的前端带宽。

手动收集到256位向量可能不值得额外的改组(额外的vinserti128 / vextracti128,因此我们可以将2个内存源vpxor组合成一个256位的一个)。 128位向量应该是好的。前端吞吐量也是一个主要问题,因为(在Intel SnB系列CPU上)您希望避免存储的索引寻址模式。 gcc使用lea指令计算寄存器中的地址,而不是使用索引的加载/存储。具有-march=skylake的clang / LLVM决定不这样做,在这种情况下这是一个错误的决定,因为端口2 /端口3上的循环瓶颈,并且花费额外的ALU uops来允许存储地址uops使用端口7是一个胜利。但如果你在p23上瓶颈,花费额外的uops来避免索引商店并不好。 (而in cases where the can stay micro-fused,绝对不仅仅是为了避免索引加载;愚蠢的gcc)。也许gcc和LLVM的寻址模式成本模型不是非常准确,或者他们没有对管道进行足够详细的建模,以确定前端的循环瓶颈何时与特定的端口。

选择寻址模式和其他asm代码选择对于在SnB系列上实现最佳性能至关重要。但是用C语言写作让你无法控制;你主要受编译器的支配,除非你可以调整源代码以使其做出不同的选择。例如gcc vs. clang在这里有很大的不同。

在SnB系列上,movhps负载需要端口5用于shuffle / blend(虽然它可以微融合到一个uop中),但是movhps存储是一个没有ALU的纯存储UOP。所以它在那里收支平衡,让我们为两个数据元素使用一个SIMD加载/ XOR。

对于AVX,ALU uops允许未对齐的内存源操作数,因此我们不需要对data[]进行对齐。但英特尔HSW / SKL可以将索引寻址模式与pxor进行微融合,而不是vpxor。因此,在没有启用AVX的情况下编译可以更好,允许编译器使用索引寻址模式而不是递增单独的指针。 (或者如果编译器不知道这一点并且无论如何都使用索引寻址模式,它会更快。)TL:DR:可能最好需要16字节对齐data[]并使用AVX编译该函数禁用,以获得更好的宏观融合。 (但是我们错过了256位SIMD,最后组合了Out个切片,除非我们把它放在用AVX或AVX2编译的不同函数中)

避免未对齐的负载也将避免任何缓存线分割,这不会花费额外的uops,但我们可能接近于L1d吞吐量限制的瓶颈,而不仅仅是加载/存储执行单元吞吐量限制。

我还查看了一次加载4个索引并使用ALU指令解压缩。例如将memcpy加入struct { uint8_t idx[4]; } idx;。但gcc会生成多个浪费的指令,用于解压缩。太糟糕了,x86没有像ARM ubfx特别是 PowerPC rlwinm那样有很好的位域指令(可以让结果左移免费,所以如果x86有了这样,静态Out可以在非PIC代码中使用base + disp32寻址模式。)

如果我们使用标量XOR,则使用来自AL / AH的shift / movzx打开dword是一个胜利,但是当我们使用SIMD进行{{1}时,它看起来并非如此并且在data[]指令上花费前端吞吐量以允许存储地址uops在端口7上运行。这使得我们前端瓶颈而不是port2 / 3瓶颈,因此使用4x lea来自根据静态分析,内存看起来最好。如果你花时间手工编辑asm,那么值得对两种方式进行基准测试。 (带有额外uops的gcc生成的asm很糟糕,包括右移24后的完全冗余movzx,使高位已经为零。)

代码

(在the Godbolt compiler explorer上查看,以及标量版本):

movzx

使用gcc7.3 #include <immintrin.h> #include <stdint.h> #include <string.h> #include <stdalign.h> #ifdef IACA_MARKS #include "/opt/iaca-3.0/iacaMarks.h" #else #define IACA_START #define IACA_END #endif void hist_gatherscatter(unsigned idx0, unsigned idx1, uint64_t Out0[256], uint64_t Out1[256], __m128i vdata) { // gather load from Out[0][?] and Out[1][?] with movq / movhps __m128i hist = _mm_loadl_epi64((__m128i*)&Out0[idx0]); hist = _mm_castps_si128( // movhps into the high half _mm_loadh_pi(_mm_castsi128_ps(hist), (__m64*)&Out1[idx1])); // xorps could bottleneck on port5. // Actually probably not, using __m128 the whole time would be simpler and maybe not confuse clang hist = _mm_xor_si128(hist, vdata); // scatter store with movq / movhps _mm_storel_epi64((__m128i*)&Out0[idx0], hist); _mm_storeh_pi((__m64*)&Out1[idx1], _mm_castsi128_ps(hist)); } void ext(uint64_t*); void xor_histo_avx(uint8_t *Indices, const uint64_t *data, size_t len) { alignas(32) uint64_t Out[4][256] = {{0}}; // optional: peel the first iteration and optimize away loading the old known-zero values from Out[0..3][Indices[0..3]]. if (len<3) // not shown: cleanup for last up-to-3 elements. return; for (size_t i = 0 ; i<len ; i+=4) { IACA_START // attempt to hand-hold compiler into a dword load + shifts to extract indices // to reduce load-port pressure struct { uint8_t idx[4]; } idx; #if 0 memcpy(&idx, Indices+i, sizeof(idx)); // safe with strict-aliasing and possibly-unaligned //gcc makes stupid asm for this, same as for memcpy into a struct, // using a dword load into EAX (good), // then AL/AH for the first 2 (good) // but then redundant mov and movzx instructions for the high 2 // clang turns it into 4 loads /* //Attempt to hand-hold gcc into less-stupid asm //doesn't work: same asm as the struct uint32_t tmp; memcpy(&tmp, Indices+i, sizeof(tmp)); // mov eax,[mem] idx.idx[0] = tmp; //movzx reg, AL idx.idx[1] = tmp>>8; //movzx reg, AH tmp >>= 16; //shr eax, 16 idx.idx[2] = tmp; //movzx reg, AL idx.idx[3] = tmp>>8; //movzx reg, AH */ #else // compiles to separate loads with gcc and clang idx.idx[0] = Indices[i+0]; idx.idx[1] = Indices[i+1]; idx.idx[2] = Indices[i+2]; idx.idx[3] = Indices[i+3]; #endif __m128i vd = _mm_load_si128((const __m128i*)&data[i]); hist_gatherscatter(idx.idx[0], idx.idx[1], Out[0], Out[1], vd); vd = _mm_load_si128((const __m128i*)&data[i+2]); hist_gatherscatter(idx.idx[2], idx.idx[3], Out[2], Out[3], vd); } IACA_END // hand-hold compilers into a pointer-increment loop // to avoid indexed addressing modes. (4/5 speedup on HSW/SKL if all the stores use port7) __m256i *outp = (__m256i*)&Out[0]; __m256i *endp = (__m256i*)&Out[3][256]; for (; outp < endp ; outp++) { outp[0] ^= outp[256/4*1]; outp[0] ^= outp[256/4*2]; outp[0] ^= outp[256/4*3]; } // This part compiles horribly with -mno-avx, but does compile // because I used GNU C native vector operators on __m256i instead of intrinsics. /* for (int i=0 ; i<256 ; i+=4) { // use loadu / storeu if Out isn't aligned __m256i out0 = _mm256_load_si256(&Out[0][i]); __m256i out1 = _mm256_load_si256(&Out[1][i]); __m256i out2 = _mm256_load_si256(&Out[2][i]); __m256i out3 = _mm256_load_si256(&Out[3][i]); out0 = _mm256_xor_si256(out0, out1); out0 = _mm256_xor_si256(out0, out2); out0 = _mm256_xor_si256(out0, out3); _mm256_store_si256(&Out[0][i], out0); } */ //ext(Out[0]); // prevent optimizing away the work asm("" :: "r"(Out) : "memory"); } 编译,并使用IACA-3.0进行分析:

-std=gnu11 -DIACA_MARKS -O3 -march=skylake -mno-avx

关于Godbolt的gcc8.1使用$ /opt/iaca-3.0/iaca xor-histo.iaca.o Intel(R) Architecture Code Analyzer Version - v3.0-28-g1ba2cbb build date: 2017-10-23;16:42:45 Analyzed File - xor-histo.iaca.o Binary Format - 64Bit Architecture - SKL Analysis Type - Throughput Throughput Analysis Report -------------------------- Block Throughput: 5.79 Cycles Throughput Bottleneck: FrontEnd Loop Count: 22 (this is fused-domain uops. It's actually 20, so a 5 cycle front-end bottleneck) Port Binding In Cycles Per Iteration: -------------------------------------------------------------------------------------------------- | Port | 0 - DV | 1 | 2 - D | 3 - D | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------------------------- | Cycles | 2.0 0.0 | 3.0 | 5.5 5.1 | 5.5 4.9 | 4.0 | 3.0 | 2.0 | 3.0 | -------------------------------------------------------------------------------------------------- DV - Divider pipe (on port 0) D - Data fetch pipe (on ports 2 and 3) F - Macro Fusion with the previous instruction occurred * - instruction micro-ops not bound to a port ^ - Micro Fusion occurred # - ESP Tracking sync uop was issued @ - SSE instruction followed an AVX256/AVX512 instruction, dozens of cycles penalty is expected X - instruction not supported, was not accounted in Analysis | Num Of | Ports pressure in cycles | | | Uops | 0 - DV | 1 | 2 - D | 3 - D | 4 | 5 | 6 | 7 | ----------------------------------------------------------------------------------------- | 1 | | | 0.5 0.5 | 0.5 0.5 | | | | | movzx r8d, byte ptr [rdi] | 1 | | | 0.5 0.5 | 0.5 0.5 | | | | | movzx edx, byte ptr [rdi+0x2] | 1 | | | | | | | 1.0 | | add rdi, 0x4 | 1 | | | | | | | 1.0 | | add rsi, 0x20 | 1 | | | 0.5 0.5 | 0.5 0.5 | | | | | movzx eax, byte ptr [rdi-0x1] | 1 | | 1.0 | | | | | | | lea r12, ptr [rcx+r8*8] | 1 | | | 0.5 0.5 | 0.5 0.5 | | | | | movzx r8d, byte ptr [rdi-0x3] | 1 | | 1.0 | | | | | | | lea rdx, ptr [r10+rdx*8] | 1 | | | 0.5 0.5 | 0.5 0.5 | | | | | movq xmm0, qword ptr [r12] | 1 | | | | | | 1.0 | | | lea rax, ptr [r9+rax*8] | 1 | | 1.0 | | | | | | | lea r8, ptr [r11+r8*8] | 2 | | | 0.5 0.5 | 0.5 0.5 | | 1.0 | | | movhps xmm0, qword ptr [r8] # Wrong, 1 micro-fused uop on SKL | 2^ | 1.0 | | 0.5 0.5 | 0.5 0.5 | | | | | pxor xmm0, xmmword ptr [rsi-0x20] | 2^ | | | 0.5 | 0.5 | 1.0 | | | | movq qword ptr [r12], xmm0 # can run on port 7, IDK why IACA chooses not to model it there | 2^ | | | | | 1.0 | | | 1.0 | movhps qword ptr [r8], xmm0 | 1 | | | 0.5 0.5 | 0.5 0.5 | | | | | movq xmm0, qword ptr [rdx] | 2 | | | 0.5 0.5 | 0.5 0.5 | | 1.0 | | | movhps xmm0, qword ptr [rax] # Wrong, 1 micro-fused uop on SKL | 2^ | 1.0 | | 0.5 0.5 | 0.5 0.5 | | | | | pxor xmm0, xmmword ptr [rsi-0x10] | 2^ | | | | | 1.0 | | | 1.0 | movq qword ptr [rdx], xmm0 | 2^ | | | | | 1.0 | | | 1.0 | movhps qword ptr [rax], xmm0 | 1* | | | | | | | | | cmp rbx, rdi | 0*F | | | | | | | | | jnz 0xffffffffffffffa0 Total Num Of Uops: 29 (This is unfused-domain, and a weird thing to total up). 的缩放索引寻址模式,使用相同的指数计数器和pxor,以便保存data[]

clang没有使用LEA,并且每7个周期有4 add个瓶颈,因为没有任何商店uops可以在端口7上运行。

标量版(仍使用4片i):

Out[4][256]

该循环是4个融合域uop,比IACA计数的短,因为它不知道只有SnB / IvB非层压索引存储。 HSW / SKL没有。但是,这样的商店仍然不能使用端口7,因此对于4个元素来说,这不会比6.5个周期更好。

(顺便说一句,对于Indices [i]的朴素处理,用movzx分别加载每个,你得到4个元素的8个周期,饱和端口2和3.即使gcc没有生成吞吐量最优的代码对于解压缩结构,4字节的加载+解压缩应该是一个净赢,通过减轻一些负载端口的压力。)

清理循环

AVX2在这里非常闪亮:我们在直方图的最低切片上循环,在其他切片中循环。这个循环是8个前端uop,在Skylake上有4个负载,应该每2个时钟运行1个iter:

$ iaca.sh -mark 2 xor-histo.iaca.o                               
Intel(R) Architecture Code Analyzer Version - 2.3 build:246dfea (Thu, 6 Jul 2017 13:38:05 +0300)
Analyzed File - xor-histo.iaca.o
Binary Format - 64Bit
Architecture  - SKL
Analysis Type - Throughput

*******************************************************************
Intel(R) Architecture Code Analyzer Mark Number 2
*******************************************************************

Throughput Analysis Report
--------------------------
Block Throughput: 7.24 Cycles       Throughput Bottleneck: FrontEnd

Port Binding In Cycles Per Iteration:
---------------------------------------------------------------------------------------
|  Port  |  0   -  DV  |  1   |  2   -  D   |  3   -  D   |  4   |  5   |  6   |  7   |
---------------------------------------------------------------------------------------
| Cycles | 3.0    0.0  | 3.0  | 6.2    4.5  | 6.8    4.5  | 4.0  | 3.0  | 3.0  | 0.0  |
---------------------------------------------------------------------------------------

N - port number or number of cycles resource conflict caused delay, DV - Divider pipe (on port 0)
D - Data fetch pipe (on ports 2 and 3), CP - on a critical path
F - Macro Fusion with the previous instruction occurred
* - instruction micro-ops not bound to a port
^ - Micro Fusion happened
# - ESP Tracking sync uop was issued
@ - SSE instruction followed an AVX256/AVX512 instruction, dozens of cycles penalty is expected
X - instruction not supported, was not accounted in Analysis

| Num Of |                    Ports pressure in cycles                     |    |
|  Uops  |  0  - DV  |  1  |  2  -  D  |  3  -  D  |  4  |  5  |  6  |  7  |    |
---------------------------------------------------------------------------------
|   1    |           |     | 0.5   0.5 | 0.5   0.5 |     |     |     |     |    | mov eax, dword ptr [rdi]
|   1    | 0.4       | 0.5 |           |           |     | 0.1 |     |     |    | add rdi, 0x4
|   1    |           | 0.7 |           |           |     | 0.3 |     |     |    | add rsi, 0x20
|   1*   |           |     |           |           |     |     |     |     |    | movzx r9d, al
|   1    |           |     | 0.5   0.5 | 0.5   0.5 |     |     |     |     |    | mov rdx, qword ptr [rbp+r9*8-0x2040]
|   2^   |           | 0.3 | 0.5   0.5 | 0.5   0.5 |     | 0.3 | 0.4 |     |    | xor rdx, qword ptr [rsi-0x20]
|   2    |           |     | 0.5       | 0.5       | 1.0 |     |     |     |    | mov qword ptr [rbp+r9*8-0x2040], rdx  # wrong, HSW/SKL can keep indexed stores fused
|   1*   |           |     |           |           |     |     |     |     |    | movzx edx, ah
|   1    |           |     |           |           |     | 0.4 | 0.6 |     |    | add rdx, 0x100
|   1    |           |     | 0.5   0.5 | 0.5   0.5 |     |     |     |     |    | mov r9, qword ptr [rbp+rdx*8-0x2040]
|   2^   | 0.6       | 0.2 | 0.5   0.5 | 0.5   0.5 |     | 0.2 | 0.1 |     |    | xor r9, qword ptr [rsi-0x18]
|   2    |           |     | 0.2       | 0.8       | 1.0 |     |     |     |    | mov qword ptr [rbp+rdx*8-0x2040], r9  # wrong, HSW/SKL can keep indexed stores fused
|   1*   |           |     |           |           |     |     |     |     |    | mov edx, eax   # gcc code-gen isn't great, but not as bad as in the SIMD loop.  No extra movzx, but not taking advantage of AL/AH
|   1    | 0.4       |     |           |           |     |     | 0.6 |     |    | shr eax, 0x18
|   1    | 0.8       |     |           |           |     |     | 0.2 |     |    | shr edx, 0x10
|   1    |           | 0.6 |           |           |     | 0.3 |     |     |    | add rax, 0x300
|   1*   |           |     |           |           |     |     |     |     |    | movzx edx, dl
|   1    | 0.2       | 0.1 |           |           |     | 0.5 | 0.2 |     |    | add rdx, 0x200
|   1    |           |     | 0.5   0.5 | 0.5   0.5 |     |     |     |     |    | mov r9, qword ptr [rbp+rdx*8-0x2040]
|   2^   |           | 0.6 | 0.5   0.5 | 0.5   0.5 |     | 0.3 | 0.1 |     |    | xor r9, qword ptr [rsi-0x10]
|   2    |           |     | 0.5       | 0.5       | 1.0 |     |     |     |    | mov qword ptr [rbp+rdx*8-0x2040], r9  # wrong, HSW/SKL can keep indexed stores fused
|   1    |           |     | 0.5   0.5 | 0.5   0.5 |     |     |     |     |    | mov rdx, qword ptr [rbp+rax*8-0x2040]
|   2^   |           |     | 0.5   0.5 | 0.5   0.5 |     | 0.6 | 0.4 |     |    | xor rdx, qword ptr [rsi-0x8]
|   2    |           |     | 0.5       | 0.5       | 1.0 |     |     |     |    | mov qword ptr [rbp+rax*8-0x2040], rdx  # wrong, HSW/SKL can keep indexed stores fused
|   1    | 0.6       |     |           |           |     |     | 0.4 |     |    | cmp r8, rdi
|   0F   |           |     |           |           |     |     |     |     |    | jnz 0xffffffffffffff75
Total Num Of Uops: 33

我尝试通过在一个链中执行XOR来进一步减少uop计数,但是gcc坚持做两个.L7: vmovdqa ymm2, YMMWORD PTR [rax+4096] vpxor ymm0, ymm2, YMMWORD PTR [rax+6144] vmovdqa ymm3, YMMWORD PTR [rax] vpxor ymm1, ymm3, YMMWORD PTR [rax+2048] vpxor ymm0, ymm0, ymm1 vmovdqa YMMWORD PTR [rax], ymm0 add rax, 32 cmp rax, rdx jne .L7 加载并且必须在没有内存操作数的情况下执行一个vmovdqa。 (OoO exec将隐藏VPXOR这个微小链/树的延迟,所以它并不重要。)

  

我如何使用AVX-512的散射?是否存在一些散射指令xors而不是覆盖?

不,您使用聚集来获取旧值,然后SIMD XOR,然后将更新的元素分散回它们来自的位置。

为避免冲突,您可能需要vpxor,因此每个向量元素都可以使用不同的表。 (否则,如果out[8][256]Indices[i+0]相等,则会出现问题,因为分散存储将只存储具有该索引的最高向量元素。

分散/收集指令需要一个基本寄存器,但您可以在执行Indices[i+4]零扩展加载后简单地添加_mm256_setr_epi64(0, 256, 256*2, ...);

备注

我使用IACA2.3进行标量分析,因为IACA3.0似乎删除了vpmovzxbq选项,以便在一个文件中有多个标记时选择要分析的循环。在这种情况下,IACA3.0没有解决IACA2.3对SKL管道错误的任何方法。

答案 1 :(得分:0)

您可以根据索引[i]对数据进行排序......这应该采用O(N * log2(N)),但可以并行化。

然后获取已排序数据的累积xor - 也可以并行化。

然后是计算Out[i] = CumXor(j) ^ Out[i-1];

的问题