就这样,我们可以几乎最快速的获得16个32位整数的序列。
那么如何获得更多整数的序列呢?
一条SIMD指令最多能检查一个512位寄存器中的16个32位整数(也可以是8个64位整数或者32个16位整数,64个8位整数等等)获得这些整数中的最大或者最小值;那么如果要检测多于16个整数的情况,该怎么处理?比如要检测256个整数中的最大和最小值,该怎么做?
很容易想到的是,16个整数一组,有一个最大和最小值(也可能有多个),那么256个整数就可以分成16组,每一组的最大最小值,先记录下来,然后这16组的最大最小值再比较,从中获取最大和最小值,那就是这256个整数的最大和最小值。这个想法显然也是很朴素的。
具体实现的话,有如下代码,
void seive_get_min_max(
int use_mask,
uint32_t& p_min,
uint32_t& p_max,
uint32_t& _min_count,
uint32_t& _max_count,
__mmask16& _all_masks,
__mmask16 masks[16],
__m512i values[16]) {
_min_count = 0;
_max_count = 0;
if (_all_masks == 0) return;
uint32_t _mines[16] = { 0 };
__mmask16 mask_mines[16] = { 0 };
uint32_t _maxes[16] = { 0 };
__mmask16 mask_maxes[16] = { 0 };
for (int i = 0; i < 16; i++) {
if ((_all_masks & (1 << i)) != 0) {
if (sieve_get_min_max(
masks[i],
values[i],
_mines[i], _maxes[i], mask_mines[i], mask_maxes[i]))
{
//OK
}
}
}
__mmask16 found_mask = 0;
if ((use_mask & 1) != 0 && sieve_get_min(_all_masks, _mm512_loadu_epi32(_mines), p_min, found_mask)) {
__m256i __mask_mines = _mm256_loadu_epi16(mask_mines);
_min_count = _mm512_reduce_add_epu16(_mm512_castsi256_si512(
_mm256_maskz_popcnt_epi16(found_mask, __mask_mines)));
__m512i _mask_mines = _mm512_cvtepu16_epi32(__mask_mines);
__m512i _masks = _mm512_cvtepu16_epi32(_mm256_loadu_epi16(masks));
_masks = _mm512_mask_andnot_epi32(_masks, found_mask, _mask_mines, _masks);
_all_masks &= ~_mm512_cmpeq_epu32_mask(
_masks, zero);
_mm256_storeu_epi16(masks, _mm512_cvtepi32_epi16(_masks));
}
if ((use_mask & 2) != 0 && sieve_get_max(_all_masks, _mm512_loadu_epi32(_maxes), p_max, found_mask)) {
__m256i __mask_maxes = _mm256_loadu_epi16(mask_maxes);
_max_count = _mm512_reduce_add_epu16(_mm512_castsi256_si512(
_mm256_maskz_popcnt_epi16(found_mask, __mask_maxes)));
__m512i _mask_maxes = _mm512_cvtepu16_epi32(__mask_maxes);
__m512i _masks = _mm512_cvtepu16_epi32(_mm256_loadu_epi16(masks));
_masks = _mm512_mask_andnot_epi32(_masks, found_mask, _mask_maxes, _masks);
_all_masks &= ~_mm512_cmpeq_epu32_mask(
_masks, zero);
_mm256_storeu_epi16(masks, _mm512_cvtepi32_epi16(_masks));
}
}
代码并不清晰,因为它是高度优化的。循环的部分获取了16对最大最小值,以及对应的掩码。后面两个部分(use_mask&1,use_mask&2)处理如下问题:获得了整体的最大值和最小值之后,要把那些具有最大值和最小值的掩码进一步掩掉。如果某一个16个整数的段都已经检测过,则把对应的整体掩码掩掉(对应的位置0)。
这就使得每一次调用都能获得256中的最大和最小值,当然也同时报出了最大最小值出现的次数。
最后,只需要把最大和最小值一一求出,然后在结果中首尾各放一个,就获得了256个整数排序的能力。
void sieve_sort_256_dual(uint32_t a[_256], uint32_t* result = nullptr) {
__m512i values[16];
for (size_t i = 0; i < 16; i++) {
values[i] = _mm512_loadu_epi32(a + (i << 4));
}
__mmask16 masks[16];
memset(masks, 0xff, sizeof(masks));
result = result == nullptr ? a : result;
__mmask16 all_masks = 0xffff;
uint32_t _min = 0, _max = 0;
uint32_t _min_count = 0, _max_count = 0;
int i = 0, j = 255;
while (i <= j) {
seive_get_min_max(
3,
_min, _max,
_min_count, _max_count,
all_masks, masks, values);
for (size_t t = 0; t < _min_count; t++) {
result[i++] = _min;
}
for (size_t t = 0; t < _max_count; t++) {
result[j--] = _max;
}
}
}
这里的里外两层循环都没有做优化。因为一个数出现16次以上的概率并不大。
上面的代码没有处理mask的部分,因为相应部分已经在seive_get_min_max处理好了。
此时,我们就有了处理256个整数的排序的能力了。
顺便说一句,如果要处理128个整数呢 ?目前我们还没有涉及到倍增的处理方法,所以可以想先用填充法来实现。比如先开辟一块内存,复制这128个整数到里面,然后在后面填充整数的最大值。计算完成之后,再截取前128个作为结果。这一点无需证明。因为最大值都是一样的。即便这128个里面含有最大值(0xffffffff)它也只是在排序过程中被排到后面去了。在尾部填0也行,只是需要在获得结果的时候,倒着数128个做为结果的开始即可。0和0xffffffff之外的别的数就不要添加了。