Skip to content

Commit 7109637

Browse files
committed
Updated prod implementation with proper AVX/no-BMI2 fallbacks
1 parent f001ff5 commit 7109637

6 files changed

Lines changed: 853 additions & 107 deletions

File tree

CMakeLists.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ if(DISABLE_AVX512)
1515
message(STATUS "DISABLE_AVX512 is ON")
1616
endif()
1717

18+
option(PIXIE_DISABLE_BMI2 "Disable BMI2 instructions" OFF)
19+
if(PIXIE_DISABLE_BMI2)
20+
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mno-bmi2")
21+
add_compile_definitions(PIXIE_DISABLE_BMI2)
22+
message(STATUS "PIXIE_DISABLE_BMI2 is ON")
23+
endif()
24+
1825
option(ENABLE_ADDRESS_SANITIZER "Enable AddressSanitizer" OFF)
1926
if(ENABLE_ADDRESS_SANITIZER)
2027
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer")

include/pixie/bits.h

Lines changed: 181 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ static inline const __m256i mask_first_half = _mm256_setr_epi8(
4545
// clang-format on
4646
#endif
4747

48+
#if defined(__BMI2__) && !defined(PIXIE_DISABLE_BMI2)
49+
#define PIXIE_BMI2_SUPPORT
50+
#endif
51+
4852
/**
4953
* @brief Test 16 int16 RmM btree child ranges for a node-local target.
5054
* @details Each lane represents one child summary. The function checks whether
@@ -235,37 +239,144 @@ static inline uint64_t rank_512(const uint64_t* x, uint64_t count) {
235239
#endif
236240
}
237241

242+
#ifndef PIXIE_BMI2_SUPPORT
243+
struct PixieSelectByteLut {
244+
uint8_t popcounts[256];
245+
uint8_t select[256][8];
246+
247+
constexpr PixieSelectByteLut() : popcounts{}, select{} {
248+
for (int byte = 0; byte < 256; ++byte) {
249+
for (int rank = 0; rank < 8; ++rank) {
250+
select[byte][rank] = 8;
251+
}
252+
253+
int count = 0;
254+
for (int bit = 0; bit < 8; ++bit) {
255+
if (((byte >> bit) & 1) != 0) {
256+
select[byte][count++] = static_cast<uint8_t>(bit);
257+
}
258+
}
259+
popcounts[byte] = static_cast<uint8_t>(count);
260+
}
261+
}
262+
};
263+
264+
static inline constexpr PixieSelectByteLut pixie_select_byte_lut;
265+
266+
static inline uint64_t select_64_no_bmi2(uint64_t x, uint64_t rank) {
267+
uint64_t offset = 0;
268+
269+
uint64_t count = std::popcount(static_cast<uint32_t>(x));
270+
if (rank >= count) {
271+
rank -= count;
272+
x >>= 32;
273+
offset += 32;
274+
}
275+
276+
count = std::popcount(static_cast<uint16_t>(x));
277+
if (rank >= count) {
278+
rank -= count;
279+
x >>= 16;
280+
offset += 16;
281+
}
282+
283+
const auto low_byte = static_cast<uint8_t>(x);
284+
count = pixie_select_byte_lut.popcounts[low_byte];
285+
if (rank >= count) {
286+
rank -= count;
287+
x >>= 8;
288+
offset += 8;
289+
}
290+
291+
return offset + pixie_select_byte_lut.select[static_cast<uint8_t>(x)][rank];
292+
}
293+
#endif
294+
238295
/**
239296
* @brief Return position of @p rank 1 bit in @p x
240297
*/
241298
static inline uint64_t select_64(uint64_t x, uint64_t rank) {
242-
return _tzcnt_u64(_pdep_u64(1ull << rank, x));
299+
#ifdef PIXIE_BMI2_SUPPORT
300+
return std::countr_zero(_pdep_u64(1ull << rank, x));
301+
#else
302+
return select_64_no_bmi2(x, rank);
303+
#endif
243304
}
244305

245-
/**
246-
* @brief Return position of @p rank 1 bit in @p x
247-
* @details Selecting within 64-bit word can be done
248-
* using combination of _tzcnt_u64(_pdep_u64(1 << rank, x))
249-
* See Pandey P., Bender M. A., Johnson R. A fast x86 implementation of select
250-
* https://arxiv.org/abs/1706.00990
251-
*
252-
* To find a 64-bit word inside a 512-bit region we use
253-
* We first popcounts of all 8 64-bit words with _mm512_popcnt_epi64
254-
* and then perform a linear scan.
255-
*
256-
* Notably a SWAR algorithm for parallel binary search
257-
* http://www-graphics.stanford.edu/~seander/bithacks.html#SelectPosFromMSBRank
258-
* might be used as a backoff algorithm for selecting in a 64-bit word.
259-
* It can also be used as an alternative for linear search but i don't
260-
* see a proper SIMD algorithm to make it faster.
261-
*/
262-
static inline uint64_t select_512(const uint64_t* x, uint64_t rank) {
263-
#ifdef PIXIE_AVX512_SUPPORT
306+
template <bool Invert>
307+
static inline uint64_t select_512_word_count(uint64_t word) {
308+
if constexpr (Invert) {
309+
return std::popcount(~word);
310+
} else {
311+
return std::popcount(word);
312+
}
313+
}
264314

265-
__m512i res = _mm512_loadu_epi64(x);
266-
__m512i counts = _mm512_popcnt_epi64(res);
267-
__m512i prefix = counts;
315+
template <bool Invert>
316+
static inline uint64_t select_512_selected_word(uint64_t word) {
317+
if constexpr (Invert) {
318+
return ~word;
319+
} else {
320+
return word;
321+
}
322+
}
323+
324+
template <bool Invert>
325+
static inline uint64_t select_512_scalar_impl(const uint64_t* x,
326+
uint64_t rank) {
327+
for (size_t i = 0; i < 8; ++i) {
328+
const uint64_t count = select_512_word_count<Invert>(x[i]);
329+
if (rank < count) {
330+
return i * 64 + select_64(select_512_selected_word<Invert>(x[i]), rank);
331+
}
332+
rank -= count;
333+
}
334+
return 512;
335+
}
268336

337+
#ifdef PIXIE_AVX2_SUPPORT
338+
template <bool Invert>
339+
static inline void select_512_avx2_counts(const uint64_t* x, uint64_t* counts) {
340+
const __m256i low_mask = _mm256_set1_epi8(0x0F);
341+
const __m256i zero = _mm256_setzero_si256();
342+
const __m256i sixty_four = _mm256_set1_epi64x(64);
343+
344+
for (int half = 0; half < 2; ++half) {
345+
const __m256i words =
346+
_mm256_loadu_si256(reinterpret_cast<const __m256i*>(x + 4 * half));
347+
348+
const __m256i low_nibbles = _mm256_and_si256(words, low_mask);
349+
const __m256i high_nibbles =
350+
_mm256_and_si256(_mm256_srli_epi16(words, 4), low_mask);
351+
const __m256i byte_counts =
352+
_mm256_add_epi8(_mm256_shuffle_epi8(lookup_popcount_4, low_nibbles),
353+
_mm256_shuffle_epi8(lookup_popcount_4, high_nibbles));
354+
__m256i word_counts = _mm256_sad_epu8(byte_counts, zero);
355+
if constexpr (Invert) {
356+
word_counts = _mm256_sub_epi64(sixty_four, word_counts);
357+
}
358+
_mm256_store_si256(reinterpret_cast<__m256i*>(counts + 4 * half),
359+
word_counts);
360+
}
361+
}
362+
363+
template <bool Invert>
364+
static inline uint64_t select_512_avx2_impl(const uint64_t* x, uint64_t rank) {
365+
alignas(32) uint64_t counts[8];
366+
select_512_avx2_counts<Invert>(x, counts);
367+
368+
for (size_t i = 0; i < 8; ++i) {
369+
if (rank < counts[i]) {
370+
return i * 64 + select_64(select_512_selected_word<Invert>(x[i]), rank);
371+
}
372+
rank -= counts[i];
373+
}
374+
return 512;
375+
}
376+
#endif
377+
378+
#ifdef PIXIE_AVX512_SUPPORT
379+
static inline __m512i select_512_avx512_prefix_sum_u64(__m512i prefix) {
269380
const __m512i idx_shift1 = _mm512_set_epi64(6, 5, 4, 3, 2, 1, 0, 0);
270381
const __m512i idx_shift2 = _mm512_set_epi64(5, 4, 3, 2, 1, 0, 0, 0);
271382
const __m512i idx_shift4 = _mm512_set_epi64(3, 2, 1, 0, 0, 0, 0, 0);
@@ -275,76 +386,67 @@ static inline uint64_t select_512(const uint64_t* x, uint64_t rank) {
275386
tmp = _mm512_maskz_permutexvar_epi64(0xFC, idx_shift2, prefix);
276387
prefix = _mm512_add_epi64(prefix, tmp);
277388
tmp = _mm512_maskz_permutexvar_epi64(0xF0, idx_shift4, prefix);
278-
prefix = _mm512_add_epi64(prefix, tmp);
389+
return _mm512_add_epi64(prefix, tmp);
390+
}
279391

280-
__mmask8 mask = _mm512_cmpgt_epu64_mask(prefix, _mm512_set1_epi64(rank));
281-
uint32_t i = _tzcnt_u32(static_cast<uint32_t>(mask));
282-
uint64_t prev = 0;
283-
if (i != 0) {
284-
__m512i idx_prev = _mm512_set1_epi64(static_cast<int64_t>(i - 1));
285-
__m512i prev_vec = _mm512_permutexvar_epi64(idx_prev, prefix);
286-
prev = static_cast<uint64_t>(
287-
_mm_cvtsi128_si64(_mm512_castsi512_si128(prev_vec)));
392+
static inline uint64_t select_512_avx512_previous_prefix(__m512i prefix,
393+
uint32_t lane) {
394+
if (lane == 0) {
395+
return 0;
288396
}
289-
return i * 64 + select_64(x[i], rank - prev);
290-
291-
#else
397+
const __m512i idx_previous =
398+
_mm512_set1_epi64(static_cast<int64_t>(lane - 1));
399+
const __m512i previous_vec = _mm512_permutexvar_epi64(idx_previous, prefix);
400+
return static_cast<uint64_t>(
401+
_mm_cvtsi128_si64(_mm512_castsi512_si128(previous_vec)));
402+
}
292403

293-
size_t i = 0;
294-
int popcount = std::popcount(x[0]);
295-
while (i < 7 && popcount <= rank) {
296-
rank -= popcount;
297-
popcount = std::popcount(x[++i]);
404+
template <bool Invert>
405+
static inline uint64_t select_512_avx512_impl(const uint64_t* x,
406+
uint64_t rank) {
407+
const __m512i words = _mm512_loadu_epi64(x);
408+
__m512i prefix = _mm512_popcnt_epi64(words);
409+
if constexpr (Invert) {
410+
prefix = _mm512_sub_epi64(_mm512_set1_epi64(64), prefix);
298411
}
299-
return i * 64 + select_64(x[i], rank);
412+
prefix = select_512_avx512_prefix_sum_u64(prefix);
413+
414+
const __mmask8 mask = _mm512_cmpgt_epu64_mask(
415+
prefix, _mm512_set1_epi64(static_cast<int64_t>(rank)));
416+
const uint32_t lane = std::countr_zero(static_cast<uint32_t>(mask));
417+
const uint64_t previous = select_512_avx512_previous_prefix(prefix, lane);
418+
return lane * 64 +
419+
select_64(select_512_selected_word<Invert>(x[lane]), rank - previous);
420+
}
421+
#endif
300422

423+
/**
424+
* @brief Return position of @p rank 1 bit in @p x.
425+
* @details Uses AVX-512, then AVX2, then scalar fallback. The 64-bit in-word
426+
* select uses BMI2 PDEP unless PIXIE_DISABLE_BMI2 is defined or BMI2 is not
427+
* available.
428+
*/
429+
static inline uint64_t select_512(const uint64_t* x, uint64_t rank) {
430+
#ifdef PIXIE_AVX512_SUPPORT
431+
return select_512_avx512_impl<false>(x, rank);
432+
#elif defined(PIXIE_AVX2_SUPPORT)
433+
return select_512_avx2_impl<false>(x, rank);
434+
#else
435+
return select_512_scalar_impl<false>(x, rank);
301436
#endif
302437
}
303438

304439
/**
305-
* @brief Return position of @p rank0 0 bit in @p x
440+
* @brief Return position of @p rank0 0 bit in @p x.
306441
* @details select_512 with bit inversion.
307442
*/
308443
static inline uint64_t select0_512(const uint64_t* x, uint64_t rank0) {
309444
#ifdef PIXIE_AVX512_SUPPORT
310-
311-
__m512i res = _mm512_loadu_epi64(x);
312-
__m512i counts =
313-
_mm512_sub_epi64(_mm512_set1_epi64(64), _mm512_popcnt_epi64(res));
314-
__m512i prefix = counts;
315-
316-
const __m512i idx_shift1 = _mm512_set_epi64(6, 5, 4, 3, 2, 1, 0, 0);
317-
const __m512i idx_shift2 = _mm512_set_epi64(5, 4, 3, 2, 1, 0, 0, 0);
318-
const __m512i idx_shift4 = _mm512_set_epi64(3, 2, 1, 0, 0, 0, 0, 0);
319-
320-
__m512i tmp = _mm512_maskz_permutexvar_epi64(0xFE, idx_shift1, prefix);
321-
prefix = _mm512_add_epi64(prefix, tmp);
322-
tmp = _mm512_maskz_permutexvar_epi64(0xFC, idx_shift2, prefix);
323-
prefix = _mm512_add_epi64(prefix, tmp);
324-
tmp = _mm512_maskz_permutexvar_epi64(0xF0, idx_shift4, prefix);
325-
prefix = _mm512_add_epi64(prefix, tmp);
326-
327-
__mmask8 mask = _mm512_cmpgt_epu64_mask(prefix, _mm512_set1_epi64(rank0));
328-
uint32_t i = _tzcnt_u32(static_cast<uint32_t>(mask));
329-
uint64_t prev = 0;
330-
if (i != 0) {
331-
__m512i idx_prev = _mm512_set1_epi64(static_cast<int64_t>(i - 1));
332-
__m512i prev_vec = _mm512_permutexvar_epi64(idx_prev, prefix);
333-
prev = static_cast<uint64_t>(
334-
_mm_cvtsi128_si64(_mm512_castsi512_si128(prev_vec)));
335-
}
336-
return i * 64 + select_64(~x[i], rank0 - prev);
337-
445+
return select_512_avx512_impl<true>(x, rank0);
446+
#elif defined(PIXIE_AVX2_SUPPORT)
447+
return select_512_avx2_impl<true>(x, rank0);
338448
#else
339-
340-
size_t i = 0;
341-
int popcount = std::popcount(~x[0]);
342-
while (i < 7 && popcount <= rank0) {
343-
rank0 -= popcount;
344-
popcount = std::popcount(~x[++i]);
345-
}
346-
return i * 64 + select_64(~x[i], rank0);
347-
449+
return select_512_scalar_impl<true>(x, rank0);
348450
#endif
349451
}
350452

0 commit comments

Comments
 (0)