C++ 使用AVX对64位结构进行排序?
我有一个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
struct MyStruct{
uint16_t a;
uint16_t b;
float f;
};
我有四个这样的结构,比如说一个std::array
是否可以使用AVX按照float成员对数组进行排序?对不起,这个答案很混乱;它不是一下子全部写下来的,我很懒。有些重复 我有4个不同的想法:
CMPP
/blendvpd
而不是minps
/maxps
。不过,额外的开销可能会扼杀加速minps
/maxps
比较器,然后执行cmpeqps min,orig
->屏蔽异或交换。这会对每个比较器的数据进行两倍的排序,但需要在比较器之间的两个寄存器上进行匹配洗牌。完成后还需要重新交错(但对于unpcklps/unpckhps,这很容易,如果您安排比较器,以便通道解包中的比较器将最终数据按正确顺序排列)
这还避免了某些CPU在对表示非规范、非规范或无穷大的有效负载中的位模式进行FP比较时可能出现的潜在减速,而无需在MXCSR中将非规范设置为零位
Furtak的论文建议在基本上使用向量进行排序后进行标量清理,这将大大减少洗牌的数量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推土机/打桩机:(
:2c横向,2c横向):横向:4c表示vblendvpd ymm
,6c表示ymm2
。更糟糕的是,CMPP和blend之间存在旁路延迟。t输出:每4摄氏度一个。(向量P1上的瓶颈)ymm3
- AMD蒸汽压路机:(
:2c车床,1c往复式输出):车床:4c表示vblendvpd ymm
,5c表示ymm2
。或者可能因为旁路延迟而增加1。t输出:每3c一个(向量端口P0/1上的瓶颈,用于cmp和混合)ymm3
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;
}