1818// / Simd integer unpacking kernels, that is small functions that efficiently operate over
1919// / a fixed input size.
2020// /
21- // / This a generalization of the algorithm from Daniel Lemire and Leonid Boytsov,
21+ // / This is a generalization of the algorithm from Daniel Lemire and Leonid Boytsov,
2222// / Decoding billions of integers per second through vectorization, Software Practice &
2323// / Experience 45 (1), 2015.
2424// / http://arxiv.org/abs/1209.2137
@@ -161,7 +161,7 @@ auto left_shift(const xsimd::batch<Int, Arch>& batch,
161161 constexpr bool kHasAvx2 = HasAvx2<Arch>;
162162 static_assert (
163163 !(kHasSse2 && kHasAvx2 ),
164- " In xsimd, an x86 arch is either part of the the SSE family or of the AVX family, "
164+ " In xsimd, an x86 arch is either part of the SSE family or of the AVX family,"
165165 " not both. If this check fails, it means the assumptions made here to detect SSE "
166166 " and AVX are out of date." );
167167
@@ -255,8 +255,8 @@ auto right_shift_by_excess(const xsimd::batch<Int, Arch>& batch,
255255// /
256256// / Due to packing misalignment, we may not be able to fit in a SIMD register as many
257257// / packed values as unpacked values.
258- // / This happens because of the packing bit alignment creates spare bits we are force to
259- // / read when reading whole bytes. This is mostly know to be the case with 63 bits values
258+ // / This happens because of the packing bit alignment creates spare bits we are forced to
259+ // / read when reading whole bytes. This is mostly known to be the case with 63 bits values
260260// / in a 128 bit SIMD register.
261261// /
262262// / @see KernelShape
@@ -308,7 +308,7 @@ constexpr bool PackedIsOversizedForSimd(int simd_bit_size, int unpacked_bit_size
308308// / bits [126, 252[, are not byte aligned and would require reading the whole [120, 256[
309309// / range, or 17 bytes. We would need to read more than one SIMD register to create
310310// / a single output register with two values. Such pathological cases are not handled with
311- // / SIMD but rather fully alling back to the exact scalar loop.
311+ // / SIMD but rather fully falling back to the exact scalar loop.
312312// /
313313// / There is currently no "small" classification, as medium kernels can handle even 1-bit
314314// / packed values. Future work may investigate a dedicated kernel for when a single byte
@@ -376,13 +376,17 @@ using KernelTraitsWithUnpackUint = KernelTraits<Uint, KerTraits::kShape.packed_b
376376 * MediumKernel *
377377 ******************/
378378
379+ // / Compile time options of a Medium kernel.
379380struct MediumKernelOptions {
380381 // / An indicative limit on the number of values unpacked by the kernel.
381382 // / This is a heuristic setting: other constraints such as alignment may not always make
382383 // / small values feasibles. Must be a power of two.
383384 int unpacked_per_kernel_limit_;
384385};
385386
387+ // / Compile time dimensions of a Medium kernel.
388+ // /
389+ // / @see MediumKernel for explanation of the algorithm.
386390struct MediumKernelPlanSize {
387391 int reads_per_kernel_;
388392 int swizzles_per_read_;
@@ -465,6 +469,12 @@ constexpr int reduced_bytes_per_read(int bits_per_read, int simd_byte_size) {
465469 return simd_byte_size;
466470}
467471
472+ // / Compile time constants of a Medium kernel.
473+ // /
474+ // / This contains most of the information about the medium kernel algorithm stored
475+ // / as index offsets and other constants (shift amounts...).
476+ // /
477+ // / @see MediumKernel for an explanation of the algorithm.
468478template <typename KerTraits, MediumKernelOptions kOptions >
469479struct MediumKernelPlan {
470480 using Traits = KerTraits;
@@ -481,7 +491,7 @@ struct MediumKernelPlan {
481491 using Shift = std::array<uint_type, kShape .unpacked_per_simd()>;
482492 using ShiftsPerSwizzle = std::array<Shift, kPlanSize .shifts_per_swizzle()>;
483493 using ShiftsPerRead = std::array<ShiftsPerSwizzle, kPlanSize .swizzles_per_read()>;
484- using ShitsPerKernel = std::array<ShiftsPerRead, kPlanSize .reads_per_kernel()>;
494+ using ShiftsPerKernel = std::array<ShiftsPerRead, kPlanSize .reads_per_kernel()>;
485495
486496 constexpr static MediumKernelPlan Build ();
487497
@@ -505,7 +515,7 @@ struct MediumKernelPlan {
505515
506516 ReadsPerKernel reads;
507517 SwizzlesPerKernel swizzles;
508- ShitsPerKernel shifts;
518+ ShiftsPerKernel shifts;
509519 uint_type mask = bit_util::LeastSignificantBitMask<uint_type>(kShape .packed_bit_size());
510520};
511521
@@ -556,6 +566,70 @@ constexpr auto MediumKernelPlan<KerTraits, kOptions>::Build()
556566 return plan;
557567}
558568
569+ // / Unpack a fixed number of packed integer using SIMD operations.
570+ // /
571+ // / This is only valid for medium sized inputs where packed values spread over fewer bytes
572+ // / than the unpacked integer size.
573+ // / All dimensions and constants are computed at compile time to reduce run-time
574+ // / computations and improve xsimd fallback possibilities (with
575+ // / ``xsimd::batch_constant``).
576+ // /
577+ // / Let us give an example of the algorithm for values packed over 3 bits, extracted to
578+ // / uint32_t, using a fictive 64 bits SIMD size.
579+ // / The input register is as follow, where 3 consecutive letter represents the 3 bits of a
580+ // / packed value in little endian representation.
581+ // /
582+ // / |AAABBBCC|CDDDEEEF|FFGGGHHH|IIIJJJKK||KLLLMMMN|NNOOOPPP|QQQRRRSS|STTTUUUV|
583+ // /
584+ // / The algorithm generates SIMD registers of unpacked values ready to be written to
585+ // / memory. Here we can fit 64/32 = 2 unpacked values per output register.
586+ // / The first step is to put the values in the 32 bit slots corresponding to the unpacked
587+ // / values using a swizzle (shuffle/indexing) operation. The spread for 3 bit value is of
588+ // / 2 bytes (at worst bit alignment) so we move 2 bytes at the time.
589+ // / We care about creating a register as follows:
590+ // /
591+ // / | ~ Val AAA anywhere here || ~ Val BBB anywhere here |
592+ // /
593+ // / Since we have unused bytes in this register, we will also use them to fit the next
594+ // / values:
595+ // /
596+ // / | ~ Val AAA | ~ Val CCC || ~ Val BBB | ~ Val DDD |
597+ // /
598+ // / Resulting in the following swizzled register.
599+ // /
600+ // / |AAABBBCC|CDDDEEEF|AAABBBCC|CDDDEEEF||AAABBBCC|CDDDEEEF|CDDDEEEF|FFGGGHHH|
601+ // /
602+ // / Now we will apply shift and mask operations to properly align the values.
603+ // / Shifting has a different value for each uint32_t in the SIMD register.
604+ // / Note that because of the figure is little endian, right shift operator >> actually
605+ // / shifts the bits representation, to the left.
606+ // /
607+ // / | Align AAA with >> 0 || Align BBB with >> 3 |
608+ // / |AAABBBCC|CDDDEEEF|AAABBBCC|CDDDEEEF||BBBCCCDD|DEEEFCDD|DEEEFFFG|GGHHH000|
609+ // /
610+ // / Masking (bitewise AND) uses the same mask for all uint32_t
611+ // /
612+ // / |11100000000000000000000000000000000||11100000000000000000000000000000000|
613+ // / & |AAABBBCC|CDDDEEEF|AAABBBCC|CDDDEEEF||BBBCCCDD|DEEEFCDD|DEEEFFFG|GGHHH000|
614+ // / = |AAA00000|00000000|00000000|00000000||BBB00000|00000000|00000000|00000000|
615+ // /
616+ // / We have created a SIMD register with the first two unpacked values A and B that we can
617+ // / write to memory.
618+ // / Now we can do the same with C and D without having to go to another swizzle (doing
619+ // / multiple shifts per swizzle).
620+ // / The next value to unpack is E, which starts on bit 12 (not byte aligned).
621+ // / We therefore cannot end here and reapply the same kernel on the same input.
622+ // / Instead, we run another similar iteration to unpack E, F, G, H, this time taking into
623+ // / account that we have an extra offset of 12 bits (this will change all the constants
624+ // / used for swizzle and shift).
625+ // /
626+ // / Note that in this example, we could have swizzled more than two values in each slot.
627+ // / In practice, there may be situations where a more complex algorithm could fit more
628+ // / shifts per swizzles, but that tend to not be the case when the SIMD register size
629+ // / increases.
630+ // /
631+ // / @see KernelShape
632+ // / @see MediumKernelPlan
559633template <typename KerTraits, MediumKernelOptions kOptions >
560634struct MediumKernel {
561635 static constexpr auto kPlan = MediumKernelPlan<KerTraits, kOptions >::Build();
@@ -632,6 +706,9 @@ struct MediumKernel {
632706 * LargeKernel *
633707 *****************/
634708
709+ // / Compile time dimensions of a Large kernel.
710+ // /
711+ // / @see LargeKernel for explanation of the algorithm.
635712struct LargeKernelPlanSize {
636713 int reads_per_kernel_;
637714 int unpacked_per_kernel_;
@@ -656,6 +733,12 @@ struct LargeKernelPlanSize {
656733 constexpr int unpacked_per_kernel () const { return unpacked_per_kernel_; }
657734};
658735
736+ // / Compile time constants of a Large kernel.
737+ // /
738+ // / This contains most of the information about the large kernel algorithm stored
739+ // / as index offsets and other constants (shift amounts...).
740+ // /
741+ // / @see LargeKernel for an explanation of the algorithm.
659742template <typename KerTraits>
660743struct LargeKernelPlan {
661744 using Traits = KerTraits;
@@ -736,6 +819,75 @@ constexpr auto LargeKernelPlan<KerTraits>::Build() -> LargeKernelPlan<KerTraits>
736819 return plan;
737820}
738821
822+ // / Unpack a fixed number of packed integer using SIMD operations.
823+ // /
824+ // / This is only valid for large sized inputs, where packed values spread over more bytes
825+ // / than the unpacked integer size.
826+ // / All dimensions and constants are computed at compile time to reduce run-time
827+ // / computations and improve xsimd fallback possibilities (with
828+ // / ``xsimd::batch_constant``).
829+ // /
830+ // / Let us give an example of the algorithm for values packed over 13 bits, extracted to
831+ // / uint16_t, using a fictive 32 bits SIMD size.
832+ // / The input register is as follow, where 13 consecutive letters represent the 13 bits of
833+ // / a packed value in little endian representation.
834+ // /
835+ // / |AAAAAAAA|AAAAABBB||BBBBBBBB|BBCCCCCC|
836+ // /
837+ // / The algorithm generates SIMD registers of unpacked values ready to be written to
838+ // / memory. Here we can fit 32/16 = 2 unpacked values per output register.
839+ // / Since 13-bit values can spread over 3 bytes (at worst bit alignment) but we're
840+ // / unpacking to uint16_t (2 bytes).
841+ // /
842+ // / The core idea is to perform two separate swizzles: one for the "low" bits and one
843+ // / for the "high" bits of each value, then combine them with shifts and OR operations.
844+ // /
845+ // / | Low bits from A || Low bits from B |
846+ // / low: |AAAAAAAA|AAAAABBB||AAAAABBB|BBBBBBBB|
847+ // /
848+ // / | High bits frm A || High bits frm B |
849+ // / high: |AAAAABBB|BBBBBBBB||BBBBBBBB|BBCCCCCC|
850+ // /
851+ // / We then apply different shifts to align the values bits. The low values are
852+ // / right-shifted by the bit offset, and the high values are left-shifted to fill the gap.
853+ // / Note that because of the figure is little endian, right (>>) and left (<<) shift
854+ // / operators actually shifts the bits representation in the oppostite directions.
855+ // /
856+ // / | Align A w. >> 0 || Align B w. >> 5 |
857+ // / low: |AAAAAAAA|AAAAABBB||BBBBBBBBB|BB00000|
858+ // /
859+ // / | Align A w. << 8 || Align B w. << 3 |
860+ // / high: |00000000|AAAAABBB||000BBBBB|BBBBBCCC|
861+ // /
862+ // / We then proceed to merge both inputs. The idea is to mask out irrelevant bits from the
863+ // / high and low parts and merge with bitwise OR. However, in practice we can merge first
864+ // / and apply the mask only once afterwards (because we shifted zeros in). Below is the
865+ // / bitwise OR of both parts:
866+ // /
867+ // / |AAAAAAAA|AAAAABBB||BBBBBBBBB|BB00000|
868+ // / & |00000000|AAAAABBB||000BBBBB|BBBBBCCC|
869+ // / = |AAAAAAAA|AAAAABBB||BBBBBBBB|BBBBBCCC|
870+ // /
871+ // / Followed by a bitwise AND mask using the same values on both uint16_t:
872+ // /
873+ // / |AAAAAAAA|AAAAABBB||BBBBBBBB|BBBBBCCC|
874+ // / & |11111111|11111000||11111111|11111000|
875+ // / = |AAAAAAAA|AAAAA000||BBBBBBBB|BBBBB000|
876+ // /
877+ // / We have created a SIMD register with the two unpacked values A and B that we can
878+ // / write to memory.
879+ // / The next value to unpack is C, which starts on bit 26 (not byte aligned).
880+ // / We therefore cannot end here and reapply the same kernel on the same input.
881+ // / Instead, we run further similar iterations to unpack the next batch of values, this
882+ // / time taking into account that we have an extra bit offset (this will change all the
883+ // / constants used for swizzle and shift).
884+ // /
885+ // / Note that in this example, the value A did not require the low/high merge operations.
886+ // / However, because this is an SIMD algorithm we must apply the same operations to all
887+ // / values, but we are also not paying an extra cost for doing so.
888+ // /
889+ // / @see KernelShape
890+ // / @see LargeKernelPlan
739891template <typename KerTraits>
740892struct LargeKernel {
741893 static constexpr auto kPlan = LargeKernelPlan<KerTraits>::Build();
0 commit comments