AVX 4-bit integers - c++
I need to perform the following operation:
w[i] = scale * v[i] + point
scale and point are fixed, whereas v[] is a vector of 4-bit integers.
I need to compute w[] for the arbitrary input vector v[] and I want to speed up the process using AVX intrinsics. However, v[i] is a vector of 4-bit integers.
The question is how to perform operations on 4-bit integers using intrinsics? I could use 8-bit integers and perform operations that way, but is there a way to do the following:
[a,b] + [c,d] = [a+b,c+d]
[a,b] * [c,d] = [a * b,c * d]
(Ignoring overflow)
Using AVX intrinsics, where [...,...] Is an 8-bit integer and a,b,c,d are 4-bit integers?
If yes, would it be possible to give a short example on how this could work?
Just a partial answer (only addition) and in pseudo code (should be easy to extent to AVX2 intrinsics):
uint8_t a, b; // input containing two nibbles each
uint8_t c = a + b; // add with (unwanted) carry between nibbles
uint8_t x = a ^ b ^ c; // bits which are result of a carry
x &= 0x10; // only bit 4 is of interest
c -= x; // undo carry of lower to upper nibble
If either a or b is known to have bit 4 unset (i.e. the lowest bit of the upper nibble), it can be left out the computation of x.
As for multiplication: If scale is the same for all products, you can likely get away with some shifting and adding/subtracting (masking out overflow bits where necessarry). Otherwise, I'm afraid you need to mask out 4 bits of each 16bit word, do the operation, and fiddle them together at the end. Pseudo code (there is no AVX 8bit multiplication, so we need to operate with 16bit words):
uint16_t m0=0xf, m1=0xf0, m2=0xf00, m3=0xf000; // masks for each nibble
uint16_t a, b; // input containing 4 nibbles each.
uint16_t p0 = (a*b) & m0; // lowest nibble, does not require masking a,b
uint16_t p1 = ((a>>4) * (b&m1)) & m1;
uint16_t p2 = ((a>>8) * (b&m2)) & m2;
uint16_t p3 = ((a>>12)* (b&m3)) & m3;
uint16_t result = p0 | p1 | p2 | p3; // join results together
For fixed a, b in w[i]=v[i] * a + b, you can simply use a lookup table w_0_3 = _mm_shuffle_epi8(LUT_03, input) for the LSB. Split the input to even and odd nibbles, with the odd LUT preshifted by 4.
auto a = input & 15; // per element
auto b = (input >> 4) & 15; // shift as 16 bits
return LUTA[a] | LUTB[b];
How to generate those LUTs dynamically, is another issue, if at all.
4-bit aditions/multiplication can be done using AVX2, particularly if you want to apply those computations on larger vectors (say more than 128 elements). However, if you want to add just 4 numbers use straight scalar code.
We have done an extensive work on how to deal with 4-bit integers, and we have recently developed a library to do it Clover: 4-bit Quantized Linear Algebra Library (with focus on quantization). The code is also available at GitHub.
As you mentioned only 4-bit integers, I would assume that you are referring to signed integers (i.e. two's complements), and base my answer accordingly. Note that handling unsigned is in fact much simpler.
I would also assume that you would like to take vector int8_t v[n/2] that contains n 4-bit integers, and produce int8_t v_sum[n/4] having n/2 4-bit integers. All the code relative to the description bellow is available as a gist.
Packing / Unpacking
Obviously AVX2 does not offer any instructions to perform additions / multiplication on 4-bit integers, therefore, you must resort to the given 8- or 16-bit instruction. The first step in dealing with 4-bit arithmetics is to devise methods on how to place the 4-bit nibble into larger chunks of 8-, 16-, or 32-bit chunk.
For a sake of clarity let's assume that you want to unpack a given nibble from a 32-bit chunk that stores multiple 4-bit signed values into a corresponding 32-bit integer (figure below). This can be done with two bit shifts:
a logical left shift is used to shift the nibble so that it occupies the highest-order 4-bits of the 32-bit entity.
an arithmetic right shift is used to shift the nibble to the lowest order 4-bits of the 32-bit entity.
The arithmetic right shift has sign extension, filling the high-order 28 bits with the sign bit of the nibble. yielding a 32-bit integer with the same value as the two’s complement 4-bit value.
The goal of packing (left part of figure above) is to revert the unpacking operation. Two bit shifts can be used to place the lowest order 4 bits of a 32-bit integer anywhere within a 32-bit entity.
a logical left shift is used to shift the nibble so that it occupies the highest-order 4-bits of the 32-bit entity.
a logical right shift is used to shift the nibble to somewhere within the 32-bit entity.
The first sets the bits lower-ordered than the nibble to zero, and the second sets the bits higher-ordered than the nibble to zero. A bitwise OR operation can then be used to store up to eight nibbles in the 32-bit entity.
How to apply this in practice?
Let's assume that you have 64 x 32-bit integer values stored in 8 AVX registers __m256i q_1, q_2, q_3, q_4, q_5, q_6, q_7, q_8. Let's also assume that each value is in the [-8, 7], range. If you want to pack them into a single AVX register of 64 x 4-bit values, you can do as follows:
//
// Transpose the 8x8 registers
//
_mm256_transpose8_epi32(q_1, q_2, q_3, q_4, q_5, q_6, q_7, q_8);
//
// Shift values left
//
q_1 = _mm256_slli_epi32(q_1, 28);
q_2 = _mm256_slli_epi32(q_2, 28);
q_3 = _mm256_slli_epi32(q_3, 28);
q_4 = _mm256_slli_epi32(q_4, 28);
q_5 = _mm256_slli_epi32(q_5, 28);
q_6 = _mm256_slli_epi32(q_6, 28);
q_7 = _mm256_slli_epi32(q_7, 28);
q_8 = _mm256_slli_epi32(q_8, 28);
//
// Shift values right (zero-extend)
//
q_1 = _mm256_srli_epi32(q_1, 7 * 4);
q_2 = _mm256_srli_epi32(q_2, 6 * 4);
q_3 = _mm256_srli_epi32(q_3, 5 * 4);
q_4 = _mm256_srli_epi32(q_4, 4 * 4);
q_5 = _mm256_srli_epi32(q_5, 3 * 4);
q_6 = _mm256_srli_epi32(q_6, 2 * 4);
q_7 = _mm256_srli_epi32(q_7, 1 * 4);
q_8 = _mm256_srli_epi32(q_8, 0 * 4);
//
// Pack together
//
__m256i t1 = _mm256_or_si256(q_1, q_2);
__m256i t2 = _mm256_or_si256(q_3, q_4);
__m256i t3 = _mm256_or_si256(q_5, q_6);
__m256i t4 = _mm256_or_si256(q_7, q_8);
__m256i t5 = _mm256_or_si256(t1, t2);
__m256i t6 = _mm256_or_si256(t3, t4);
__m256i t7 = _mm256_or_si256(t5, t6);
Shifts usually take 1 cycle of throughput, and 1 cycle of latency, thus you can assume that are in fact quite inexpensive. If you have to deal with unsigned 4-bit values, the left shifts can be skipped all together.
To reverse the procedure, you can apply the same method. Let's assume that you have loaded 64 4-bit values into a single AVX register __m256i qu_64. In order to produce 64 x 32-bit integers __m256i q_1, q_2, q_3, q_4, q_5, q_6, q_7, q_8, you can execute the following:
//
// Shift values left
//
const __m256i qu_1 = _mm256_slli_epi32(qu_64, 4 * 7);
const __m256i qu_2 = _mm256_slli_epi32(qu_64, 4 * 6);
const __m256i qu_3 = _mm256_slli_epi32(qu_64, 4 * 5);
const __m256i qu_4 = _mm256_slli_epi32(qu_64, 4 * 4);
const __m256i qu_5 = _mm256_slli_epi32(qu_64, 4 * 3);
const __m256i qu_6 = _mm256_slli_epi32(qu_64, 4 * 2);
const __m256i qu_7 = _mm256_slli_epi32(qu_64, 4 * 1);
const __m256i qu_8 = _mm256_slli_epi32(qu_64, 4 * 0);
//
// Shift values right (sign-extent) and obtain 8x8
// 32-bit values
//
__m256i q_1 = _mm256_srai_epi32(qu_1, 28);
__m256i q_2 = _mm256_srai_epi32(qu_2, 28);
__m256i q_3 = _mm256_srai_epi32(qu_3, 28);
__m256i q_4 = _mm256_srai_epi32(qu_4, 28);
__m256i q_5 = _mm256_srai_epi32(qu_5, 28);
__m256i q_6 = _mm256_srai_epi32(qu_6, 28);
__m256i q_7 = _mm256_srai_epi32(qu_7, 28);
__m256i q_8 = _mm256_srai_epi32(qu_8, 28);
//
// Transpose the 8x8 values
//
_mm256_transpose8_epi32(q_1, q_2, q_3, q_4, q_5, q_6, q_7, q_8);
If dealing with unsigned 4-bits, the right shifts (_mm256_srai_epi32) can be skipped all-together, and instead of left shifts, we can perform left-logical shifts (_mm256_srli_epi32 ).
To see more details have a look a the gist here.
Adding Odd and Even 4-bit entries
Let's assume that you load from the vector using AVX:
const __m256i qv = _mm256_loadu_si256( ... );
Now, we can easily extract the odd and the even parts. Life would have been much easier if there were 8-bit shifts in AVX2, but there are none, so we have to deal with 16-bit shifts:
const __m256i hi_mask_08 = _mm256_set1_epi8(-16);
const __m256i qv_odd_dirty = _mm256_slli_epi16(qv, 4);
const __m256i qv_odd_shift = _mm256_and_si256(hi_mask_08, qv_odd_dirty);
const __m256i qv_evn_shift = _mm256_and_si256(hi_mask_08, qv);
At this point in time, you have essentially separated the odd and the even nibbles, in two AVX registers that hold their values in the high 4-bits (i.e. values in the range [-8 * 2^4, 7 * 2^4]). The procedure is the same even when dealing with unsigned 4-bit values. Now it is time to add the values.
const __m256i qv_sum_shift = _mm256_add_epi8(qv_odd_shift, qv_evn_shift);
This will work with both signed and unsigned, as binary addition work with two's complements. However, if you want to avoid overflows or underflows you can also consider addition with saturation already supported in AVX (for both signed and unsigned):
__m256i _mm256_adds_epi8 (__m256i a, __m256i b)
__m256i _mm256_adds_epu8 (__m256i a, __m256i b)
qv_sum_shift will be in the range [-8 * 2^4, 7 * 2^4]. To set it to the right value, we need to shift it back (Note that if qv_sum has to be unsigned, we can use _mm256_srli_epi16 instead):
const __m256i qv_sum = _mm256_srai_epi16(qv_sum_shift, 4);
The summation is now complete. Depending on your use case, this could as well be the end of the program, assuming that you want to produce 8-bit chunks of memory as a result. But let's assume that you want to solve a harder problem. Let's assume that the output is again a vector of 4-bit elements, with the same memory layout as the input one. In that case, we need to pack the 8-bit chunks into 4-bit chunks. However, the problem is that instead of having 64 values, we will end up with 32 values (i.e. half the size of the vector).
From this point there are two options. We either look ahead in the vector, processing 128 x 4-bit values, such that we produce 64 x 4-bit values. Or we revert to SSE, dealing with 32 x 4-bit values. Either way, the fastest way to pack the 8-bit chunks into 4-bit chunks would be to use the vpackuswb (or packuswb for SSE) instruction:
__m256i _mm256_packus_epi16 (__m256i a, __m256i b)
This instruction convert packed 16-bit integers from a and b to packed 8-bit integers using unsigned saturation, and store the results in dst. This means that we have to interleave the odd and even 4-bit values, such that they reside in the 8 low-bits of a 16-bit memory chunk. We can proceed as follows:
const __m256i lo_mask_16 = _mm256_set1_epi16(0x0F);
const __m256i hi_mask_16 = _mm256_set1_epi16(0xF0);
const __m256i qv_sum_lo = _mm256_and_si256(lo_mask_16, qv_sum);
const __m256i qv_sum_hi_dirty = _mm256_srli_epi16(qv_sum_shift, 8);
const __m256i qv_sum_hi = _mm256_and_si256(hi_mask_16, qv_sum_hi_dirty);
const __m256i qv_sum_16 = _mm256_or_si256(qv_sum_lo, qv_sum_hi);
The procedure will be identical for both signed and unsigned 4-bit values. Now, qv_sum_16 contains two consecutive 4-bit values, stored in the low-bits of a 16-bit memory chunk. Assuming that we have obtained qv_sum_16 from the next iteration (call it qv_sum_16_next), we can pack everything with:
const __m256i qv_sum_pack = _mm256_packus_epi16(qv_sum_16, qv_sum_16_next);
const __m256i result = _mm256_permute4x64_epi64(qv_sum_pack, 0xD8);
Alternatively, if we want to produce only 32 x 4-bit values, we can do the following:
const __m128i lo = _mm256_extractf128_si256(qv_sum_16, 0);
const __m128i hi = _mm256_extractf128_si256(qv_sum_16, 1);
const __m256i result = _mm_packus_epi16(lo, hi)
Putting it all together
Assuming signed nibbles, and vector size n, such that n is larger than 128 elements and is multiple of 128, we can perform the odd-even addition, producing n/2 elements as follows:
void add_odd_even(uint64_t n, int8_t * v, int8_t * r)
{
//
// Make sure that the vector size that is a multiple of 128
//
assert(n % 128 == 0);
const uint64_t blocks = n / 64;
//
// Define constants that will be used for masking operations
//
const __m256i hi_mask_08 = _mm256_set1_epi8(-16);
const __m256i lo_mask_16 = _mm256_set1_epi16(0x0F);
const __m256i hi_mask_16 = _mm256_set1_epi16(0xF0);
for (uint64_t b = 0; b < blocks; b += 2) {
//
// Calculate the offsets
//
const uint64_t offset0 = b * 32;
const uint64_t offset1 = b * 32 + 32;
const uint64_t offset2 = b * 32 / 2;
//
// Load 128 values in two AVX registers. Each register will
// contain 64 x 4-bit values in the range [-8, 7].
//
const __m256i qv_1 = _mm256_loadu_si256((__m256i *) (v + offset0));
const __m256i qv_2 = _mm256_loadu_si256((__m256i *) (v + offset1));
//
// Extract the odd and the even parts. The values will be split in
// two registers qv_odd_shift and qv_evn_shift, each of them having
// 32 x 8-bit values, such that each value is multiplied by 2^4
// and resides in the range [-8 * 2^4, 7 * 2^4]
//
const __m256i qv_odd_dirty_1 = _mm256_slli_epi16(qv_1, 4);
const __m256i qv_odd_shift_1 = _mm256_and_si256(hi_mask_08, qv_odd_dirty_1);
const __m256i qv_evn_shift_1 = _mm256_and_si256(hi_mask_08, qv_1);
const __m256i qv_odd_dirty_2 = _mm256_slli_epi16(qv_2, 4);
const __m256i qv_odd_shift_2 = _mm256_and_si256(hi_mask_08, qv_odd_dirty_2);
const __m256i qv_evn_shift_2 = _mm256_and_si256(hi_mask_08, qv_2);
//
// Perform addition. In case of overflows / underflows, behaviour
// is undefined. Values are still in the range [-8 * 2^4, 7 * 2^4].
//
const __m256i qv_sum_shift_1 = _mm256_add_epi8(qv_odd_shift_1, qv_evn_shift_1);
const __m256i qv_sum_shift_2 = _mm256_add_epi8(qv_odd_shift_2, qv_evn_shift_2);
//
// Divide by 2^4. At this point in time, each of the two AVX registers holds
// 32 x 8-bit values that are in the range of [-8, 7]. Summation is complete.
//
const __m256i qv_sum_1 = _mm256_srai_epi16(qv_sum_shift_1, 4);
const __m256i qv_sum_2 = _mm256_srai_epi16(qv_sum_shift_2, 4);
//
// Now, we want to take the even numbers of the 32 x 4-bit register, and
// store them in the high-bits of the odd numbers. We do this with
// left shifts that extend in zero, and 16-bit masks. This operation
// results in two registers qv_sum_lo and qv_sum_hi that hold 32
// values. However, each consecutive 4-bit values reside in the
// low-bits of a 16-bit chunk.
//
const __m256i qv_sum_1_lo = _mm256_and_si256(lo_mask_16, qv_sum_1);
const __m256i qv_sum_1_hi_dirty = _mm256_srli_epi16(qv_sum_shift_1, 8);
const __m256i qv_sum_1_hi = _mm256_and_si256(hi_mask_16, qv_sum_1_hi_dirty);
const __m256i qv_sum_2_lo = _mm256_and_si256(lo_mask_16, qv_sum_2);
const __m256i qv_sum_2_hi_dirty = _mm256_srli_epi16(qv_sum_shift_2, 8);
const __m256i qv_sum_2_hi = _mm256_and_si256(hi_mask_16, qv_sum_2_hi_dirty);
const __m256i qv_sum_16_1 = _mm256_or_si256(qv_sum_1_lo, qv_sum_1_hi);
const __m256i qv_sum_16_2 = _mm256_or_si256(qv_sum_2_lo, qv_sum_2_hi);
//
// Pack the two registers of 32 x 4-bit values, into a single one having
// 64 x 4-bit values. Use the unsigned version, to avoid saturation.
//
const __m256i qv_sum_pack = _mm256_packus_epi16(qv_sum_16_1, qv_sum_16_2);
//
// Interleave the 64-bit chunks.
//
const __m256i qv_sum = _mm256_permute4x64_epi64(qv_sum_pack, 0xD8);
//
// Store the result
//
_mm256_storeu_si256((__m256i *)(r + offset2), qv_sum);
}
}
A self-contained tester and validator of this code is available in the gist here.
Multiplying Odd and Even 4-bit entries
For the multiplication of the odd and even entries, we can use the same strategy as described above to extract the 4-bits into larger chunks.
AVX2 does not offer 8-bit multiplication, only 16-bit. However, we can implement 8-bit multiplication following the method implemented in the Agner Fog's C++ vector class library:
static inline Vec32c operator * (Vec32c const & a, Vec32c const & b) {
// There is no 8-bit multiply in SSE2. Split into two 16-bit multiplies
__m256i aodd = _mm256_srli_epi16(a,8); // odd numbered elements of a
__m256i bodd = _mm256_srli_epi16(b,8); // odd numbered elements of b
__m256i muleven = _mm256_mullo_epi16(a,b); // product of even numbered elements
__m256i mulodd = _mm256_mullo_epi16(aodd,bodd); // product of odd numbered elements
mulodd = _mm256_slli_epi16(mulodd,8); // put odd numbered elements back in place
__m256i mask = _mm256_set1_epi32(0x00FF00FF); // mask for even positions
__m256i product = selectb(mask,muleven,mulodd); // interleave even and odd
return product;
}
I would suggest however to extract the nibbles into 16-bit chunks first and then use _mm256_mullo_epi16 to avoid performing unnecessary shifts.
Related
Creating bitmask in O(1) with SIMD operations
I am new at c++ and working with simd. What i am trying to do is creating a bitmask from input in constant time. For example: input 1,3,4 => output 13 (or 26 indexing scheme does not matter): 1101 (1st, 3rd and 4th bits are 1) input 2,5,7 => output 82 : 1010010 (2nd, 5th and 7th bits are 1) input type does not matter, it can be array for example. I accomplished this with for loop, which is not wanted. Is there a function to create bitmask in a constant time?
Constant time cannot work if you have a variable number inputs. You have to iterate over the values at least once, right? In any case, you can use intrinsics to minimize the number of operations. You have not specified your target architecture or the integer size. So I assume AVX2 and 64 bit integers as output. Also, for convenience, I assume that the inputs are 64 bit. If your inputs are smaller-sized integers than the output, you have to add some zero-extensions. #include <immintrin.h> #include <array> #include <cstdint> #include <cstdio> std::uint64_t accum(const std::uint64_t* bitpositions, std::size_t n) { // 2 x 64 bit integers set to 1 const __m128i ones2 = _mm_set1_epi64(_m_from_int64(1)); // 4 x 64 bit integers set to 1 const __m256i ones4 = _mm256_broadcastsi128_si256(ones2); // 4 x 64 bit integers serving as partial bit masks __m256i accum4 = _mm256_setzero_si256(); std::size_t i; for(i = 0; i + 4 <= n; i += 4) { // may be replaced with aligned load __m256i positions = _mm256_loadu_si256((const __m256i*)(bitpositions + i)); // vectorized (1 << position) bit shift __m256i shifted = _mm256_sllv_epi64(ones4, positions); // add new bits to accumulator accum4 = _mm256_or_si256(accum4, shifted); } // reduce 4 to 2 64 bit integers __m128i accum2 = _mm256_castsi256_si128(accum4); __m128i high2 = _mm256_extracti128_si256(accum4, 1); if(i + 2 <= n) { // zero or one iteration with 2 64 bit integers __m128i positions = _mm_loadu_si128((const __m128i*)(bitpositions + i)); __m128i shifted = _mm_sllv_epi64(ones2, positions); accum2 = _mm_or_si128(accum2, shifted); i += 2; } // high2 folded in with delay to account for extract latency accum2 = _mm_or_si128(accum2, high2); // reduce to 1 64 bit integer __m128i high1 = _mm_unpackhi_epi64(accum2, accum2); accum2 = _mm_or_si128(accum2, high1); std::uint64_t accum1 = static_cast<std::uint64_t>(_mm_cvtsi128_si64(accum2)); if(i < n) accum1 |= 1 << bitpositions[i]; return accum1; } EDIT I have just seen that your example inputs use 1-based indexing. So bit 1 would be set for value 1 and input value 0 is probably undefined behavior. I suggest switching to zero-based indexing. But if you are stuck with that notation, just add a _mm256_sub_epi64(positions, ones4) or _mm_sub_epi64(positions, ones2) before the shift. With smaller input size And here is a version for byte-sized input integers. std::uint64_t accum(const std::uint8_t* bitpositions, std::size_t n) { const __m128i ones2 = _mm_set1_epi64(_m_from_int64(1)); const __m256i ones4 = _mm256_broadcastsi128_si256(ones2); __m256i accum4 = _mm256_setzero_si256(); std::size_t i; for(i = 0; i + 4 <= n; i += 4) { /* * As far as I can see, there is no point in loading a full 128 or 256 bit * vector. To zero-extend more values, we would need to use many shuffle * instructions and those have a lower throughput than repeated * 32 bit loads */ __m128i positions = _mm_cvtsi32_si128(*(const int*)(bitpositions + i)); __m256i extended = _mm256_cvtepu8_epi64(positions); __m256i shifted = _mm256_sllv_epi64(ones4, extended); accum4 = _mm256_or_si256(accum4, shifted); } __m128i accum2 = _mm256_castsi256_si128(accum4); __m128i high2 = _mm256_extracti128_si256(accum4, 1); accum2 = _mm_or_si128(accum2, high2); /* * Until AVX512, there is no single instruction to load 2 byte into a vector * register. So we don't bother. Instead, the scalar code below will run up * to 3 times */ __m128i high1 = _mm_unpackhi_epi64(accum2, accum2); accum2 = _mm_or_si128(accum2, high1); std::uint64_t accum1 = static_cast<std::uint64_t>(_mm_cvtsi128_si64(accum2)); /* * We use a separate accumulator to avoid the long dependency chain through * the reduction above */ std::uint64_t tail = 0; /* * Compilers create a ton of code if we give them a simple loop because they * think they can vectorize. So we unroll the loop, even if it is stupid */ if(i + 2 <= n) { tail = std::uint64_t(1) << bitpositions[i++]; tail |= std::uint64_t(1) << bitpositions[i++]; } if(i < n) tail |= std::uint64_t(1) << bitpositions[i]; return accum1 | tail; }
Count leading zero bits for each element in AVX2 vector, emulate _mm256_lzcnt_epi32
With AVX512, there is the intrinsic _mm256_lzcnt_epi32, which returns a vector that, for each of the 8 32-bit elements, contains the number of leading zero bits in the input vector's element. Is there an efficient way to implement this using AVX and AVX2 instructions only? Currently I'm using a loop which extracts each element and applies the _lzcnt_u32 function. Related: to bit-scan one large bitmap, see Count leading zeros in __m256i word which uses pmovmskb -> bitscan to find which byte to do a scalar bitscan on. This question is about doing 8 separate lzcnts on 8 separate 32-bit elements when you're actually going to use all 8 results, not just select one.
float represents numbers in an exponential format, so int->FP conversion gives us the position of the highest set bit encoded in the exponent field. We want int->float with magnitude rounded down (truncate the value towards 0), not the default rounding of nearest. That could round up and make 0x3FFFFFFF look like 0x40000000. If you're doing a lot of these conversions without doing any FP math, you could set the rounding mode in the MXCSR1 to truncation then set it back when you're done. Otherwise you can use v & ~(v>>8) to keep the 8 most-significant bits and zero some or all lower bits, including a potentially-set bit 8 below the MSB. That's enough to ensure all rounding modes never round up to the next power of two. It always keeps the 8 MSB because v>>8 shifts in 8 zeros, so inverted that's 8 ones. At lower bit positions, wherever the MSB is, 8 zeros are shifted past there from higher positions, so it will never clear the most significant bit of any integer. Depending on how set bits below the MSB line up, it might or might not clear more below the 8 most significant. After conversion, we use an integer shift on the bit-pattern to bring the exponent (and sign bit) to the bottom and undo the bias with a saturating subtract. We use min to set the result to 32 if no bits were set in the original 32-bit input. __m256i avx2_lzcnt_epi32 (__m256i v) { // prevent value from being rounded up to the next power of two v = _mm256_andnot_si256(_mm256_srli_epi32(v, 8), v); // keep 8 MSB v = _mm256_castps_si256(_mm256_cvtepi32_ps(v)); // convert an integer to float v = _mm256_srli_epi32(v, 23); // shift down the exponent v = _mm256_subs_epu16(_mm256_set1_epi32(158), v); // undo bias v = _mm256_min_epi16(v, _mm256_set1_epi32(32)); // clamp at 32 return v; } Footnote 1: fp->int conversion is available with truncation (cvtt), but int->fp conversion is only available with default rounding (subject to MXCSR). AVX512F introduces rounding-mode overrides for 512-bit vectors which would solve the problem, __m512 _mm512_cvt_roundepi32_ps( __m512i a, int r);. But all CPUs with AVX512F also support AVX512CD so you could just use _mm512_lzcnt_epi32. And with AVX512VL, _mm256_lzcnt_epi32
#aqrit's answer looks like a more-clever use of FP bithacks. My answer below is based on the first place I looked for a bithack which was old and aimed at scalar so it didn't try to avoid double (which is wider than int32 and thus a problem for SIMD). It uses HW signed int->float conversion and saturating integer subtracts to handle the MSB being set (negative float), instead of stuffing bits into a mantissa for manual uint->double. If you can set MXCSR to round down across a lot of these _mm256_lzcnt_epi32, that's even more efficient. https://graphics.stanford.edu/~seander/bithacks.html#IntegerLogIEEE64Float suggests stuffing integers into the mantissa of a large double, then subtracting to get the FPU hardware to get a normalized double. (I think this bit of magic is doing uint32_t -> double, with the technique #Mysticial explains in How to efficiently perform double/int64 conversions with SSE/AVX? (which works for uint64_t up to 252-1) Then grab the exponent bits of the double and undo the bias. I think integer log2 is the same thing as lzcnt, but there might be an off-by-1 at powers of 2. The Standford Graphics bithack page lists other branchless bithacks you could use that would probably still be better than 8x scalar lzcnt. If you knew your numbers were always small-ish (like less than 2^23) you could maybe do this with float and avoid splitting and blending. int v; // 32-bit integer to find the log base 2 of int r; // result of log_2(v) goes here union { unsigned int u[2]; double d; } t; // temp t.u[__FLOAT_WORD_ORDER==LITTLE_ENDIAN] = 0x43300000; t.u[__FLOAT_WORD_ORDER!=LITTLE_ENDIAN] = v; t.d -= 4503599627370496.0; r = (t.u[__FLOAT_WORD_ORDER==LITTLE_ENDIAN] >> 20) - 0x3FF; The code above loads a 64-bit (IEEE-754 floating-point) double with a 32-bit integer (with no paddding bits) by storing the integer in the mantissa while the exponent is set to 252. From this newly minted double, 252 (expressed as a double) is subtracted, which sets the resulting exponent to the log base 2 of the input value, v. All that is left is shifting the exponent bits into position (20 bits right) and subtracting the bias, 0x3FF (which is 1023 decimal). To do this with AVX2, blend and shift+blend odd/even halves with set1_epi32(0x43300000) and _mm256_castps_pd to get a __m256d. And after subtracting, _mm256_castpd_si256 and shift / blend the low/high halves into place then mask to get the exponents. Doing integer operations on FP bit-patterns is very efficient with AVX2, just 1 cycle of extra latency for a bypass delay when doing integer shifts on the output of an FP math instruction. (TODO: write it with C++ intrinsics, edit welcome or someone else could just post it as an answer.) I'm not sure if you can do anything with int -> double conversion and then reading the exponent field. Negative numbers have no leading zeros and positive numbers give an exponent that depends on the magnitude. If you did want that, you'd go one 128-bit lane at a time, shuffling to feed xmm -> ymm packed int32_t -> packed double conversion.
The question is also tagged AVX, but there are no instructions for integer processing in AVX, which means one needs to fall back to SSE on platforms that support AVX but not AVX2. I am showing an exhaustively tested, but a bit pedestrian version below. The basic idea here is as in the other answers, in that the count of leading zeros is determined by the floating-point normalization that occurs during integer to floating-point conversion. The exponent of the result has a one-to-one correspondence with the count of leading zeros, except that the result is wrong in the case of an argument of zero. Conceptually: clz (a) = (158 - (float_as_uint32 (uint32_to_float_rz (a)) >> 23)) + (a == 0) where float_as_uint32() is a re-interpreting cast and uint32_to_float_rz() is a conversion from unsigned integer to floating-point with truncation. A normal, rounding, conversion could bump up the conversion result to the next power of two, resulting in an incorrect count of leading zero bits. SSE does not provide truncating integer to floating-point conversion as a single instruction, nor conversions from unsigned integers. This functionality needs to be emulated. The emulation does not need to be exact, as long as it does not change the magnitude of the conversion result. The truncation part is handled by the invert - right shift - andn technique from aqrit's answer. To use signed conversion, we cut the number in half before the conversion, then double and increment after the conversion: float approximate_uint32_to_float_rz (uint32_t a) { float r = (float)(int)((a >> 1) & ~(a >> 2)); return r + r + 1.0f; } This approach is translated into SSE intrinsics in sse_clz() below. #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <string.h> #include "immintrin.h" /* compute count of leading zero bits using floating-point normalization. clz(a) = (158 - (float_as_uint32 (uint32_to_float_rz (a)) >> 23)) + (a == 0) The problematic part here is uint32_to_float_rz(). SSE does not offer conversion of unsigned integers, and no rounding modes in integer to floating-point conversion. Since all we need is an approximate version that preserves order of magnitude: float approximate_uint32_to_float_rz (uint32_t a) { float r = (float)(int)((a >> 1) & ~(a >> 2)); return r + r + 1.0f; } */ __m128i sse_clz (__m128i a) { __m128 fp1 = _mm_set_ps1 (1.0f); __m128i zero = _mm_set1_epi32 (0); __m128i i158 = _mm_set1_epi32 (158); __m128i iszero = _mm_cmpeq_epi32 (a, zero); __m128i lsr1 = _mm_srli_epi32 (a, 1); __m128i lsr2 = _mm_srli_epi32 (a, 2); __m128i atrunc = _mm_andnot_si128 (lsr2, lsr1); __m128 atruncf = _mm_cvtepi32_ps (atrunc); __m128 atruncf2 = _mm_add_ps (atruncf, atruncf); __m128 conv = _mm_add_ps (atruncf2, fp1); __m128i convi = _mm_castps_si128 (conv); __m128i lsr23 = _mm_srli_epi32 (convi, 23); __m128i res = _mm_sub_epi32 (i158, lsr23); return _mm_sub_epi32 (res, iszero); } /* Portable reference implementation of 32-bit count of leading zeros */ int clz32 (uint32_t a) { uint32_t r = 32; if (a >= 0x00010000) { a >>= 16; r -= 16; } if (a >= 0x00000100) { a >>= 8; r -= 8; } if (a >= 0x00000010) { a >>= 4; r -= 4; } if (a >= 0x00000004) { a >>= 2; r -= 2; } r -= a - (a & (a >> 1)); return r; } /* Test floating-point based count leading zeros exhaustively */ int main (void) { __m128i res; uint32_t resi[4], refi[4]; uint32_t count = 0; do { refi[0] = clz32 (count); refi[1] = clz32 (count + 1); refi[2] = clz32 (count + 2); refi[3] = clz32 (count + 3); res = sse_clz (_mm_set_epi32 (count + 3, count + 2, count + 1, count)); memcpy (resi, &res, sizeof resi); if ((resi[0] != refi[0]) || (resi[1] != refi[1]) || (resi[2] != refi[2]) || (resi[3] != refi[3])) { printf ("error # %08x %08x %08x %08x\n", count, count+1, count+2, count+3); return EXIT_FAILURE; } count += 4; } while (count); return EXIT_SUCCESS; }
SSE2 packed 8-bit integer signed multiply (high-half): Decomposing a m128i (16x8 bit) into two m128i (8x16 each) and repack
I'm trying to multiply two m128i byte per byte (8 bit signed integers). The problem here is overflow. My solution is to store these 8 bit signed integers into 16 bit signed integers, multiply, then pack the whole thing into a m128i of 16 x 8 bit integers. Here is the __m128i mulhi_epi8(__m128i a, __m128i b) emulation I made: inline __m128i mulhi_epi8(__m128i a, __m128i b) { auto a_decomposed = decompose_epi8(a); auto b_decomposed = decompose_epi8(b); __m128i r1 = _mm_mullo_epi16(a_decomposed.first, b_decomposed.first); __m128i r2 = _mm_mullo_epi16(a_decomposed.second, b_decomposed.second); return _mm_packs_epi16(_mm_srai_epi16(r1, 8), _mm_srai_epi16(r2, 8)); } decompose_epi8 is implemented in a non-simd way: inline std::pair<__m128i, __m128i> decompose_epi8(__m128i input) { std::pair<__m128i, __m128i> result; // result.first => should contain 8 shorts in [-128, 127] (8 first bytes of the input) // result.second => should contain 8 shorts in [-128, 127] (8 last bytes of the input) for (int i = 0; i < 8; ++i) { result.first.m128i_i16[i] = input.m128i_i8[i]; result.second.m128i_i16[i] = input.m128i_i8[i + 8]; } return result; } This code works well. My goal now is to implement a simd version of this for loop. I looked at the Intel Intrinsics Guide but I can't find a way to do this. I guess shuffle could do the trick but I have trouble conceptualising this.
As you want to do signed multiplication, you need to sign-extend each byte to 16bit words, or move them into the upper half of each 16bit word. Since you pack the results back together afterwards, you can split the input into odd and even bytes, instead of the higher and lower half. Then sign-extension of the odd bytes can be done by arithmetically shifting all 16bit parts to the right You can extract the odd bytes by masking out the even bytes, and to get the even bytes, you can shift all 16bit parts to the left (both need to be multiplied by _mm_mulhi_epi16). The following should work with SSE2: __m128i mulhi_epi8(__m128i a, __m128i b) { __m128i mask = _mm_set1_epi16(0xff00); // mask higher bytes: __m128i a_hi = _mm_and_si128(a, mask); __m128i b_hi = _mm_and_si128(b, mask); __m128i r_hi = _mm_mulhi_epi16(a_hi, b_hi); // mask out garbage in lower half: r_hi = _mm_and_si128(r_hi, mask); // shift lower bytes to upper half __m128i a_lo = _mm_slli_epi16(a,8); __m128i b_lo = _mm_slli_epi16(b,8); __m128i r_lo = _mm_mulhi_epi16(a_lo, b_lo); // shift result to the lower half: r_lo = _mm_srli_epi16(r_lo,8); // join result and return: return _mm_or_si128(r_hi, r_lo); } Note: a previous version used shifts to sign-extend the odd bytes. On most Intel CPUs this would increase P0 usage (which needs to be used for multiplication as well). Bit-logic can operate on more ports, so this version should have better throughput.
SIMD __m256i to __m256d cast results
I am trying to cast a SIMD integer variable into a double. But I can't see what the result of this operation will be. Example: int arr[8]={12345678,12333333,12344444,12355555,12366666,12377777,12388888,12399999}; __m256i temp = _mm256_load_si256((__m256i *) arr); __m256d temp2 = _mm256_castsi256_pd (temp); as a result of this operation what are the members in my temp2?
Short answer The members in temp2 will be: {4.014635e-305, 4.062922e-305, 4.111209e-305, 4.159495e-305} How to obtain the values Just write the SIMD data into an double array back and print it. #include <stdio.h> #include <immintrin.h> int main(void) { int hoge[4]; /* hack that worked on tested environment to avoid Segmentation Fault */ double result[4]; int i; int arr[8]={12345678,12333333,12344444,12355555,12366666,12377777,12388888,12399999}; __m256i temp = _mm256_load_si256((__m256i *) arr); __m256d temp2 = _mm256_castsi256_pd (temp); _mm256_storeu_pd(result, temp2); for (i = 0; i < 4; i++) printf("result[%d] = %.6e (%.15a)\n", i, result[i], result[i]); return 0; } I ran this code on Wandbox and got this output: result[0] = 4.014635e-305 (0x1.c311500bc614e00p-1012) result[1] = 4.062922e-305 (0x1.c87e300bc5c7c00p-1012) result[2] = 4.111209e-305 (0x1.cdeb100bcb34a00p-1012) result[3] = 4.159495e-305 (0x1.d357f00bd0a1800p-1012) You can write the SIMD data to an double array via _mm256_storeu_pd(). An exception may be generated when address that is not 32-byte aligned is passed into _mm256_load_si256(), so you should do the alignment. Actually Segmentation Fault occurred on Wandbox, so I inserted the dummy array hoge to do the alignment. Why the values were obtained _mm256_castsi256_pd() is actually just copying the bytes and changing their interpretation. Assuming little-endian is used and int is 4-byte long, the data in arr is like this in the byte-addressed memory: data in arr[8]: | 12345678| 12333333| 12344444| 12355555| 12366666| 12377777| 12388888| 12399999| byte data in arr[8] (in little endian): |4e 61 bc 00|15 31 bc 00|7c 5c bc 00|e3 87 bc 00|4a b3 bc 00|b1 de bc 00|18 0a bd 00|7f 35 bd 00| data seen as 64-bit hex: | 0x00bc311500bc614e| 0x00bc87e300bc5c7c| 0x00bcdeb100bcb34a| 0x00bd357f00bd0a18| Then, assuming that 64-bit IEEE754 is used in double, the 64-bit data consists of 1-bit sign, 11-bit exponent and 52-bit significand. Taking the first element 0x00bc311500bc614e as example, the sign bit is 0 (plus/zero), the exponent is 0x00b (11 - 1023 = -1012) and the significand is 0xc311500bc614e. This matches with what is printed via %.15a in the sample code above. (two extra 0s are printed because printing 15 digits is specified while only data for 13 digits is reordered, so the remainder is padded with 0.) The other elements also matches like this.
Operation _mm256_castsi256_pd does literally nothing, it is a reinterpretation - equivalent to: int v_i; double d_i = *((double*)(int*)&v_i). Use __m256d _mm256_cvtepi32_pd (__m128i a) as it actually converts 4 integers to 4 doubles. alignas(16) int arr[4]={12345678,12333333,12344444,12355555}; __m128i temp = _mm_load_si128((__m128i *) arr); __m256d temp2 = _mm256_cvtepi32_pd(temp); Note: the loading operations _mm_load_si128 and _mm256_load_si256 require the addresses to be properly aligned. Else use the unaligned versions _mm_loadu_si128 and _mm256_loadu_si256; thought the unaligned versions are slower.
As a result of this operation, temp2 will contain garbage. For example, the first double lane will be 4.0146351468550722e-305. This is by design. _mm256_castsi256_pd intrinsic doesn’t convert values, it only re-interprets bits in register as doubles. If you want these double constants in the register, just use _mm256_setr_pd intrinsic: // Set double values to the constants __m256d temp2 = _mm256_setr_pd( 12345678, 12333333, 12344444, 12355555 ); Or if these values aren’t constant, use _mm256_cvtepi32_pd intrinsic, here's a complete example: alignas( 32 ) int arr[ 8 ] = { 12345678, 12333333, 12344444, 12355555, 12366666, 12377777, 12388888, 12399999 }; __m256i integers = _mm256_load_si256( ( const __m256i* ) &arr ); // Convert first 4 int32 values to doubles __m256d lowDoubles = _mm256_cvtepi32_pd( _mm256_castsi256_si128( integers ) ); // Convert last 4 values to doubles __m256d highDoubles = _mm256_cvtepi32_pd( _mm256_extracti128_si256( integers, 1 ) ); This will actually convert, not bit cast, the values. AVX registers hold 256 bits of data. This is 8 int32 values in __m256i type, 8 float values in __m256 data type, but only 4 double values in __m256d type. P.S. There’s also alignment bug in your code, best way to fix is add alignas(32) before int arr[8]
Global bitwise shift of 128, 256, 512 bit registry using intrinsics?
Consider an array of 64 bit unsigned integers, like: std::array<unsigned long long int, 20> a; What is the fastest way, including using intel or compiler intrinsics (this or that) (using g++ 5.3), to perform a global bitshift (right or left) as this array was a single bit integer?
You might want to look at std::bitset, which is a container for a number of bits known at compile time. If I'm understanding your question right, that is what you are trying to simulate with your array. The bitset class includes overloaded >> and << operators to perform bit shifts, and those implementations may be optimised in your compiler/standard library combination.
Here are some x86 left shift functions that use xmm and ymm registers through intrinsics. It shouldn't be too hard to make corresponding right shift functions. They are taken from a software lfsr benchmark: //---------------------------------------------------------------------------- // bit shift left a 128-bit value using xmm registers // __m128i *data - data to shift // int count - number of bits to shift // return: __m128i - carry out bit(s) static __m128i bitShiftLeft128xmm (__m128i *data, int count) { __m128i innerCarry, carryOut; innerCarry = _mm_srli_epi64 (*data, 64 - count); // carry outs in bit 0 of each qword carryOut = _mm_shuffle_epi32 (innerCarry, 0xFE); // upper carry in xmm bit 0, others zero innerCarry = _mm_shuffle_epi32 (innerCarry, 0xCF); // lower carry in xmm bit 64, others zero *data = _mm_slli_epi64 (*data, count); // shift all qwords left *data = _mm_or_si128 (*data, innerCarry); // propagate carry out from low qword return carryOut; } //---------------------------------------------------------------------------- // bit shift left a 256-bit value using xmm registers // __m128i *data - data to shift, ls part stored first // int count - number of bits to shift // return: __m128i - carry out bit(s) static __m128i bitShiftLeft256xmm (__m128i *data, int count) { __m128i carryOut0, carryOut1; carryOut0 = bitShiftLeft128xmm (&data [0], count); carryOut1 = bitShiftLeft128xmm (&data [1], count); data [1] = _mm_or_si128 (data [1], carryOut0); return carryOut1; } //---------------------------------------------------------------------------- // bit shift left a 512-bit value using xmm registers // __m128i *data - data to shift, ls part stored first // int count - number of bits to shift // return: __m128i - carry out bit(s) static __m128i bitShiftLeft512xmm (__m128i *data, int count) { __m128i carryOut0, carryOut1; carryOut0 = bitShiftLeft256xmm (&data [0], count); carryOut1 = bitShiftLeft256xmm (&data [2], count); data [2] = _mm_or_si128 (data [2], carryOut0); return carryOut1; } //---------------------------------------------------------------------------- // bit shift left a 256-bit value using ymm registers // __m256i *data - data to shift // int count - number of bits to shift // return: __m256i - carry out bit(s) static __m256i bitShiftLeft256ymm (__m256i *data, int count) { __m256i innerCarry, carryOut, rotate; innerCarry = _mm256_srli_epi64 (*data, 64 - count); // carry outs in bit 0 of each qword rotate = _mm256_permute4x64_epi64 (innerCarry, 0x93); // rotate ymm left 64 bits innerCarry = _mm256_blend_epi32 (_mm256_setzero_si256 (), rotate, 0xFC); // clear lower qword *data = _mm256_slli_epi64 (*data, count); // shift all qwords left *data = _mm256_or_si256 (*data, innerCarry); // propagate carrys from low qwords carryOut = _mm256_xor_si256 (innerCarry, rotate); // clear all except lower qword return carryOut; } //---------------------------------------------------------------------------- // bit shift left a 512-bit value using ymm registers // __m256i *data - data to shift, ls part stored first // int count - number of bits to shift // return: __m256i - carry out bit(s) static __m256i bitShiftLeft512ymm (__m256i *data, int count) { __m256i carryOut0, carryOut1; carryOut0 = bitShiftLeft256ymm (&data [0], count); carryOut1 = bitShiftLeft256ymm (&data [1], count); data [1] = _mm256_or_si256 (data [1], carryOut0); return carryOut1; } //----------------------------------------------------------------------------