Skip to content

Commit 237fc12

Browse files
committed
Kernel documentation
1 parent d761ed8 commit 237fc12

1 file changed

Lines changed: 159 additions & 7 deletions

File tree

cpp/src/arrow/util/bpacking_simd_kernel_internal.h

Lines changed: 159 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
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.
379380
struct 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.
386390
struct 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.
468478
template <typename KerTraits, MediumKernelOptions kOptions>
469479
struct 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
559633
template <typename KerTraits, MediumKernelOptions kOptions>
560634
struct 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.
635712
struct 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.
659742
template <typename KerTraits>
660743
struct 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
739891
template <typename KerTraits>
740892
struct LargeKernel {
741893
static constexpr auto kPlan = LargeKernelPlan<KerTraits>::Build();

0 commit comments

Comments
 (0)