C++ 使用AVX对64位结构进行排序?

C++ 使用AVX对64位结构进行排序?,c++,intrinsics,avx,C++,Intrinsics,Avx,我有一个64位结构,它表示多个数据段,其中一个是浮点值: struct MyStruct{ uint16_t a; uint16_t b; float f; }; 我有四个这样的结构,比如说一个std::array 是否可以使用AVX按照float成员对数组进行排序?对不起,这个答案很混乱;它不是一下子全部写下来的,我很懒。有些重复 我有4个不同的想法: 正常排序,但将结构作为64位单元移动 矢量化插入排序作为qsort的构建块 排序网络,使用比较器实现,使用CMPP

我有一个64位结构,它表示多个数据段,其中一个是浮点值:

struct MyStruct{
    uint16_t a;
    uint16_t b;
    float f;
}; 
我有四个这样的结构,比如说一个
std::array


是否可以使用AVX按照float成员对数组进行排序?

对不起,这个答案很混乱;它不是一下子全部写下来的,我很懒。有些重复

我有4个不同的想法:

  • 正常排序,但将结构作为64位单元移动
  • 矢量化插入排序作为qsort的构建块
  • 排序网络,使用比较器实现,使用
    CMPP
    /
    blendvpd
    而不是
    minps
    /
    maxps
    。不过,额外的开销可能会扼杀加速

  • 排序网络:加载一些结构,然后洗牌/混合以获得一些刚刚浮动的寄存器和一些刚刚有效负载的寄存器。使用Timothy Furtak的技术,对有效负载执行正常的
    minps
    /
    maxps
    比较器,然后执行
    cmpeqps min,orig
    ->屏蔽异或交换。这会对每个比较器的数据进行两倍的排序,但需要在比较器之间的两个寄存器上进行匹配洗牌。完成后还需要重新交错(但对于unpcklps/unpckhps,这很容易,如果您安排比较器,以便通道解包中的比较器将最终数据按正确顺序排列)

    这还避免了某些CPU在对表示非规范、非规范或无穷大的有效负载中的位模式进行FP比较时可能出现的潜在减速,而无需在MXCSR中将非规范设置为零位

    Furtak的论文建议在基本上使用向量进行排序后进行标量清理,这将大大减少洗牌的数量

  • 正常分拣 使用普通排序算法时,通过使用64位加载/存储移动整个结构,并在FP元素上进行标量FP比较,至少可以获得一个小的加速。为了让这个想法尽可能好地发挥作用,首先用浮点值对结构进行排序,然后可以
    movq
    将整个结构转换成xmm reg
    ,并且
    ucomiss
    的浮点值将处于低位32。然后,您(或者可能是一个智能编译器)可以使用
    movq
    存储结构

    查看Kerrek SB链接到的asm输出,编译器似乎在高效复制结构方面做得相当糟糕:

    icc
    似乎将两个uint值分别移动,而不是在64b的负载中占用整个结构。也许它没有打包结构
    gcc
    5.1在大多数情况下似乎没有这个问题

    加速插入排序 对于足够小的问题,大排序通常使用插入排序进行分治。一个一个地复制数组元素,只有当我们找到当前元素所属的位置时才会停止。因此,我们需要将一个元素与一系列压缩元素进行比较,如果对任何元素的比较为真,则停止。你闻到向量了吗?我闻到了向量

    # RSI points to  struct { float f; uint... payload; } buf[];
    # RDI points to the next element to be inserted into the sorted portion
    # [ rsi to rdi ) is sorted, the rest isn't.
    ##### PROOF OF CONCEPT: debug / finish writing before using!  ######
    
    .new_elem:
    vbroadcastsd ymm0, [rdi]      # broadcast the whole struct
    mov rdx, rdi
    
    .search_loop:
        sub        rdx, 32
        vmovups    ymm1, [rdx]    # load some sorted data
        vcmplt_oqps ymm2, ymm0, ymm1   # all-ones in any element where ymm0[i] < ymm1[i] (FP compare, false if either is NaN).
        vmovups    [rdx+8], ymm1  # shuffle it over to make space, usual insertion-sort style
        cmp        rdx, rsi
        jbe     .endsearch        # below-or-equal (addresses are unsigned)
        movmskps   eax, ymm2
        test       al, 0b01010101 # test only the compare results for 
    
        jz      .search_loop      # [rdi] wasn't less than any of the 4 elements
    
    .endsearch:
    # TODO: scalar loop to find out where the new element goes.
    #  All we know is that it's less than one of the elements in ymm1, but not which
    add           rdi, 8
    vmovsd         [rdx], ymm0
    cmp           rdi, r8   # pointer to the end of the buf
    jle           .new_elem
    
      # worse alternative to movmskps / test:
      # vtestps    ymm2, ymm7     # where ymm7 is loaded with 1s in the odd (float) elements, and 0s in the even (payload) elements.
      # vtestps is like PTEST, but only tests the high bit.  If the struct was in the other order, with the float high, vtestpd against a register of all-1s would work, as that's more convenient to generate.
    
    您可能需要设置MXCSR,以确保int位在恰好表示非规范或NaN浮点时不会减慢FP运算。我不确定这种情况是否只发生在mul/div上,或者是否会影响比较

    • 英特尔Haswell:延迟:准备就绪的
      ymm2需要5个周期,准备就绪的
      ymm3需要7个周期。吞吐量:每4个周期一次。(p5瓶颈)
    • 英特尔Sandybridge/Ivybridge:延迟:准备就绪的
      ymm2需要5个周期,准备就绪的
      ymm3需要6个周期。吞吐量:每2个周期一次。(p0/p5瓶颈)
    • AMD推土机/打桩机:(
      vblendvpd ymm
      :2c横向,2c横向):横向:4c表示
      ymm2
      ,6c表示
      ymm3
      。更糟糕的是,CMPP和blend之间存在旁路延迟。t输出:每4摄氏度一个。(向量P1上的瓶颈)
    • AMD蒸汽压路机:(
      vblendvpd ymm
      :2c车床,1c往复式输出):车床:4c表示
      ymm2
      ,5c表示
      ymm3
      。或者可能因为旁路延迟而增加1。t输出:每3c一个(向量端口P0/1上的瓶颈,用于cmp和混合)
    VBLENDVPD
    为2个计量单位。(它有3个reg输入,因此不能是1个uop:/)。两个UOP只能在随机端口上运行。在哈斯韦尔,那只是5号门。在瑞士央行,这是p0/p5。(与SnB/IvB相比,IDK why Haswell将混洗/混合处理量减半。)

    如果AMD的设计有256b宽的矢量单元,他们的低延迟FP比较和3输入指令的单宏运算解码将使他们领先

    通常的minps/maxps对是3和4个周期的延迟(
    ymm2/3
    ),以及每2个周期一个吞吐量(Intel)。(FP添加/子/比较单元上的p1瓶颈)。最公平的比较可能是64位双倍排序。如果没有多对独立寄存器进行比较,那么额外的延迟可能会受到影响。Haswell减半的吞吐量将大大降低任何加速

    还请记住,在比较器操作之间需要进行洗牌,以获得用于比较的正确元素。min/maxps不使用洗牌端口,但我的cmpps/blendv版本使它们饱和,这意味着洗牌不能与比较重叠,除非作为填补数据依赖性留下的空白的东西

    对于超线程,另一个可以让其他端口保持繁忙的线程(例如端口0/1 fp mul/add units或整数代码)可以很好地与这个混合瓶颈版本共享一个内核

    我尝试了Haswell的另一个版本,它使用按位和/或操作“手动”混合。不过,它的速度要慢一些,因为在合并之前,两个源都必须双向屏蔽

    # AVX2 comparator for Haswell
    # struct { float f; uint16_t a, b; }  inputs in ymm0, ymm1
    #
    vcmpps ymm7, ymm0, ymm1, _CMP_LT_OQ  # imm8=17: less-than, ordered, quiet (non-signalling on NaN)
         # ymm7 32bit elements = 0xFFFFFFFF if ymm0[i] < ymm1[i], else 0
    vshufps ymm7, ymm7, ymm7, mask(0, 0, 2, 2)  # extend the mask to the payload part.  There's no mask function, I just don't want to work out the result in my head.
    vpand    ymm10, ymm7, ymm0       # ymm10 = ymm0 keeping elements where ymm0[i] < ymm1[i]
    vpandn   ymm11, ymm7, ymm1       # ymm11 = ymm1 keeping elements where !(ymm0[i] < ymm1[i])
    vpor     ymm2, ymm10, ymm11      # ymm2 = min_packed_mystruct(ymm0, ymm1)
    
    vpandn   ymm10, ymm7, ymm0       # ymm10 = ymm0 keeping elements where !(ymm0[i] < ymm1[i])
    vpand    ymm11, ymm7, ymm1       # ymm11 = ymm1 keeping elements where ymm0[i] < ymm1[i]
    vpor     ymm3, ymm10, ymm11  # ymm2 = max_packed_mystruct(ymm0, ymm1)
    
    # result: !(ymm2[i] > ymm3[i])
    #  UNTESTED
    
    Haswell的AVX2比较器 #ymm0,ymm1中的结构{float f;uint16_t a,b;}输入 # vcmpps ymm7,ymm0,ymm1,_CMP_LT_OQ#im
    # AVX2 comparator for Haswell
    # struct { float f; uint16_t a, b; }  inputs in ymm0, ymm1
    #
    vcmpps ymm7, ymm0, ymm1, _CMP_LT_OQ  # imm8=17: less-than, ordered, quiet (non-signalling on NaN)
         # ymm7 32bit elements = 0xFFFFFFFF if ymm0[i] < ymm1[i], else 0
    vshufps ymm7, ymm7, ymm7, mask(0, 0, 2, 2)  # extend the mask to the payload part.  There's no mask function, I just don't want to work out the result in my head.
    vpand    ymm10, ymm7, ymm0       # ymm10 = ymm0 keeping elements where ymm0[i] < ymm1[i]
    vpandn   ymm11, ymm7, ymm1       # ymm11 = ymm1 keeping elements where !(ymm0[i] < ymm1[i])
    vpor     ymm2, ymm10, ymm11      # ymm2 = min_packed_mystruct(ymm0, ymm1)
    
    vpandn   ymm10, ymm7, ymm0       # ymm10 = ymm0 keeping elements where !(ymm0[i] < ymm1[i])
    vpand    ymm11, ymm7, ymm1       # ymm11 = ymm1 keeping elements where ymm0[i] < ymm1[i]
    vpor     ymm3, ymm10, ymm11  # ymm2 = max_packed_mystruct(ymm0, ymm1)
    
    # result: !(ymm2[i] > ymm3[i])
    #  UNTESTED
    
    __m256i lut[4096];    //LUT of 128Kb size must be precomputed
    __m256 Sort4(__m256 val) {
        __m256 aaabbcaa = _mm256_permutevar8x32_ps(val, _mm256_setr_epi32(0, 0, 0, 2, 2, 4, 0, 0));
        __m256 bcdcddaa = _mm256_permutevar8x32_ps(val, _mm256_setr_epi32(2, 4, 6, 4, 6, 6, 0, 0));
        __m256 cmpLt = _mm256_cmp_ps(aaabbcaa, bcdcddaa, _CMP_LT_OQ);
        __m256 cmpGt = _mm256_cmp_ps(aaabbcaa, bcdcddaa, _CMP_GT_OQ);
        int idxLt = _mm256_movemask_ps(cmpLt);
        int idxGt = _mm256_movemask_ps(cmpGt);
        __m256i shuf = lut[idxGt * 64 + idxLt];
        __m256 res = _mm256_permutevar8x32_ps(val, shuf);
        return res;
    }