From ca50d939215ce36cf4cc9e9c7b3ac5cfac552e17 Mon Sep 17 00:00:00 2001 From: Benjamin Kietzman Date: Tue, 11 May 2021 20:35:54 -0400 Subject: [PATCH 01/11] ARROW-12010: [C++][Compute] Improve performance of the hash table used in Grouper --- cpp/src/arrow/CMakeLists.txt | 64 +- .../arrow/compute/exec/doc/img/key_map_1.jpg | Bin 0 -> 53790 bytes .../arrow/compute/exec/doc/img/key_map_10.jpg | Bin 0 -> 69625 bytes .../arrow/compute/exec/doc/img/key_map_11.jpg | Bin 0 -> 60687 bytes .../arrow/compute/exec/doc/img/key_map_2.jpg | Bin 0 -> 43971 bytes .../arrow/compute/exec/doc/img/key_map_3.jpg | Bin 0 -> 59985 bytes .../arrow/compute/exec/doc/img/key_map_4.jpg | Bin 0 -> 56289 bytes .../arrow/compute/exec/doc/img/key_map_5.jpg | Bin 0 -> 61950 bytes .../arrow/compute/exec/doc/img/key_map_6.jpg | Bin 0 -> 43687 bytes .../arrow/compute/exec/doc/img/key_map_7.jpg | Bin 0 -> 43687 bytes .../arrow/compute/exec/doc/img/key_map_8.jpg | Bin 0 -> 48054 bytes .../arrow/compute/exec/doc/img/key_map_9.jpg | Bin 0 -> 52894 bytes cpp/src/arrow/compute/exec/doc/key_map.md | 204 +++ cpp/src/arrow/compute/exec/key_compare.cc | 266 +++ cpp/src/arrow/compute/exec/key_compare.h | 101 + .../arrow/compute/exec/key_compare_avx2.cc | 189 ++ cpp/src/arrow/compute/exec/key_encode.cc | 1625 +++++++++++++++++ cpp/src/arrow/compute/exec/key_encode.h | 627 +++++++ cpp/src/arrow/compute/exec/key_encode_avx2.cc | 545 ++++++ cpp/src/arrow/compute/exec/key_hash.cc | 247 +++ cpp/src/arrow/compute/exec/key_hash.h | 93 + cpp/src/arrow/compute/exec/key_hash_avx2.cc | 250 +++ cpp/src/arrow/compute/exec/key_map.cc | 490 +++++ cpp/src/arrow/compute/exec/key_map.h | 173 ++ cpp/src/arrow/compute/exec/key_map_avx2.cc | 406 ++++ cpp/src/arrow/compute/exec/util.cc | 237 +++ cpp/src/arrow/compute/exec/util.h | 168 ++ cpp/src/arrow/compute/exec/util_avx2.cc | 217 +++ .../arrow/compute/kernels/hash_aggregate.cc | 298 ++- .../compute/kernels/hash_aggregate_test.cc | 48 +- cpp/src/arrow/dataset/partition_test.cc | 25 +- 31 files changed, 6229 insertions(+), 44 deletions(-) create mode 100644 cpp/src/arrow/compute/exec/doc/img/key_map_1.jpg create mode 100644 cpp/src/arrow/compute/exec/doc/img/key_map_10.jpg create mode 100644 cpp/src/arrow/compute/exec/doc/img/key_map_11.jpg create mode 100644 cpp/src/arrow/compute/exec/doc/img/key_map_2.jpg create mode 100644 cpp/src/arrow/compute/exec/doc/img/key_map_3.jpg create mode 100644 cpp/src/arrow/compute/exec/doc/img/key_map_4.jpg create mode 100644 cpp/src/arrow/compute/exec/doc/img/key_map_5.jpg create mode 100644 cpp/src/arrow/compute/exec/doc/img/key_map_6.jpg create mode 100644 cpp/src/arrow/compute/exec/doc/img/key_map_7.jpg create mode 100644 cpp/src/arrow/compute/exec/doc/img/key_map_8.jpg create mode 100644 cpp/src/arrow/compute/exec/doc/img/key_map_9.jpg create mode 100644 cpp/src/arrow/compute/exec/doc/key_map.md create mode 100644 cpp/src/arrow/compute/exec/key_compare.cc create mode 100644 cpp/src/arrow/compute/exec/key_compare.h create mode 100644 cpp/src/arrow/compute/exec/key_compare_avx2.cc create mode 100644 cpp/src/arrow/compute/exec/key_encode.cc create mode 100644 cpp/src/arrow/compute/exec/key_encode.h create mode 100644 cpp/src/arrow/compute/exec/key_encode_avx2.cc create mode 100644 cpp/src/arrow/compute/exec/key_hash.cc create mode 100644 cpp/src/arrow/compute/exec/key_hash.h create mode 100644 cpp/src/arrow/compute/exec/key_hash_avx2.cc create mode 100644 cpp/src/arrow/compute/exec/key_map.cc create mode 100644 cpp/src/arrow/compute/exec/key_map.h create mode 100644 cpp/src/arrow/compute/exec/key_map_avx2.cc create mode 100644 cpp/src/arrow/compute/exec/util.cc create mode 100644 cpp/src/arrow/compute/exec/util.h create mode 100644 cpp/src/arrow/compute/exec/util_avx2.cc diff --git a/cpp/src/arrow/CMakeLists.txt b/cpp/src/arrow/CMakeLists.txt index bee14ae4ce3..1d832cc25a2 100644 --- a/cpp/src/arrow/CMakeLists.txt +++ b/cpp/src/arrow/CMakeLists.txt @@ -119,6 +119,22 @@ function(ADD_ARROW_BENCHMARK REL_TEST_NAME) ${ARG_UNPARSED_ARGUMENTS}) endfunction() +macro(append_avx2_src SRC) + if(ARROW_HAVE_RUNTIME_AVX2) + list(APPEND ARROW_SRCS ${SRC}) + set_source_files_properties(${SRC} PROPERTIES SKIP_PRECOMPILE_HEADERS ON) + set_source_files_properties(${SRC} PROPERTIES COMPILE_FLAGS ${ARROW_AVX2_FLAG}) + endif() +endmacro() + +macro(append_avx512_src SRC) + if(ARROW_HAVE_RUNTIME_AVX512) + list(APPEND ARROW_SRCS ${SRC}) + set_source_files_properties(${SRC} PROPERTIES SKIP_PRECOMPILE_HEADERS ON) + set_source_files_properties(${SRC} PROPERTIES COMPILE_FLAGS ${ARROW_AVX512_FLAG}) + endif() +endmacro() + set(ARROW_SRCS array/array_base.cc array/array_binary.cc @@ -215,19 +231,9 @@ set(ARROW_SRCS vendored/double-conversion/diy-fp.cc vendored/double-conversion/strtod.cc) -if(ARROW_HAVE_RUNTIME_AVX2) - list(APPEND ARROW_SRCS util/bpacking_avx2.cc) - set_source_files_properties(util/bpacking_avx2.cc PROPERTIES SKIP_PRECOMPILE_HEADERS ON) - set_source_files_properties(util/bpacking_avx2.cc PROPERTIES COMPILE_FLAGS - ${ARROW_AVX2_FLAG}) -endif() -if(ARROW_HAVE_RUNTIME_AVX512) - list(APPEND ARROW_SRCS util/bpacking_avx512.cc) - set_source_files_properties(util/bpacking_avx512.cc PROPERTIES SKIP_PRECOMPILE_HEADERS - ON) - set_source_files_properties(util/bpacking_avx512.cc PROPERTIES COMPILE_FLAGS - ${ARROW_AVX512_FLAG}) -endif() +append_avx2_src(util/bpacking_avx2.cc) +append_avx512_src(util/bpacking_avx512.cc) + if(ARROW_HAVE_NEON) list(APPEND ARROW_SRCS util/bpacking_neon.cc) endif() @@ -397,23 +403,21 @@ if(ARROW_COMPUTE) compute/kernels/vector_hash.cc compute/kernels/vector_nested.cc compute/kernels/vector_selection.cc - compute/kernels/vector_sort.cc) - - if(ARROW_HAVE_RUNTIME_AVX2) - list(APPEND ARROW_SRCS compute/kernels/aggregate_basic_avx2.cc) - set_source_files_properties(compute/kernels/aggregate_basic_avx2.cc PROPERTIES - SKIP_PRECOMPILE_HEADERS ON) - set_source_files_properties(compute/kernels/aggregate_basic_avx2.cc PROPERTIES - COMPILE_FLAGS ${ARROW_AVX2_FLAG}) - endif() - - if(ARROW_HAVE_RUNTIME_AVX512) - list(APPEND ARROW_SRCS compute/kernels/aggregate_basic_avx512.cc) - set_source_files_properties(compute/kernels/aggregate_basic_avx512.cc PROPERTIES - SKIP_PRECOMPILE_HEADERS ON) - set_source_files_properties(compute/kernels/aggregate_basic_avx512.cc PROPERTIES - COMPILE_FLAGS ${ARROW_AVX512_FLAG}) - endif() + compute/kernels/vector_sort.cc + compute/exec/key_hash.cc + compute/exec/key_map.cc + compute/exec/key_compare.cc + compute/exec/key_encode.cc + compute/exec/util.cc) + + append_avx2_src(compute/kernels/aggregate_basic_avx2.cc) + append_avx512_src(compute/kernels/aggregate_basic_avx512.cc) + + append_avx2_src(compute/exec/key_hash_avx2.cc) + append_avx2_src(compute/exec/key_map_avx2.cc) + append_avx2_src(compute/exec/key_compare_avx2.cc) + append_avx2_src(compute/exec/key_encode_avx2.cc) + append_avx2_src(compute/exec/util_avx2.cc) list(APPEND ARROW_TESTING_SRCS compute/exec/test_util.cc) endif() diff --git a/cpp/src/arrow/compute/exec/doc/img/key_map_1.jpg b/cpp/src/arrow/compute/exec/doc/img/key_map_1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..814ad8a69f60bf71d873a34fbc0e3854b7317d0c GIT binary patch literal 53790 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 I>z9H50`1O7cmMzZ literal 0 HcmV?d00001 diff --git a/cpp/src/arrow/compute/exec/doc/img/key_map_10.jpg b/cpp/src/arrow/compute/exec/doc/img/key_map_10.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7a75c96dfc59c0877532eaffd6d47976e3ff59ac GIT binary patch literal 69625 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%0Dsx_WGywKtGyTQYQ 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 G|4jgz)nCy7 literal 0 HcmV?d00001 diff --git a/cpp/src/arrow/compute/exec/doc/img/key_map_2.jpg b/cpp/src/arrow/compute/exec/doc/img/key_map_2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4484c57a81d1860a2befcbcd87024ae69c58710d GIT binary patch literal 43971 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 P{bNo3(=x!m)b)P=(OM2p literal 0 HcmV?d00001 diff --git a/cpp/src/arrow/compute/exec/doc/img/key_map_4.jpg b/cpp/src/arrow/compute/exec/doc/img/key_map_4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f026aebe9a26ae173301ba880df4e5e71897060d GIT binary patch literal 56289 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 literal 0 HcmV?d00001 diff --git a/cpp/src/arrow/compute/exec/doc/img/key_map_5.jpg b/cpp/src/arrow/compute/exec/doc/img/key_map_5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8e1981b6571f9e58e02ec1615096ec64329eacde GIT binary patch literal 61950 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�MmsyK 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 Qnt#j8{BO!l)Y;en0E*WRl>h($ literal 0 HcmV?d00001 diff --git a/cpp/src/arrow/compute/exec/doc/img/key_map_7.jpg b/cpp/src/arrow/compute/exec/doc/img/key_map_7.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7552d5af6afc064d6fb9b2c4a332072217b2d2fb GIT binary patch literal 43687 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 o3ETbgKH+B~sjz!LWWGzZUQ7T)qFB$vLV#$`> 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 is the number of slots and also the maximum number of inserted keys and hence (N + 3) is the number of bits required to store a key id. We will refer to N as the **size of the hash table**. + +Index of a block within an array will be called **block id**, and similarly index of a slot will be **slot id**. Sometimes we will focus on a single block and refer to slots that belong to it by using a **local slot id**, which is an index from 0 to 7. + +Every slot can either be **empty** or store data related to a single inserted key. There are three pieces of information stored inside a slot: +- status byte, +- key id, +- key hash. + +Status byte, as the name suggests, stores 8 bits. The highest bit indicates if the slot is empty (the highest bit is set) or corresponds to one of inserted keys (the highest bit is zero). The remaining 7 bits contain 7 bits of key hash that we call a **stamp**. The stamp is used to eliminate some false positives when searching for a matching key for a given input. Slot also stores **key id**, which is a non-negative integer smaller than the number of inserted keys, that is used as a reference to the actual inserted key. The last piece of information related to an inserted key is its **hash** value. We store hashes for all keys, so that they never need to be re-computed. That greatly simplifies some operations, like resizing of a hash table, that may not even need to look at the keys at all. For an empty slot, the status byte is 0x80, key id is zero and the hash is not used and can be set to any number. + +A single block contains 8 slots and can be viewed as a micro-stack of up to 8 inserted keys. When the first key is inserted into an empty block, it will occupy a slot with local id 0. The second inserted key will go into slot number 1 and so on. We use N highest bits of hash to get an index of a **start block**, when searching for a match or an empty slot to insert a previously not seen key when that is the case. If the start block contains any empty slots, then the search for either a match or place to insert a key will end at that block. We will call such a block an **open block**. A block that is not open is a full block. In the case of full block, the input key related search may continue in the next block module the number of blocks. If the key is not inserted into its start block, we will refer to it as an **overflow** entry, other entries being **non-overflow**. Overflow entries are slower to process, since they require visiting more than one block, so we want to keep their percentage low. This is done by choosing the right **load factor** (percentage of occupied slots in the hash table) at which the hash table gets resized and the number of blocks gets doubled. By tuning this value we can control the probability of encountering an overflow entry. + +The most interesting part of each block is the set of status bytes of its slots, which is simply a single 64-bit word. The implementation of efficient searches across these bytes during lookups require using either leading zero count or trailing zero count intrinsic. Since there are cases when only the first one is available, in order to take advantage of it, we order the bytes in the 64-bit status word so that the first slot within a block uses the highest byte and the last one uses the lowest byte (slots are in reversed bytes order). The diagram below shows how the information about slots is stored within a 64-bit status word: + +![alt text](img/key_map_3.jpg) + +Each status byte has a 7-bit fragment of hash value - a **stamp** - and an empty slot bit. Empty slots have status byte equal to 0x80 - the highest bit is set to 1 to indicate an empty slot and the lowest bits, which are used by a stamp, are set to zero. + +The diagram below shows which bits of hash value are used by hash table: + +![alt text](img/key_map_4.jpg) + +If a hash table has 2N blocks, then we use N highest bits of a hash to select a start block when searching for a match. The next 7 bits are used as a stamp. Using the highest bits to pick a start block means that a range of hash values can be easily mapped to a range of block ids of start blocks for hashes in that range. This is useful when resizing a hash table or merging two hash tables together. + +### Interleaving status bytes and key ids + +Status bytes and key ids for all slots are stored in a single array of bytes. They are first grouped by 8 into blocks, then each block of status bytes is interleaved with a corresponding block of key ids. Finally key ids are represented using the smallest possible number of bits and bit-packed (bits representing each next key id start right after the last bit of the previous key id). Note that regardless of the chosen number of bits, a block of bit-packed key ids (that is 8 of them) will start and end on the byte boundary. + +The diagram below shows the organization of bytes and bits of a single block in interleaved array: +![alt text](img/key_map_5.jpg) + +From the size of the hash table we can derive the number K of bits needed in the worst case to encode any key id. K is equal to the number of bits needed to represent slot id (number of keys is not greater than the number of slots and any key id is strictly less than the number of keys), which for a hash table of size N (N blocks) equals (N+3). To simplify bit packing and unpacking and avoid handling of special cases, we will round up K to full bytes for K > 24 bits. + +Status bytes are stored in a single 64-bit word in reverse byte order (the last byte corresponds to the slot with local id 0). On the other hand key ids are stored in the normal order (the order of slot ids). + +Since both status byte and key id for a given slot are stored in the same array close to each other, we can expect that most of the lookups will read only one CPU cache-line from memory inside Swiss table code (then at least another one outside Swiss table to access the bytes of the key for the purpose of comparison). Even if we hit an overflow entry, it is still likely to reside on the same cache-line as the start block data. Hash values, which are stored separately from status byte and key id, are only used when resizing and do not impact the lookups outside these events. + +> Improvement to consider: +> In addition to the Swiss table data, we need to store an array of inserted keys, one for each key id. If keys are of fixed length, then the address of the bytes of the key can be calculated by multiplying key id by the common length of the key. If keys are of varying length, then there will be an additional array with an offset of each key within the array of concatenated bytes of keys. That means that any key comparison during lookup will involve 3 arrays: one to get key id, one to get key offset and final one with bytes of the key. This could be reduced to 2 array lookups if we stored key offset instead of key id interleaved with slot status bytes. Offset indexed by key id and stored in its own array becomes offset indexed by slot id and stored interleaved with slot status bytes. At the same time key id indexed by slot id and interleaved with slot status bytes before becomes key id referenced using offset and stored with key bytes. There may be a slight increase in the total size of memory needed by the hash table, equal to the difference in the number of bits used to store offset and those used to store key id, multiplied by the number of slots, but that should be a small fraction of the total size. + +### 32-bit hash vs 64-bit hash + +Currently we use 32-bit hash values in Swiss table code and 32-bit integers as key ids. For the robust implementation, sooner or later we will need to support 64-bit hash and 64-bit key ids. When we use 32-bit hash, it means that we run out of hash bits when hash table size N is greater than 25 (25 bits of hash needed to select a block and 7 bits needed to generate a stamp byte reach 32 total bits). When the number of inserted keys exceeds the maximal number of keys stored in a hash table of size 25 (which is at least 224), the chance of false positives during lookups will start quickly growing. 32-bit hash should not be used with more than about 16 million inserted keys. + +### Low memory footprint and low chance of hash collisions + +Swiss table is a good choice of a hash table for modern hardware, because it combines lookups that can take advantage of special CPU instructions with space efficiency and low chance of hash collisions. + +Space efficiency is important for performance, because the cost of random array accesses, often dominating the lookup cost for larger hash tables, increases with the size of the arrays. This happens due to limited space of CPU caches. Let us look at what is the amortized additional storage cost for a key in a hash table apart from the essential cost of storing data of all those keys. Furthermore, we can skip the storage of hash values, since these are only used during infrequent hash table resize operations (should not have a big impact on CPU cache usage in normal cases). + +Half full hash table of size N will use 2 status bytes per inserted key (because for every filled slot there is one empty slot) and 2\*(N+3) bits for key id (again, one for the occupied slot and one for the empty). For N = 16 for instance this is slightly under 7 bytes per inserted key. + +Swiss table also has a low probability of false positives leading to wasted key comparisons. Here is some rationale behind why this should be the case. Hash table of size N can contain up to 2N+3 keys. Search for a match involves (N + 7) hash bits: N to select a start block and 7 to use as a stamp. There are always at least 16 times more combinations of used hash bits than there are keys in the hash table (32 times more if the hash table is half full). These numbers mean that the probability of false positives resulting from a search for a matching slot should be low. That corresponds to an expected number of comparisons per lookup being close to 1 for keys already present and 0 for new keys. + +## Lookup + +Lookup-or-insert operation, given a hash of a key, finds a list of candidate slots with corresponding keys that are likely to be equal to the input key. The list may be empty, which means that the key does not exist yet in the hash table. If it is not empty, then the callback function for key comparison is called for each next candidate to verify that there is indeed a match. False positives get rejected and we end up either finding an actual match or an empty slot, which means that the key is new to the hash table. New keys get assigned next available integers as key ids, and are appended to the set of keys stored in the hash table. As a result of inserting new keys to the hash table, the density of occupied slots may reach an upper limit, at which point the hash table will be resized and will afterwards have twice as many slots. That is in summary lookup-or-insert functionality, but the actual implementation is a bit more involved, because of vectorization of the processing and various optimizations for common cases. + +### Search within a single block + +There are three possible cases that can occur when searching for a match for a given key (that is, for a given stamp of a key) within a single block, illustrated below. + + 1. There is a matching stamp in the block of status bytes: + +![alt text](img/key_map_6.jpg) + + 2. There is no matching stamp in the block, but there is an empty slot in the block: + +![alt text](img/key_map_7.jpg) + + 3. There is no matching stamp in the block and the block is full (there are no empty slots left): + +![alt text](img/key_map_8.jpg) + +64-bit arithmetic can be used to search for a matching slot within the entire single block at once, without iterating over all slots in it. Following is an example of a sequence of steps to find the first status byte for a given stamp, returning the first empty slot on miss if the block is not full or 8 (one past maximum local slot id) otherwise. + +Following is a sketch of the possible steps to execute when searching for the matching stamp in a single block. + +*Example will use input stamp 0x5E and a 64-bit status bytes word with one empty slot: +0x 4B17 5E3A 5E2B 1180*. + +1. [1 instruction] Replicate stamp to all bytes by multiplying it by 0x 0101 0101 0101 0101. + + *We obtain: 0x 5E5E 5E5E 5E5E 5E5E.* + +2. [1 instruction] XOR replicated stamp with status bytes word. Bytes corresponding to a matching stamp will be 0, bytes corresponding to empty slots will have a value between 128 and 255, bytes corresponding to non-matching non-empty slots will have a value between 1 and 127. + + *We obtain: 0x 1549 0064 0075 4FDE.* + +3. [2 instructions] In the next step we want to have information about a match in the highest bit of each byte. We can ignore here empty slot bytes, because they will be taken care of at a later step. Set the highest bit in each byte (OR with 0x 8080 8080 8080 8080) and then subtract 1 from each byte (subtract 0x 0101 0101 0101 0101 from 64-bit word). Now if a byte corresponds to a non-empty slot then the highest bit 0 indicates a match and 1 indicates a miss. + + *We obtain: 0x 95C9 80E4 80F5 CFDE, + then 0x 94C8 7FE3 7FF4 CEDD.* + +4. [3 instructions] In the next step we want to obtain in each byte one of two values: 0x80 if it is either an empty slot or a match, 0x00 otherwise. We do it in three steps: NOT the result of the previous step to change the meaning of the highest bit; OR with the original status word to set highest bit in a byte to 1 for empty slots; mask out everything other than the highest bits in all bytes (AND with 0x 8080 8080 8080 8080). + + *We obtain: 6B37 801C 800B 3122, + then 6B37 DE3E DE2B 31A2, + finally 0x0000 8000 8000 0080.* + +5. [2 instructions] Finally, use leading zero bits count and divide it by 8 to find an index of the last byte that corresponds either to a match or an empty slot. If the leading zero count intrinsic returns 64 for a 64-bit input zero, then after dividing by 8 we will also get the desired answer in case of a full block without any matches. + + *We obtain: 16, + then 2 (index of the first slot within the block that matches the stamp).* + +If SIMD instructions with 64-bit lanes are available, multiple single block searches for different keys can be executed together. For instance AVX2 instruction set allows to process quadruplets of 64-bit values in a single instruction, four searches at once. + +### Complete search potentially across multiple blocks + +Full implementation of a search for a matching key may involve visiting multiple blocks beginning with the start block selected based on the hash of the key. We move to the next block modulo the number of blocks, whenever we do not find a match in the current block and the current block is full. The search may also involve visiting one or more slots in each block. Visiting in this case means calling a comparison callback to verify the match whenever a slot with a matching stamp is encountered. Eventually the search stops when either: +- the matching key is found in one of the slots matching the stamp, or + +- an empty slot is reached. This is illustrated in the diagram below: +![alt text](img/key_map_9.jpg) + + +### Optimistic processing with two passes + +Hash table lookups may have high cost in the pessimistic case, when we encounter cases of hash collisions and full blocks that lead to visiting further blocks. In the majority of cases we can expect an optimistic situation - the start block is not full, so we will only visit this one block, and all stamps in the block are different, so we will need at most one comparison to find a match. We can expect about 90% of the key lookups for an existing key to go through the optimistic path of processing. For that reason it pays off to optimize especially for this 90% of inputs. + +Lookups in Swiss table are split into two passes over an input batch of keys. The **first pass: fast-path lookup** , is a highly optimized, vectorized, SIMD-friendly, branch-free code that fully handles optimistic cases. The **second pass: slow-path lookup** , is normally executed only for the selection of inputs that have not been finished in the first pass, although it can also be called directly on all of the inputs, skipping fast-path lookup. It handles all special cases and inserts but in order to be robust it is not as efficient as fast-path. Slow-path lookup does not need to repeat the work done in fast-path lookup - it can use the state reached at the end of fast-path lookup as a starting point. + +Fast-path lookup implements search only for the first stamp match and only within the start block. It only makes sense when we already have at least one key inserted into the hash table, since it does not handle inserts. It takes a vector of key hashes as an input and based on it outputs three pieces of information for each key: + +- Key id corresponding to the slot in which a matching stamp was found. Any valid key id if a matching stamp was not found. +- A flag indicating if a match was found or not. +- Slot id of a slot from which slow-path should pick up the search if the first match was either not found or it turns out to be false positive after evaluating key comparison. + +> Improvement to consider: +> precomputing 1st pass lookup results. +> +> If the hash table is small, the number of inserted keys is small, we could further simplify and speed-up the first pass by storing in a lookup table pre-computed results for all combinations of hash bits. Let us consider the case of Swiss table of size 5 that has 256 slots and up to 128 inserted keys. Only 12 bits of hash are used by lookup in that case: 5 to select a block, 7 to create a stamp. For all 212 combinations of those bits we could keep the result of first pass lookup in an array. Key id and a match indicating flag can use one byte: 7 bits for key id and 1 bit for the flag. Note that slot id is only needed if we go into 2nd pass lookup, so it can be stored separately and likely only accessed by a small subset of keys. Fast-path lookup becomes almost a single fetch of result from a 4KB array. Lookup arrays used to implement this need to be kept in sync with the main copy of data about slots, which requires extra care during inserts. Since the number of entries in lookup arrays is much higher than the number of slots, this technique only makes sense for small hash tables. + +### Dense comparisons + +If there is at least one key inserted into a hash table, then every slot contains a key id value that corresponds to some actual key that can be used in comparison. That is because empty slots are initialized with 0 as their key id. After the fast-path lookup we get a match-found flag for each input. If it is set, then we need to run a comparison of the input key with the key in the hash table identified by key id returned by fast-path code. The comparison will verify that there is a true match between the keys. We only need to do this for a subset of inputs that have a match candidate, but since we have key id values corresponding to some real key for all inputs, we may as well execute comparisons on all inputs unconditionally. If the majority (e.g. more than 80%) of the keys have a match candidate, the cost of evaluating comparison for the remaining fraction of keys but without filtering may actually be cheaper than the cost of running evaluation only for required keys while referencing filter information. This can be seen as a variant of general preconditioning techniques used to avoid diverging conditional branches in the code. It may be used, based on some heuristic, to verify matches reported by fast-path lookups and is referred to as **dense comparisons**. + +## Resizing + +New hash table is initialized as empty and has only a single block with a space for only a few key entries. Doubling of the hash table size becomes necessary as more keys get inserted. It is invoked during the 2nd pass of the lookups, which also handles inserts. It happens immediately after the number of inserted keys reaches a specific upper limit decided based on a current size of the hash table. There may still be unprocessed entries from the input mini-batch after resizing, so the 2nd pass of the lookup is restarted right after, with the bigger hash table and the remaining subset of unprocessed entries. + +Current policy, that should work reasonably well, is to resize a small hash table (up to 8KB) when it is 50% full. Larger hash tables are resized when 75% full. We want to keep size in memory as small as possible, while maintaining a low probability of blocks becoming full. + +When discussing resizing we will be talking about **resize source** and **resize target** tables. The diagram below shows how the same hash bits are interpreted differently by the source and the target. + +![alt text](img/key_map_10.jpg) + +For a given hash, if a start block id was L in the source table, it will be either (2\*L+0) or (2\*L+1) in the target table. Based on that we can expect data access locality when migrating the data between the tables. + +Resizing is cheap also thanks to the fact that hash values for keys in the hash table are kept together with other slot data and do not need to be recomputed. That means that resizing procedure does not ever need to access the actual bytes of the key. + +### 1st pass + +Based on the hash value for a given slot we can tell whether this slot contains an overflow or non-overflow entry. In the first pass we go over all source slots in sequence, filter out overflow entries and move to the target table all other entries. Non-overflow entries from a block L will be distributed between blocks (2\*L+0) and (2\*L+1) of the target table. None of these target blocks can overflow, since they will be accommodating at most 8 input entries during this pass. + +For every non-overflow entry, the highest bit of a stamp in the source slot decides whether it will go to the left or to the right target block. It is further possible to avoid any conditional branches in this partitioning code, so that the result is friendly to the CPU execution pipeline. + +![alt text](img/key_map_11.jpg) + + +### 2nd pass + +In the second pass of resizing, we scan all source slots again, this time focusing only on the overflow entries that were all skipped in the 1st pass. We simply reinsert them in the target table using generic insertion code with one exception. Since we know that all the source keys are different, there is no need to search for a matching stamp or run key comparisons (or look at the key values). We just need to find the first open block beginning with the start block in the target table and use its first empty slot as the insert destination. + +We expect overflow entries to be rare and therefore the relative cost of that pass should stay low. + diff --git a/cpp/src/arrow/compute/exec/key_compare.cc b/cpp/src/arrow/compute/exec/key_compare.cc new file mode 100644 index 00000000000..381f25cf36a --- /dev/null +++ b/cpp/src/arrow/compute/exec/key_compare.cc @@ -0,0 +1,266 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#include "arrow/compute/exec/key_compare.h" + +#include +#include + +#include "arrow/compute/exec/util.h" + +namespace arrow { +namespace compute { + +void KeyCompare::CompareRows(uint32_t num_rows_to_compare, + const uint16_t* sel_left_maybe_null, + const uint32_t* left_to_right_map, + KeyEncoder::KeyEncoderContext* ctx, uint32_t* out_num_rows, + uint16_t* out_sel_left_maybe_same, + const KeyEncoder::KeyRowArray& rows_left, + const KeyEncoder::KeyRowArray& rows_right) { + ARROW_DCHECK(rows_left.metadata().is_compatible(rows_right.metadata())); + + if (num_rows_to_compare == 0) { + *out_num_rows = 0; + return; + } + + // Allocate temporary byte and bit vectors + auto bytevector_holder = + util::TempVectorHolder(ctx->stack, num_rows_to_compare); + auto bitvector_holder = + util::TempVectorHolder(ctx->stack, num_rows_to_compare); + + uint8_t* match_bytevector = bytevector_holder.mutable_data(); + uint8_t* match_bitvector = bitvector_holder.mutable_data(); + + // All comparison functions called here will update match byte vector + // (AND it with comparison result) instead of overwriting it. + memset(match_bytevector, 0xff, num_rows_to_compare); + + if (rows_left.metadata().is_fixed_length) { + CompareFixedLength(num_rows_to_compare, sel_left_maybe_null, left_to_right_map, + match_bytevector, ctx, rows_left.metadata().fixed_length, + rows_left.data(1), rows_right.data(1)); + } else { + CompareVaryingLength(num_rows_to_compare, sel_left_maybe_null, left_to_right_map, + match_bytevector, ctx, rows_left.data(2), rows_right.data(2), + rows_left.offsets(), rows_right.offsets()); + } + + // CompareFixedLength can be used to compare nulls as well + bool nulls_present = rows_left.has_any_nulls(ctx) || rows_right.has_any_nulls(ctx); + if (nulls_present) { + CompareFixedLength(num_rows_to_compare, sel_left_maybe_null, left_to_right_map, + match_bytevector, ctx, + rows_left.metadata().null_masks_bytes_per_row, + rows_left.null_masks(), rows_right.null_masks()); + } + + util::BitUtil::bytes_to_bits(ctx->cpu_info, num_rows_to_compare, match_bytevector, + match_bitvector); + if (sel_left_maybe_null) { + int out_num_rows_int; + util::BitUtil::bits_filter_indexes(0, ctx->cpu_info, num_rows_to_compare, + match_bitvector, sel_left_maybe_null, + &out_num_rows_int, out_sel_left_maybe_same); + *out_num_rows = out_num_rows_int; + } else { + int out_num_rows_int; + util::BitUtil::bits_to_indexes(0, ctx->cpu_info, num_rows_to_compare, match_bitvector, + &out_num_rows_int, out_sel_left_maybe_same); + *out_num_rows = out_num_rows_int; + } +} + +void KeyCompare::CompareFixedLength(uint32_t num_rows_to_compare, + const uint16_t* sel_left_maybe_null, + const uint32_t* left_to_right_map, + uint8_t* match_bytevector, + KeyEncoder::KeyEncoderContext* ctx, + uint32_t fixed_length, const uint8_t* rows_left, + const uint8_t* rows_right) { + bool use_selection = (sel_left_maybe_null != nullptr); + + uint32_t num_rows_already_processed = 0; + +#if defined(ARROW_HAVE_AVX2) + if (ctx->has_avx2() && !use_selection) { + // Choose between up-to-8B length, up-to-16B length and any size versions + if (fixed_length <= 8) { + num_rows_already_processed = CompareFixedLength_UpTo8B_avx2( + num_rows_to_compare, left_to_right_map, match_bytevector, fixed_length, + rows_left, rows_right); + } else if (fixed_length <= 16) { + num_rows_already_processed = CompareFixedLength_UpTo16B_avx2( + num_rows_to_compare, left_to_right_map, match_bytevector, fixed_length, + rows_left, rows_right); + } else { + num_rows_already_processed = + CompareFixedLength_avx2(num_rows_to_compare, left_to_right_map, + match_bytevector, fixed_length, rows_left, rows_right); + } + } +#endif + + typedef void (*CompareFixedLengthImp_t)(uint32_t, uint32_t, const uint16_t*, + const uint32_t*, uint8_t*, uint32_t, + const uint8_t*, const uint8_t*); + static const CompareFixedLengthImp_t CompareFixedLengthImp_fn[] = { + CompareFixedLengthImp, CompareFixedLengthImp, + CompareFixedLengthImp, CompareFixedLengthImp, + CompareFixedLengthImp, CompareFixedLengthImp}; + int dispatch_const = (use_selection ? 3 : 0) + + ((fixed_length <= 8) ? 0 : ((fixed_length <= 16) ? 1 : 2)); + CompareFixedLengthImp_fn[dispatch_const]( + num_rows_already_processed, num_rows_to_compare, sel_left_maybe_null, + left_to_right_map, match_bytevector, fixed_length, rows_left, rows_right); +} + +template +void KeyCompare::CompareFixedLengthImp(uint32_t num_rows_already_processed, + uint32_t num_rows, + const uint16_t* sel_left_maybe_null, + const uint32_t* left_to_right_map, + uint8_t* match_bytevector, uint32_t length, + const uint8_t* rows_left, + const uint8_t* rows_right) { + // Key length (for encoded key) has to be non-zero + ARROW_DCHECK(length > 0); + + // Non-zero length guarantees no underflow + int32_t num_loops_less_one = (static_cast(length) + 7) / 8 - 1; + + // Length remaining in last loop can only be zero for input length equal to zero + uint32_t length_remaining_last_loop = length - num_loops_less_one * 8; + uint64_t tail_mask = (~0ULL) >> (8 * (8 - length_remaining_last_loop)); + + for (uint32_t id_input = num_rows_already_processed; id_input < num_rows; ++id_input) { + uint32_t irow_left = use_selection ? sel_left_maybe_null[id_input] : id_input; + uint32_t irow_right = left_to_right_map[irow_left]; + uint32_t begin_left = length * irow_left; + uint32_t begin_right = length * irow_right; + const uint64_t* key_left_ptr = + reinterpret_cast(rows_left + begin_left); + const uint64_t* key_right_ptr = + reinterpret_cast(rows_right + begin_right); + uint64_t result_or = 0ULL; + int32_t istripe = 0; + + // Specializations for keys up to 8 bytes and between 9 and 16 bytes to + // avoid internal loop over words in the value for short ones. + // + // Template argument 0 means arbitrarily many 64-bit words, + // 1 means up to 1 and 2 means up to 2. + // + if (num_64bit_words == 0) { + for (; istripe < num_loops_less_one; ++istripe) { + uint64_t key_left = key_left_ptr[istripe]; + uint64_t key_right = key_right_ptr[istripe]; + result_or |= (key_left ^ key_right); + } + } else if (num_64bit_words == 2) { + uint64_t key_left = key_left_ptr[istripe]; + uint64_t key_right = key_right_ptr[istripe]; + result_or |= (key_left ^ key_right); + ++istripe; + } + + uint64_t key_left = key_left_ptr[istripe]; + uint64_t key_right = key_right_ptr[istripe]; + result_or |= (tail_mask & (key_left ^ key_right)); + + int result = (result_or == 0 ? 0xff : 0); + match_bytevector[id_input] &= result; + } +} + +void KeyCompare::CompareVaryingLength(uint32_t num_rows_to_compare, + const uint16_t* sel_left_maybe_null, + const uint32_t* left_to_right_map, + uint8_t* match_bytevector, + KeyEncoder::KeyEncoderContext* ctx, + const uint8_t* rows_left, const uint8_t* rows_right, + const uint32_t* offsets_left, + const uint32_t* offsets_right) { + bool use_selection = (sel_left_maybe_null != nullptr); + +#if defined(ARROW_HAVE_AVX2) + if (ctx->has_avx2() && !use_selection) { + CompareVaryingLength_avx2(num_rows_to_compare, left_to_right_map, match_bytevector, + rows_left, rows_right, offsets_left, offsets_right); + } else { +#endif + if (use_selection) { + CompareVaryingLengthImp(num_rows_to_compare, sel_left_maybe_null, + left_to_right_map, match_bytevector, rows_left, + rows_right, offsets_left, offsets_right); + } else { + CompareVaryingLengthImp(num_rows_to_compare, sel_left_maybe_null, + left_to_right_map, match_bytevector, rows_left, + rows_right, offsets_left, offsets_right); + } +#if defined(ARROW_HAVE_AVX2) + } +#endif +} + +template +void KeyCompare::CompareVaryingLengthImp( + uint32_t num_rows, const uint16_t* sel_left_maybe_null, + const uint32_t* left_to_right_map, uint8_t* match_bytevector, + const uint8_t* rows_left, const uint8_t* rows_right, const uint32_t* offsets_left, + const uint32_t* offsets_right) { + static const uint64_t tail_masks[] = { + 0x0000000000000000ULL, 0x00000000000000ffULL, 0x000000000000ffffULL, + 0x0000000000ffffffULL, 0x00000000ffffffffULL, 0x000000ffffffffffULL, + 0x0000ffffffffffffULL, 0x00ffffffffffffffULL, 0xffffffffffffffffULL}; + for (uint32_t i = 0; i < num_rows; ++i) { + uint32_t irow_left = use_selection ? sel_left_maybe_null[i] : i; + uint32_t irow_right = left_to_right_map[irow_left]; + uint32_t begin_left = offsets_left[irow_left]; + uint32_t begin_right = offsets_right[irow_right]; + uint32_t length_left = offsets_left[irow_left + 1] - begin_left; + uint32_t length_right = offsets_right[irow_right + 1] - begin_right; + uint32_t length = std::min(length_left, length_right); + const uint64_t* key_left_ptr = + reinterpret_cast(rows_left + begin_left); + const uint64_t* key_right_ptr = + reinterpret_cast(rows_right + begin_right); + uint64_t result_or = 0; + int32_t istripe; + // length can be zero + for (istripe = 0; istripe < (static_cast(length) + 7) / 8 - 1; ++istripe) { + uint64_t key_left = key_left_ptr[istripe]; + uint64_t key_right = key_right_ptr[istripe]; + result_or |= (key_left ^ key_right); + } + + uint32_t length_remaining = length - static_cast(istripe) * 8; + uint64_t tail_mask = tail_masks[length_remaining]; + + uint64_t key_left = key_left_ptr[istripe]; + uint64_t key_right = key_right_ptr[istripe]; + result_or |= (tail_mask & (key_left ^ key_right)); + + int result = (result_or == 0 ? 0xff : 0); + match_bytevector[i] &= result; + } +} + +} // namespace compute +} // namespace arrow diff --git a/cpp/src/arrow/compute/exec/key_compare.h b/cpp/src/arrow/compute/exec/key_compare.h new file mode 100644 index 00000000000..1dffabb884b --- /dev/null +++ b/cpp/src/arrow/compute/exec/key_compare.h @@ -0,0 +1,101 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#pragma once + +#include + +#include "arrow/compute/exec/key_encode.h" +#include "arrow/compute/exec/util.h" +#include "arrow/memory_pool.h" +#include "arrow/result.h" +#include "arrow/status.h" + +namespace arrow { +namespace compute { + +class KeyCompare { + public: + // Returns a single 16-bit selection vector of rows that failed comparison. + // If there is input selection on the left, the resulting selection is a filtered image + // of input selection. + static void CompareRows(uint32_t num_rows_to_compare, + const uint16_t* sel_left_maybe_null, + const uint32_t* left_to_right_map, + KeyEncoder::KeyEncoderContext* ctx, uint32_t* out_num_rows, + uint16_t* out_sel_left_maybe_same, + const KeyEncoder::KeyRowArray& rows_left, + const KeyEncoder::KeyRowArray& rows_right); + + private: + static void CompareFixedLength(uint32_t num_rows_to_compare, + const uint16_t* sel_left_maybe_null, + const uint32_t* left_to_right_map, + uint8_t* match_bytevector, + KeyEncoder::KeyEncoderContext* ctx, + uint32_t fixed_length, const uint8_t* rows_left, + const uint8_t* rows_right); + static void CompareVaryingLength(uint32_t num_rows_to_compare, + const uint16_t* sel_left_maybe_null, + const uint32_t* left_to_right_map, + uint8_t* match_bytevector, + KeyEncoder::KeyEncoderContext* ctx, + const uint8_t* rows_left, const uint8_t* rows_right, + const uint32_t* offsets_left, + const uint32_t* offsets_right); + + // Second template argument is 0, 1 or 2. + // 0 means arbitrarily many 64-bit words, 1 means up to 1 and 2 means up to 2. + template + static void CompareFixedLengthImp(uint32_t num_rows_already_processed, + uint32_t num_rows, + const uint16_t* sel_left_maybe_null, + const uint32_t* left_to_right_map, + uint8_t* match_bytevector, uint32_t length, + const uint8_t* rows_left, const uint8_t* rows_right); + template + static void CompareVaryingLengthImp(uint32_t num_rows, + const uint16_t* sel_left_maybe_null, + const uint32_t* left_to_right_map, + uint8_t* match_bytevector, const uint8_t* rows_left, + const uint8_t* rows_right, + const uint32_t* offsets_left, + const uint32_t* offsets_right); + +#if defined(ARROW_HAVE_AVX2) + + static uint32_t CompareFixedLength_UpTo8B_avx2( + uint32_t num_rows, const uint32_t* left_to_right_map, uint8_t* match_bytevector, + uint32_t length, const uint8_t* rows_left, const uint8_t* rows_right); + static uint32_t CompareFixedLength_UpTo16B_avx2( + uint32_t num_rows, const uint32_t* left_to_right_map, uint8_t* match_bytevector, + uint32_t length, const uint8_t* rows_left, const uint8_t* rows_right); + static uint32_t CompareFixedLength_avx2(uint32_t num_rows, + const uint32_t* left_to_right_map, + uint8_t* match_bytevector, uint32_t length, + const uint8_t* rows_left, + const uint8_t* rows_right); + static void CompareVaryingLength_avx2( + uint32_t num_rows, const uint32_t* left_to_right_map, uint8_t* match_bytevector, + const uint8_t* rows_left, const uint8_t* rows_right, const uint32_t* offsets_left, + const uint32_t* offsets_right); + +#endif +}; + +} // namespace compute +} // namespace arrow diff --git a/cpp/src/arrow/compute/exec/key_compare_avx2.cc b/cpp/src/arrow/compute/exec/key_compare_avx2.cc new file mode 100644 index 00000000000..db4d43f0e82 --- /dev/null +++ b/cpp/src/arrow/compute/exec/key_compare_avx2.cc @@ -0,0 +1,189 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#include + +#include "arrow/compute/exec/key_compare.h" +#include "arrow/util/bit_util.h" + +namespace arrow { +namespace compute { + +#if defined(ARROW_HAVE_AVX2) + +uint32_t KeyCompare::CompareFixedLength_UpTo8B_avx2( + uint32_t num_rows, const uint32_t* left_to_right_map, uint8_t* match_bytevector, + uint32_t length, const uint8_t* rows_left, const uint8_t* rows_right) { + ARROW_DCHECK(length <= 8); + __m256i offset_left = _mm256_setr_epi64x(0, length, length * 2, length * 3); + __m256i offset_left_incr = _mm256_set1_epi64x(length * 4); + __m256i mask = _mm256_set1_epi64x(~0ULL >> (8 * (8 - length))); + + constexpr uint32_t unroll = 4; + for (uint32_t i = 0; i < num_rows / unroll; ++i) { + __m256i key_left = + _mm256_i64gather_epi64((const long long*)rows_left, offset_left, 1); + offset_left = _mm256_add_epi64(offset_left, offset_left_incr); + __m128i offset_right = + _mm_loadu_si128(reinterpret_cast(left_to_right_map) + i); + offset_right = _mm_mullo_epi32(offset_right, _mm_set1_epi32(length)); + __m256i key_right = + _mm256_i32gather_epi64((const long long*)rows_right, offset_right, 1); + uint32_t cmp = _mm256_movemask_epi8(_mm256_cmpeq_epi64( + _mm256_and_si256(key_left, mask), _mm256_and_si256(key_right, mask))); + reinterpret_cast(match_bytevector)[i] &= cmp; + } + + uint32_t num_rows_processed = num_rows - (num_rows % unroll); + return num_rows_processed; +} + +uint32_t KeyCompare::CompareFixedLength_UpTo16B_avx2( + uint32_t num_rows, const uint32_t* left_to_right_map, uint8_t* match_bytevector, + uint32_t length, const uint8_t* rows_left, const uint8_t* rows_right) { + ARROW_DCHECK(length <= 16); + + constexpr uint64_t kByteSequence0To7 = 0x0706050403020100ULL; + constexpr uint64_t kByteSequence8To15 = 0x0f0e0d0c0b0a0908ULL; + + __m256i mask = + _mm256_cmpgt_epi8(_mm256_set1_epi8(length), + _mm256_setr_epi64x(kByteSequence0To7, kByteSequence8To15, + kByteSequence0To7, kByteSequence8To15)); + const uint8_t* key_left_ptr = rows_left; + + constexpr uint32_t unroll = 2; + for (uint32_t i = 0; i < num_rows / unroll; ++i) { + __m256i key_left = _mm256_inserti128_si256( + _mm256_castsi128_si256( + _mm_loadu_si128(reinterpret_cast(key_left_ptr))), + _mm_loadu_si128(reinterpret_cast(key_left_ptr + length)), 1); + key_left_ptr += length * 2; + __m256i key_right = _mm256_inserti128_si256( + _mm256_castsi128_si256(_mm_loadu_si128(reinterpret_cast( + rows_right + length * left_to_right_map[2 * i]))), + _mm_loadu_si128(reinterpret_cast( + rows_right + length * left_to_right_map[2 * i + 1])), + 1); + __m256i cmp = _mm256_cmpeq_epi64(_mm256_and_si256(key_left, mask), + _mm256_and_si256(key_right, mask)); + cmp = _mm256_and_si256(cmp, _mm256_shuffle_epi32(cmp, 0xee)); // 0b11101110 + cmp = _mm256_permute4x64_epi64(cmp, 0x08); // 0b00001000 + reinterpret_cast(match_bytevector)[i] &= + (_mm256_movemask_epi8(cmp) & 0xffff); + } + + uint32_t num_rows_processed = num_rows - (num_rows % unroll); + return num_rows_processed; +} + +uint32_t KeyCompare::CompareFixedLength_avx2(uint32_t num_rows, + const uint32_t* left_to_right_map, + uint8_t* match_bytevector, uint32_t length, + const uint8_t* rows_left, + const uint8_t* rows_right) { + ARROW_DCHECK(length > 0); + + constexpr uint64_t kByteSequence0To7 = 0x0706050403020100ULL; + constexpr uint64_t kByteSequence8To15 = 0x0f0e0d0c0b0a0908ULL; + constexpr uint64_t kByteSequence16To23 = 0x1716151413121110ULL; + constexpr uint64_t kByteSequence24To31 = 0x1f1e1d1c1b1a1918ULL; + + // Non-zero length guarantees no underflow + int32_t num_loops_less_one = (static_cast(length) + 31) / 32 - 1; + + __m256i tail_mask = + _mm256_cmpgt_epi8(_mm256_set1_epi8(length - num_loops_less_one * 32), + _mm256_setr_epi64x(kByteSequence0To7, kByteSequence8To15, + kByteSequence16To23, kByteSequence24To31)); + + for (uint32_t irow_left = 0; irow_left < num_rows; ++irow_left) { + uint32_t irow_right = left_to_right_map[irow_left]; + uint32_t begin_left = length * irow_left; + uint32_t begin_right = length * irow_right; + const __m256i* key_left_ptr = + reinterpret_cast(rows_left + begin_left); + const __m256i* key_right_ptr = + reinterpret_cast(rows_right + begin_right); + __m256i result_or = _mm256_setzero_si256(); + int32_t i; + // length cannot be zero + for (i = 0; i < num_loops_less_one; ++i) { + __m256i key_left = _mm256_loadu_si256(key_left_ptr + i); + __m256i key_right = _mm256_loadu_si256(key_right_ptr + i); + result_or = _mm256_or_si256(result_or, _mm256_xor_si256(key_left, key_right)); + } + + __m256i key_left = _mm256_loadu_si256(key_left_ptr + i); + __m256i key_right = _mm256_loadu_si256(key_right_ptr + i); + result_or = _mm256_or_si256( + result_or, _mm256_and_si256(tail_mask, _mm256_xor_si256(key_left, key_right))); + int result = _mm256_testz_si256(result_or, result_or) * 0xff; + match_bytevector[irow_left] &= result; + } + + uint32_t num_rows_processed = num_rows; + return num_rows_processed; +} + +void KeyCompare::CompareVaryingLength_avx2( + uint32_t num_rows, const uint32_t* left_to_right_map, uint8_t* match_bytevector, + const uint8_t* rows_left, const uint8_t* rows_right, const uint32_t* offsets_left, + const uint32_t* offsets_right) { + for (uint32_t irow_left = 0; irow_left < num_rows; ++irow_left) { + uint32_t irow_right = left_to_right_map[irow_left]; + uint32_t begin_left = offsets_left[irow_left]; + uint32_t begin_right = offsets_right[irow_right]; + uint32_t length_left = offsets_left[irow_left + 1] - begin_left; + uint32_t length_right = offsets_right[irow_right + 1] - begin_right; + uint32_t length = std::min(length_left, length_right); + const __m256i* key_left_ptr = + reinterpret_cast(rows_left + begin_left); + const __m256i* key_right_ptr = + reinterpret_cast(rows_right + begin_right); + __m256i result_or = _mm256_setzero_si256(); + int32_t i; + // length can be zero + for (i = 0; i < (static_cast(length) + 31) / 32 - 1; ++i) { + __m256i key_left = _mm256_loadu_si256(key_left_ptr + i); + __m256i key_right = _mm256_loadu_si256(key_right_ptr + i); + result_or = _mm256_or_si256(result_or, _mm256_xor_si256(key_left, key_right)); + } + + constexpr uint64_t kByteSequence0To7 = 0x0706050403020100ULL; + constexpr uint64_t kByteSequence8To15 = 0x0f0e0d0c0b0a0908ULL; + constexpr uint64_t kByteSequence16To23 = 0x1716151413121110ULL; + constexpr uint64_t kByteSequence24To31 = 0x1f1e1d1c1b1a1918ULL; + + __m256i tail_mask = + _mm256_cmpgt_epi8(_mm256_set1_epi8(length - i * 32), + _mm256_setr_epi64x(kByteSequence0To7, kByteSequence8To15, + kByteSequence16To23, kByteSequence24To31)); + + __m256i key_left = _mm256_loadu_si256(key_left_ptr + i); + __m256i key_right = _mm256_loadu_si256(key_right_ptr + i); + result_or = _mm256_or_si256( + result_or, _mm256_and_si256(tail_mask, _mm256_xor_si256(key_left, key_right))); + int result = _mm256_testz_si256(result_or, result_or) * 0xff; + match_bytevector[irow_left] &= result; + } +} + +#endif + +} // namespace compute +} // namespace arrow diff --git a/cpp/src/arrow/compute/exec/key_encode.cc b/cpp/src/arrow/compute/exec/key_encode.cc new file mode 100644 index 00000000000..eb1424a126d --- /dev/null +++ b/cpp/src/arrow/compute/exec/key_encode.cc @@ -0,0 +1,1625 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#include "arrow/compute/exec/key_encode.h" + +#include + +#include + +#include "arrow/compute/exec/util.h" +#include "arrow/util/bit_util.h" +#include "arrow/util/ubsan.h" + +namespace arrow { +namespace compute { + +KeyEncoder::KeyRowArray::KeyRowArray() + : pool_(nullptr), rows_capacity_(0), bytes_capacity_(0) {} + +Status KeyEncoder::KeyRowArray::Init(MemoryPool* pool, const KeyRowMetadata& metadata) { + pool_ = pool; + metadata_ = metadata; + + ARROW_DCHECK(!null_masks_ && !offsets_ && !rows_); + + constexpr int64_t rows_capacity = 8; + constexpr int64_t bytes_capacity = 1024; + + // Null masks + ARROW_ASSIGN_OR_RAISE(auto null_masks, + AllocateResizableBuffer(size_null_masks(rows_capacity), pool_)); + null_masks_ = std::move(null_masks); + memset(null_masks_->mutable_data(), 0, size_null_masks(rows_capacity)); + + // Offsets and rows + if (!metadata.is_fixed_length) { + ARROW_ASSIGN_OR_RAISE(auto offsets, + AllocateResizableBuffer(size_offsets(rows_capacity), pool_)); + offsets_ = std::move(offsets); + memset(offsets_->mutable_data(), 0, size_offsets(rows_capacity)); + reinterpret_cast(offsets_->mutable_data())[0] = 0; + + ARROW_ASSIGN_OR_RAISE( + auto rows, + AllocateResizableBuffer(size_rows_varying_length(bytes_capacity), pool_)); + rows_ = std::move(rows); + memset(rows_->mutable_data(), 0, size_rows_varying_length(bytes_capacity)); + bytes_capacity_ = size_rows_varying_length(bytes_capacity) - padding_for_vectors; + } else { + ARROW_ASSIGN_OR_RAISE( + auto rows, AllocateResizableBuffer(size_rows_fixed_length(rows_capacity), pool_)); + rows_ = std::move(rows); + memset(rows_->mutable_data(), 0, size_rows_fixed_length(rows_capacity)); + bytes_capacity_ = size_rows_fixed_length(rows_capacity) - padding_for_vectors; + } + + update_buffer_pointers(); + + rows_capacity_ = rows_capacity; + + num_rows_ = 0; + num_rows_for_has_any_nulls_ = 0; + has_any_nulls_ = false; + + return Status::OK(); +} + +void KeyEncoder::KeyRowArray::Clean() { + num_rows_ = 0; + num_rows_for_has_any_nulls_ = 0; + has_any_nulls_ = false; + + if (!metadata_.is_fixed_length) { + reinterpret_cast(offsets_->mutable_data())[0] = 0; + } +} + +int64_t KeyEncoder::KeyRowArray::size_null_masks(int64_t num_rows) { + return num_rows * metadata_.null_masks_bytes_per_row + padding_for_vectors; +} + +int64_t KeyEncoder::KeyRowArray::size_offsets(int64_t num_rows) { + return (num_rows + 1) * sizeof(uint32_t) + padding_for_vectors; +} + +int64_t KeyEncoder::KeyRowArray::size_rows_fixed_length(int64_t num_rows) { + return num_rows * metadata_.fixed_length + padding_for_vectors; +} + +int64_t KeyEncoder::KeyRowArray::size_rows_varying_length(int64_t num_bytes) { + return num_bytes + padding_for_vectors; +} + +void KeyEncoder::KeyRowArray::update_buffer_pointers() { + buffers_[0] = mutable_buffers_[0] = null_masks_->mutable_data(); + if (metadata_.is_fixed_length) { + buffers_[1] = mutable_buffers_[1] = rows_->mutable_data(); + buffers_[2] = mutable_buffers_[2] = nullptr; + } else { + buffers_[1] = mutable_buffers_[1] = offsets_->mutable_data(); + buffers_[2] = mutable_buffers_[2] = rows_->mutable_data(); + } +} + +Status KeyEncoder::KeyRowArray::ResizeFixedLengthBuffers(int64_t num_extra_rows) { + if (rows_capacity_ >= num_rows_ + num_extra_rows) { + return Status::OK(); + } + + int64_t rows_capacity_new = std::max(static_cast(1), 2 * rows_capacity_); + while (rows_capacity_new < num_rows_ + num_extra_rows) { + rows_capacity_new *= 2; + } + + // Null masks + RETURN_NOT_OK(null_masks_->Resize(size_null_masks(rows_capacity_new), false)); + memset(null_masks_->mutable_data() + size_null_masks(rows_capacity_), 0, + size_null_masks(rows_capacity_new) - size_null_masks(rows_capacity_)); + + // Either offsets or rows + if (!metadata_.is_fixed_length) { + RETURN_NOT_OK(offsets_->Resize(size_offsets(rows_capacity_new), false)); + memset(offsets_->mutable_data() + size_offsets(rows_capacity_), 0, + size_offsets(rows_capacity_new) - size_offsets(rows_capacity_)); + } else { + RETURN_NOT_OK(rows_->Resize(size_rows_fixed_length(rows_capacity_new), false)); + memset(rows_->mutable_data() + size_rows_fixed_length(rows_capacity_), 0, + size_rows_fixed_length(rows_capacity_new) - + size_rows_fixed_length(rows_capacity_)); + bytes_capacity_ = size_rows_fixed_length(rows_capacity_new) - padding_for_vectors; + } + + update_buffer_pointers(); + + rows_capacity_ = rows_capacity_new; + + return Status::OK(); +} + +Status KeyEncoder::KeyRowArray::ResizeOptionalVaryingLengthBuffer( + int64_t num_extra_bytes) { + int64_t num_bytes = offsets()[num_rows_]; + if (bytes_capacity_ >= num_bytes + num_extra_bytes || metadata_.is_fixed_length) { + return Status::OK(); + } + + int64_t bytes_capacity_new = std::max(static_cast(1), 2 * bytes_capacity_); + while (bytes_capacity_new < num_bytes + num_extra_bytes) { + bytes_capacity_new *= 2; + } + + RETURN_NOT_OK(rows_->Resize(size_rows_varying_length(bytes_capacity_new), false)); + memset(rows_->mutable_data() + size_rows_varying_length(bytes_capacity_), 0, + size_rows_varying_length(bytes_capacity_new) - + size_rows_varying_length(bytes_capacity_)); + + update_buffer_pointers(); + + bytes_capacity_ = bytes_capacity_new; + + return Status::OK(); +} + +Status KeyEncoder::KeyRowArray::AppendSelectionFrom(const KeyRowArray& from, + uint32_t num_rows_to_append, + const uint16_t* source_row_ids) { + ARROW_DCHECK(metadata_.is_compatible(from.metadata())); + + RETURN_NOT_OK(ResizeFixedLengthBuffers(num_rows_to_append)); + + if (!metadata_.is_fixed_length) { + // Varying-length rows + const uint32_t* from_offsets = + reinterpret_cast(from.offsets_->data()); + uint32_t* to_offsets = reinterpret_cast(offsets_->mutable_data()); + uint32_t total_length = to_offsets[num_rows_]; + uint32_t total_length_to_append = 0; + for (uint32_t i = 0; i < num_rows_to_append; ++i) { + uint16_t row_id = source_row_ids[i]; + uint32_t length = from_offsets[row_id + 1] - from_offsets[row_id]; + total_length_to_append += length; + to_offsets[num_rows_ + i + 1] = total_length + total_length_to_append; + } + + RETURN_NOT_OK(ResizeOptionalVaryingLengthBuffer(total_length_to_append)); + + const uint8_t* src = from.rows_->data(); + uint8_t* dst = rows_->mutable_data() + total_length; + for (uint32_t i = 0; i < num_rows_to_append; ++i) { + uint16_t row_id = source_row_ids[i]; + uint32_t length = from_offsets[row_id + 1] - from_offsets[row_id]; + const uint64_t* src64 = + reinterpret_cast(src + from_offsets[row_id]); + uint64_t* dst64 = reinterpret_cast(dst); + for (uint32_t j = 0; j < (length + 7) / 8; ++j) { + dst64[j] = src64[j]; + } + dst += length; + } + } else { + // Fixed-length rows + const uint8_t* src = from.rows_->data(); + uint8_t* dst = rows_->mutable_data() + num_rows_ * metadata_.fixed_length; + for (uint32_t i = 0; i < num_rows_to_append; ++i) { + uint16_t row_id = source_row_ids[i]; + uint32_t length = metadata_.fixed_length; + const uint64_t* src64 = reinterpret_cast(src + length * row_id); + uint64_t* dst64 = reinterpret_cast(dst); + for (uint32_t j = 0; j < (length + 7) / 8; ++j) { + dst64[j] = src64[j]; + } + dst += length; + } + } + + // Null masks + uint32_t byte_length = metadata_.null_masks_bytes_per_row; + uint64_t dst_byte_offset = num_rows_ * byte_length; + const uint8_t* src_base = from.null_masks_->data(); + uint8_t* dst_base = null_masks_->mutable_data(); + for (uint32_t i = 0; i < num_rows_to_append; ++i) { + uint32_t row_id = source_row_ids[i]; + int64_t src_byte_offset = row_id * byte_length; + const uint8_t* src = src_base + src_byte_offset; + uint8_t* dst = dst_base + dst_byte_offset; + for (uint32_t ibyte = 0; ibyte < byte_length; ++ibyte) { + dst[ibyte] = src[ibyte]; + } + dst_byte_offset += byte_length; + } + + num_rows_ += num_rows_to_append; + + return Status::OK(); +} + +Status KeyEncoder::KeyRowArray::AppendEmpty(uint32_t num_rows_to_append, + uint32_t num_extra_bytes_to_append) { + RETURN_NOT_OK(ResizeFixedLengthBuffers(num_rows_to_append)); + RETURN_NOT_OK(ResizeOptionalVaryingLengthBuffer(num_extra_bytes_to_append)); + num_rows_ += num_rows_to_append; + if (metadata_.row_alignment > 1 || metadata_.string_alignment > 1) { + memset(rows_->mutable_data(), 0, bytes_capacity_); + } + return Status::OK(); +} + +bool KeyEncoder::KeyRowArray::has_any_nulls(const KeyEncoderContext* ctx) const { + if (has_any_nulls_) { + return true; + } + if (num_rows_for_has_any_nulls_ < num_rows_) { + auto size_per_row = metadata().null_masks_bytes_per_row; + has_any_nulls_ = !util::BitUtil::are_all_bytes_zero( + ctx->cpu_info, null_masks() + size_per_row * num_rows_for_has_any_nulls_, + static_cast(size_per_row * (num_rows_ - num_rows_for_has_any_nulls_))); + num_rows_for_has_any_nulls_ = num_rows_; + } + return has_any_nulls_; +} + +KeyEncoder::KeyColumnArray::KeyColumnArray(const KeyColumnMetadata& metadata, + const KeyColumnArray& left, + const KeyColumnArray& right, + int buffer_id_to_replace) { + metadata_ = metadata; + length_ = left.length(); + for (int i = 0; i < max_buffers_; ++i) { + buffers_[i] = left.buffers_[i]; + mutable_buffers_[i] = left.mutable_buffers_[i]; + } + buffers_[buffer_id_to_replace] = right.buffers_[buffer_id_to_replace]; + mutable_buffers_[buffer_id_to_replace] = right.mutable_buffers_[buffer_id_to_replace]; +} + +KeyEncoder::KeyColumnArray::KeyColumnArray(const KeyColumnMetadata& metadata, + int64_t length, const uint8_t* buffer0, + const uint8_t* buffer1, + const uint8_t* buffer2) { + metadata_ = metadata; + length_ = length; + buffers_[0] = buffer0; + buffers_[1] = buffer1; + buffers_[2] = buffer2; + mutable_buffers_[0] = mutable_buffers_[1] = mutable_buffers_[2] = nullptr; +} + +KeyEncoder::KeyColumnArray::KeyColumnArray(const KeyColumnMetadata& metadata, + int64_t length, uint8_t* buffer0, + uint8_t* buffer1, uint8_t* buffer2) { + metadata_ = metadata; + length_ = length; + buffers_[0] = mutable_buffers_[0] = buffer0; + buffers_[1] = mutable_buffers_[1] = buffer1; + buffers_[2] = mutable_buffers_[2] = buffer2; +} + +KeyEncoder::KeyColumnArray::KeyColumnArray(const KeyColumnArray& from, int64_t start, + int64_t length) { + ARROW_DCHECK((start % 8) == 0); + metadata_ = from.metadata_; + length_ = length; + uint32_t fixed_size = + !metadata_.is_fixed_length ? sizeof(uint32_t) : metadata_.fixed_length; + + buffers_[0] = from.buffers_[0] ? from.buffers_[0] + start / 8 : nullptr; + mutable_buffers_[0] = + from.mutable_buffers_[0] ? from.mutable_buffers_[0] + start / 8 : nullptr; + + if (fixed_size == 0) { + buffers_[1] = from.buffers_[1] ? from.buffers_[1] + start / 8 : nullptr; + mutable_buffers_[1] = + from.mutable_buffers_[1] ? from.mutable_buffers_[1] + start / 8 : nullptr; + } else { + buffers_[1] = from.buffers_[1] ? from.buffers_[1] + start * fixed_size : nullptr; + mutable_buffers_[1] = from.mutable_buffers_[1] + ? from.mutable_buffers_[1] + start * fixed_size + : nullptr; + } + + buffers_[2] = from.buffers_[2]; + mutable_buffers_[2] = from.mutable_buffers_[2]; +} + +KeyEncoder::KeyColumnArray KeyEncoder::TransformBoolean::ArrayReplace( + const KeyColumnArray& column, const KeyColumnArray& temp) { + // Make sure that the temp buffer is large enough + ARROW_DCHECK(temp.length() >= column.length() && temp.metadata().is_fixed_length && + temp.metadata().fixed_length >= sizeof(uint8_t)); + KeyColumnMetadata metadata; + metadata.is_fixed_length = true; + metadata.fixed_length = sizeof(uint8_t); + constexpr int buffer_index = 1; + KeyColumnArray result = KeyColumnArray(metadata, column, temp, buffer_index); + return result; +} + +void KeyEncoder::TransformBoolean::PreEncode(const KeyColumnArray& input, + KeyColumnArray* output, + KeyEncoderContext* ctx) { + // Make sure that metadata and lengths are compatible. + ARROW_DCHECK(output->metadata().is_fixed_length == input.metadata().is_fixed_length); + ARROW_DCHECK(output->metadata().fixed_length == 1 && + input.metadata().fixed_length == 0); + ARROW_DCHECK(output->length() == input.length()); + constexpr int buffer_index = 1; + ARROW_DCHECK(input.data(buffer_index) != nullptr); + ARROW_DCHECK(output->mutable_data(buffer_index) != nullptr); + util::BitUtil::bits_to_bytes(ctx->cpu_info, static_cast(input.length()), + input.data(buffer_index), + output->mutable_data(buffer_index)); +} + +void KeyEncoder::TransformBoolean::PostDecode(const KeyColumnArray& input, + KeyColumnArray* output, + KeyEncoderContext* ctx) { + // Make sure that metadata and lengths are compatible. + ARROW_DCHECK(output->metadata().is_fixed_length == input.metadata().is_fixed_length); + ARROW_DCHECK(output->metadata().fixed_length == 0 && + input.metadata().fixed_length == 1); + ARROW_DCHECK(output->length() == input.length()); + constexpr int buffer_index = 1; + ARROW_DCHECK(input.data(buffer_index) != nullptr); + ARROW_DCHECK(output->mutable_data(buffer_index) != nullptr); + + util::BitUtil::bytes_to_bits(ctx->cpu_info, static_cast(input.length()), + input.data(buffer_index), + output->mutable_data(buffer_index)); +} + +bool KeyEncoder::EncoderInteger::IsBoolean(const KeyColumnMetadata& metadata) { + return metadata.is_fixed_length && metadata.fixed_length == 0; +} + +bool KeyEncoder::EncoderInteger::UsesTransform(const KeyColumnArray& column) { + return IsBoolean(column.metadata()); +} + +KeyEncoder::KeyColumnArray KeyEncoder::EncoderInteger::ArrayReplace( + const KeyColumnArray& column, const KeyColumnArray& temp) { + if (IsBoolean(column.metadata())) { + return TransformBoolean::ArrayReplace(column, temp); + } + return column; +} + +void KeyEncoder::EncoderInteger::PreEncode(const KeyColumnArray& input, + KeyColumnArray* output, + KeyEncoderContext* ctx) { + if (IsBoolean(input.metadata())) { + TransformBoolean::PreEncode(input, output, ctx); + } +} + +void KeyEncoder::EncoderInteger::PostDecode(const KeyColumnArray& input, + KeyColumnArray* output, + KeyEncoderContext* ctx) { + if (IsBoolean(output->metadata())) { + TransformBoolean::PostDecode(input, output, ctx); + } +} + +void KeyEncoder::EncoderInteger::Encode(uint32_t offset_within_row, KeyRowArray* rows, + const KeyColumnArray& col, KeyEncoderContext* ctx, + KeyColumnArray* temp) { + KeyColumnArray col_prep; + if (UsesTransform(col)) { + col_prep = ArrayReplace(col, *temp); + PreEncode(col, &col_prep, ctx); + } else { + col_prep = col; + } + + uint32_t num_rows = static_cast(col.length()); + + // When we have a single fixed length column we can just do memcpy + if (rows->metadata().is_fixed_length && + rows->metadata().fixed_length == col.metadata().fixed_length) { + ARROW_DCHECK(offset_within_row == 0); + uint32_t row_size = col.metadata().fixed_length; + memcpy(rows->mutable_data(1), col.data(1), num_rows * row_size); + } else if (rows->metadata().is_fixed_length) { + uint32_t row_size = rows->metadata().fixed_length; + uint8_t* row_base = rows->mutable_data(1) + offset_within_row; + const uint8_t* col_base = col_prep.data(1); + switch (col_prep.metadata().fixed_length) { + case 1: + for (uint32_t i = 0; i < num_rows; ++i) { + row_base[i * row_size] = col_base[i]; + } + break; + case 2: + for (uint32_t i = 0; i < num_rows; ++i) { + *reinterpret_cast(row_base + i * row_size) = + reinterpret_cast(col_base)[i]; + } + break; + case 4: + for (uint32_t i = 0; i < num_rows; ++i) { + *reinterpret_cast(row_base + i * row_size) = + reinterpret_cast(col_base)[i]; + } + break; + case 8: + for (uint32_t i = 0; i < num_rows; ++i) { + *reinterpret_cast(row_base + i * row_size) = + reinterpret_cast(col_base)[i]; + } + break; + default: + ARROW_DCHECK(false); + } + } else { + const uint32_t* row_offsets = rows->offsets(); + uint8_t* row_base = rows->mutable_data(2) + offset_within_row; + const uint8_t* col_base = col_prep.data(1); + switch (col_prep.metadata().fixed_length) { + case 1: + for (uint32_t i = 0; i < num_rows; ++i) { + row_base[row_offsets[i]] = col_base[i]; + } + break; + case 2: + for (uint32_t i = 0; i < num_rows; ++i) { + *reinterpret_cast(row_base + row_offsets[i]) = + reinterpret_cast(col_base)[i]; + } + break; + case 4: + for (uint32_t i = 0; i < num_rows; ++i) { + *reinterpret_cast(row_base + row_offsets[i]) = + reinterpret_cast(col_base)[i]; + } + break; + case 8: + for (uint32_t i = 0; i < num_rows; ++i) { + *reinterpret_cast(row_base + row_offsets[i]) = + reinterpret_cast(col_base)[i]; + } + break; + default: + ARROW_DCHECK(false); + } + } +} + +void KeyEncoder::EncoderInteger::Decode(uint32_t start_row, uint32_t num_rows, + uint32_t offset_within_row, + const KeyRowArray& rows, KeyColumnArray* col, + KeyEncoderContext* ctx, KeyColumnArray* temp) { + KeyColumnArray col_prep; + if (UsesTransform(*col)) { + col_prep = ArrayReplace(*col, *temp); + } else { + col_prep = *col; + } + + // When we have a single fixed length column we can just do memcpy + if (rows.metadata().is_fixed_length && + col_prep.metadata().fixed_length == rows.metadata().fixed_length) { + ARROW_DCHECK(offset_within_row == 0); + uint32_t row_size = rows.metadata().fixed_length; + memcpy(col_prep.mutable_data(1), rows.data(1) + start_row * row_size, + num_rows * row_size); + } else if (rows.metadata().is_fixed_length) { + uint32_t row_size = rows.metadata().fixed_length; + const uint8_t* row_base = rows.data(1) + start_row * row_size; + row_base += offset_within_row; + uint8_t* col_base = col_prep.mutable_data(1); + switch (col_prep.metadata().fixed_length) { + case 1: + for (uint32_t i = 0; i < num_rows; ++i) { + col_base[i] = row_base[i * row_size]; + } + break; + case 2: + for (uint32_t i = 0; i < num_rows; ++i) { + reinterpret_cast(col_base)[i] = + *reinterpret_cast(row_base + i * row_size); + } + break; + case 4: + for (uint32_t i = 0; i < num_rows; ++i) { + reinterpret_cast(col_base)[i] = + *reinterpret_cast(row_base + i * row_size); + } + break; + case 8: + for (uint32_t i = 0; i < num_rows; ++i) { + reinterpret_cast(col_base)[i] = + *reinterpret_cast(row_base + i * row_size); + } + break; + default: + ARROW_DCHECK(false); + } + } else { + const uint32_t* row_offsets = rows.offsets() + start_row; + const uint8_t* row_base = rows.data(2); + row_base += offset_within_row; + uint8_t* col_base = col_prep.mutable_data(1); + switch (col_prep.metadata().fixed_length) { + case 1: + for (uint32_t i = 0; i < num_rows; ++i) { + col_base[i] = row_base[row_offsets[i]]; + } + break; + case 2: + for (uint32_t i = 0; i < num_rows; ++i) { + reinterpret_cast(col_base)[i] = + *reinterpret_cast(row_base + row_offsets[i]); + } + break; + case 4: + for (uint32_t i = 0; i < num_rows; ++i) { + reinterpret_cast(col_base)[i] = + *reinterpret_cast(row_base + row_offsets[i]); + } + break; + case 8: + for (uint32_t i = 0; i < num_rows; ++i) { + reinterpret_cast(col_base)[i] = + *reinterpret_cast(row_base + row_offsets[i]); + } + break; + default: + ARROW_DCHECK(false); + } + } + + if (UsesTransform(*col)) { + PostDecode(col_prep, col, ctx); + } +} + +bool KeyEncoder::EncoderBinary::IsInteger(const KeyColumnMetadata& metadata) { + bool is_fixed_length = metadata.is_fixed_length; + auto size = metadata.fixed_length; + return is_fixed_length && + (size == 0 || size == 1 || size == 2 || size == 4 || size == 8); +} + +void KeyEncoder::EncoderBinary::Encode(uint32_t offset_within_row, KeyRowArray* rows, + const KeyColumnArray& col, KeyEncoderContext* ctx, + KeyColumnArray* temp) { + if (IsInteger(col.metadata())) { + EncoderInteger::Encode(offset_within_row, rows, col, ctx, temp); + } else { + KeyColumnArray col_prep; + if (EncoderInteger::UsesTransform(col)) { + col_prep = EncoderInteger::ArrayReplace(col, *temp); + EncoderInteger::PreEncode(col, &col_prep, ctx); + } else { + col_prep = col; + } + + bool is_row_fixed_length = rows->metadata().is_fixed_length; + +#if defined(ARROW_HAVE_AVX2) + if (ctx->has_avx2()) { + EncodeHelper_avx2(is_row_fixed_length, offset_within_row, rows, col); + } else { +#endif + if (is_row_fixed_length) { + EncodeImp(offset_within_row, rows, col); + } else { + EncodeImp(offset_within_row, rows, col); + } +#if defined(ARROW_HAVE_AVX2) + } +#endif + } + + ARROW_DCHECK(temp->metadata().is_fixed_length); + ARROW_DCHECK(temp->length() * temp->metadata().fixed_length >= + col.length() * static_cast(sizeof(uint16_t))); + + KeyColumnArray temp16bit(KeyColumnMetadata(true, sizeof(uint16_t)), col.length(), + nullptr, temp->mutable_data(1), nullptr); + ColumnMemsetNulls(offset_within_row, rows, col, ctx, &temp16bit, 0xae); +} + +void KeyEncoder::EncoderBinary::Decode(uint32_t start_row, uint32_t num_rows, + uint32_t offset_within_row, + const KeyRowArray& rows, KeyColumnArray* col, + KeyEncoderContext* ctx, KeyColumnArray* temp) { + if (IsInteger(col->metadata())) { + EncoderInteger::Decode(start_row, num_rows, offset_within_row, rows, col, ctx, temp); + } else { + KeyColumnArray col_prep; + if (EncoderInteger::UsesTransform(*col)) { + col_prep = EncoderInteger::ArrayReplace(*col, *temp); + } else { + col_prep = *col; + } + + bool is_row_fixed_length = rows.metadata().is_fixed_length; + +#if defined(ARROW_HAVE_AVX2) + if (ctx->has_avx2()) { + DecodeHelper_avx2(is_row_fixed_length, start_row, num_rows, offset_within_row, rows, + col); + } else { +#endif + if (is_row_fixed_length) { + DecodeImp(start_row, num_rows, offset_within_row, rows, col); + } else { + DecodeImp(start_row, num_rows, offset_within_row, rows, col); + } +#if defined(ARROW_HAVE_AVX2) + } +#endif + + if (EncoderInteger::UsesTransform(*col)) { + EncoderInteger::PostDecode(col_prep, col, ctx); + } + } +} + +template +void KeyEncoder::EncoderBinary::EncodeImp(uint32_t offset_within_row, KeyRowArray* rows, + const KeyColumnArray& col) { + EncodeDecodeHelper( + 0, static_cast(col.length()), offset_within_row, rows, rows, &col, + nullptr, [](uint8_t* dst, const uint8_t* src, int64_t length) { + uint64_t* dst64 = reinterpret_cast(dst); + const uint64_t* src64 = reinterpret_cast(src); + uint32_t istripe; + for (istripe = 0; istripe < length / 8; ++istripe) { + dst64[istripe] = util::SafeLoad(src64 + istripe); + } + if ((length % 8) > 0) { + uint64_t mask_last = ~0ULL >> (8 * (8 * (istripe + 1) - length)); + dst64[istripe] = (dst64[istripe] & ~mask_last) | + (util::SafeLoad(src64 + istripe) & mask_last); + } + }); +} + +template +void KeyEncoder::EncoderBinary::DecodeImp(uint32_t start_row, uint32_t num_rows, + uint32_t offset_within_row, + const KeyRowArray& rows, KeyColumnArray* col) { + EncodeDecodeHelper( + start_row, num_rows, offset_within_row, &rows, nullptr, col, col, + [](uint8_t* dst, const uint8_t* src, int64_t length) { + for (uint32_t istripe = 0; istripe < (length + 7) / 8; ++istripe) { + uint64_t* dst64 = reinterpret_cast(dst); + const uint64_t* src64 = reinterpret_cast(src); + util::SafeStore(dst64 + istripe, src64[istripe]); + } + }); +} + +void KeyEncoder::EncoderBinary::ColumnMemsetNulls( + uint32_t offset_within_row, KeyRowArray* rows, const KeyColumnArray& col, + KeyEncoderContext* ctx, KeyColumnArray* temp_vector_16bit, uint8_t byte_value) { + typedef void (*ColumnMemsetNullsImp_t)(uint32_t, KeyRowArray*, const KeyColumnArray&, + KeyEncoderContext*, KeyColumnArray*, uint8_t); + static const ColumnMemsetNullsImp_t ColumnMemsetNullsImp_fn[] = { + ColumnMemsetNullsImp, ColumnMemsetNullsImp, + ColumnMemsetNullsImp, ColumnMemsetNullsImp, + ColumnMemsetNullsImp, ColumnMemsetNullsImp, + ColumnMemsetNullsImp, ColumnMemsetNullsImp, + ColumnMemsetNullsImp, ColumnMemsetNullsImp}; + uint32_t col_width = col.metadata().fixed_length; + int dispatch_const = + (rows->metadata().is_fixed_length ? 5 : 0) + + (col_width == 1 ? 0 + : col_width == 2 ? 1 : col_width == 4 ? 2 : col_width == 8 ? 3 : 4); + ColumnMemsetNullsImp_fn[dispatch_const](offset_within_row, rows, col, ctx, + temp_vector_16bit, byte_value); +} + +template +void KeyEncoder::EncoderBinary::ColumnMemsetNullsImp( + uint32_t offset_within_row, KeyRowArray* rows, const KeyColumnArray& col, + KeyEncoderContext* ctx, KeyColumnArray* temp_vector_16bit, uint8_t byte_value) { + // Nothing to do when there are no nulls + if (!col.data(0)) { + return; + } + + uint32_t num_rows = static_cast(col.length()); + + // Temp vector needs space for the required number of rows + ARROW_DCHECK(temp_vector_16bit->length() >= num_rows); + ARROW_DCHECK(temp_vector_16bit->metadata().is_fixed_length && + temp_vector_16bit->metadata().fixed_length == sizeof(uint16_t)); + uint16_t* temp_vector = reinterpret_cast(temp_vector_16bit->mutable_data(1)); + + // Bit vector to index vector of null positions + int num_selected; + util::BitUtil::bits_to_indexes(0, ctx->cpu_info, static_cast(col.length()), + col.data(0), &num_selected, temp_vector); + + for (int i = 0; i < num_selected; ++i) { + uint32_t row_id = temp_vector[i]; + + // Target binary field pointer + uint8_t* dst; + if (is_row_fixed_length) { + dst = rows->mutable_data(1) + rows->metadata().fixed_length * row_id; + } else { + dst = rows->mutable_data(2) + rows->offsets()[row_id]; + } + dst += offset_within_row; + + if (col_width == 1) { + *dst = byte_value; + } else if (col_width == 2) { + *reinterpret_cast(dst) = + (static_cast(byte_value) * static_cast(0x0101)); + } else if (col_width == 4) { + *reinterpret_cast(dst) = + (static_cast(byte_value) * static_cast(0x01010101)); + } else if (col_width == 8) { + *reinterpret_cast(dst) = + (static_cast(byte_value) * 0x0101010101010101ULL); + } else { + uint64_t value = (static_cast(byte_value) * 0x0101010101010101ULL); + uint32_t col_width_actual = col.metadata().fixed_length; + uint32_t j; + for (j = 0; j < col_width_actual / 8; ++j) { + reinterpret_cast(dst)[j] = value; + } + int tail = col_width_actual % 8; + if (tail) { + uint64_t mask = ~0ULL >> (8 * (8 - tail)); + reinterpret_cast(dst)[j] = + (reinterpret_cast(dst)[j] & ~mask) | (value & mask); + } + } + } +} + +void KeyEncoder::EncoderBinaryPair::Encode(uint32_t offset_within_row, KeyRowArray* rows, + const KeyColumnArray& col1, + const KeyColumnArray& col2, + KeyEncoderContext* ctx, KeyColumnArray* temp1, + KeyColumnArray* temp2) { + ARROW_DCHECK(CanProcessPair(col1.metadata(), col2.metadata())); + + KeyColumnArray col_prep[2]; + if (EncoderInteger::UsesTransform(col1)) { + col_prep[0] = EncoderInteger::ArrayReplace(col1, *temp1); + EncoderInteger::PreEncode(col1, &(col_prep[0]), ctx); + } else { + col_prep[0] = col1; + } + if (EncoderInteger::UsesTransform(col2)) { + col_prep[1] = EncoderInteger::ArrayReplace(col2, *temp2); + EncoderInteger::PreEncode(col2, &(col_prep[1]), ctx); + } else { + col_prep[1] = col2; + } + + uint32_t col_width1 = col_prep[0].metadata().fixed_length; + uint32_t col_width2 = col_prep[1].metadata().fixed_length; + int log_col_width1 = + col_width1 == 8 ? 3 : col_width1 == 4 ? 2 : col_width1 == 2 ? 1 : 0; + int log_col_width2 = + col_width2 == 8 ? 3 : col_width2 == 4 ? 2 : col_width2 == 2 ? 1 : 0; + + bool is_row_fixed_length = rows->metadata().is_fixed_length; + + uint32_t num_rows = static_cast(col1.length()); + uint32_t num_processed = 0; +#if defined(ARROW_HAVE_AVX2) + if (ctx->has_avx2() && col_width1 == col_width2) { + num_processed = EncodeHelper_avx2(is_row_fixed_length, col_width1, offset_within_row, + rows, col_prep[0], col_prep[1]); + } +#endif + if (num_processed < num_rows) { + using EncodeImp_t = void (*)(uint32_t, uint32_t, KeyRowArray*, const KeyColumnArray&, + const KeyColumnArray&); + static const EncodeImp_t EncodeImp_fn[] = { + EncodeImp, EncodeImp, + EncodeImp, EncodeImp, + EncodeImp, EncodeImp, + EncodeImp, EncodeImp, + EncodeImp, EncodeImp, + EncodeImp, EncodeImp, + EncodeImp, EncodeImp, + EncodeImp, EncodeImp, + EncodeImp, EncodeImp, + EncodeImp, EncodeImp, + EncodeImp, EncodeImp, + EncodeImp, EncodeImp, + EncodeImp, EncodeImp, + EncodeImp, EncodeImp, + EncodeImp, EncodeImp, + EncodeImp, EncodeImp}; + int dispatch_const = (log_col_width2 << 2) | log_col_width1; + dispatch_const += (is_row_fixed_length ? 16 : 0); + EncodeImp_fn[dispatch_const](num_processed, offset_within_row, rows, col_prep[0], + col_prep[1]); + } +} + +template +void KeyEncoder::EncoderBinaryPair::EncodeImp(uint32_t num_rows_to_skip, + uint32_t offset_within_row, + KeyRowArray* rows, + const KeyColumnArray& col1, + const KeyColumnArray& col2) { + const uint8_t* src_A = col1.data(1); + const uint8_t* src_B = col2.data(1); + + uint32_t num_rows = static_cast(col1.length()); + + uint32_t fixed_length = rows->metadata().fixed_length; + const uint32_t* offsets; + uint8_t* dst_base; + if (is_row_fixed_length) { + dst_base = rows->mutable_data(1) + offset_within_row; + offsets = nullptr; + } else { + dst_base = rows->mutable_data(2) + offset_within_row; + offsets = rows->offsets(); + } + + using col1_type_const = typename std::add_const::type; + using col2_type_const = typename std::add_const::type; + + if (is_row_fixed_length) { + uint8_t* dst = dst_base + num_rows_to_skip * fixed_length; + for (uint32_t i = num_rows_to_skip; i < num_rows; ++i) { + *reinterpret_cast(dst) = reinterpret_cast(src_A)[i]; + *reinterpret_cast(dst + sizeof(col1_type)) = + reinterpret_cast(src_B)[i]; + dst += fixed_length; + } + } else { + for (uint32_t i = num_rows_to_skip; i < num_rows; ++i) { + uint8_t* dst = dst_base + offsets[i]; + *reinterpret_cast(dst) = reinterpret_cast(src_A)[i]; + *reinterpret_cast(dst + sizeof(col1_type)) = + reinterpret_cast(src_B)[i]; + } + } +} + +void KeyEncoder::EncoderBinaryPair::Decode(uint32_t start_row, uint32_t num_rows, + uint32_t offset_within_row, + const KeyRowArray& rows, KeyColumnArray* col1, + KeyColumnArray* col2, KeyEncoderContext* ctx, + KeyColumnArray* temp1, KeyColumnArray* temp2) { + ARROW_DCHECK(CanProcessPair(col1->metadata(), col2->metadata())); + + KeyColumnArray col_prep[2]; + if (EncoderInteger::UsesTransform(*col1)) { + col_prep[0] = EncoderInteger::ArrayReplace(*col1, *temp1); + } else { + col_prep[0] = *col1; + } + if (EncoderInteger::UsesTransform(*col2)) { + col_prep[1] = EncoderInteger::ArrayReplace(*col2, *temp2); + } else { + col_prep[1] = *col2; + } + + uint32_t col_width1 = col_prep[0].metadata().fixed_length; + uint32_t col_width2 = col_prep[1].metadata().fixed_length; + int log_col_width1 = + col_width1 == 8 ? 3 : col_width1 == 4 ? 2 : col_width1 == 2 ? 1 : 0; + int log_col_width2 = + col_width2 == 8 ? 3 : col_width2 == 4 ? 2 : col_width2 == 2 ? 1 : 0; + + bool is_row_fixed_length = rows.metadata().is_fixed_length; + + uint32_t num_processed = 0; +#if defined(ARROW_HAVE_AVX2) + if (ctx->has_avx2() && col_width1 == col_width2) { + num_processed = + DecodeHelper_avx2(is_row_fixed_length, col_width1, start_row, num_rows, + offset_within_row, rows, &col_prep[0], &col_prep[1]); + } +#endif + if (num_processed < num_rows) { + typedef void (*DecodeImp_t)(uint32_t, uint32_t, uint32_t, uint32_t, + const KeyRowArray&, KeyColumnArray*, KeyColumnArray*); + static const DecodeImp_t DecodeImp_fn[] = { + DecodeImp, DecodeImp, + DecodeImp, DecodeImp, + DecodeImp, DecodeImp, + DecodeImp, DecodeImp, + DecodeImp, DecodeImp, + DecodeImp, DecodeImp, + DecodeImp, DecodeImp, + DecodeImp, DecodeImp, + DecodeImp, DecodeImp, + DecodeImp, DecodeImp, + DecodeImp, DecodeImp, + DecodeImp, DecodeImp, + DecodeImp, DecodeImp, + DecodeImp, DecodeImp, + DecodeImp, DecodeImp, + DecodeImp, DecodeImp}; + int dispatch_const = + (log_col_width2 << 2) | log_col_width1 | (is_row_fixed_length ? 16 : 0); + DecodeImp_fn[dispatch_const](num_processed, start_row, num_rows, offset_within_row, + rows, &(col_prep[0]), &(col_prep[1])); + } + + if (EncoderInteger::UsesTransform(*col1)) { + EncoderInteger::PostDecode(col_prep[0], col1, ctx); + } + if (EncoderInteger::UsesTransform(*col2)) { + EncoderInteger::PostDecode(col_prep[1], col2, ctx); + } +} + +template +void KeyEncoder::EncoderBinaryPair::DecodeImp(uint32_t num_rows_to_skip, + uint32_t start_row, uint32_t num_rows, + uint32_t offset_within_row, + const KeyRowArray& rows, + KeyColumnArray* col1, + KeyColumnArray* col2) { + ARROW_DCHECK(rows.length() >= start_row + num_rows); + ARROW_DCHECK(col1->length() == num_rows && col2->length() == num_rows); + + uint8_t* dst_A = col1->mutable_data(1); + uint8_t* dst_B = col2->mutable_data(1); + + uint32_t fixed_length = rows.metadata().fixed_length; + const uint32_t* offsets; + const uint8_t* src_base; + if (is_row_fixed_length) { + src_base = rows.data(1) + fixed_length * start_row + offset_within_row; + offsets = nullptr; + } else { + src_base = rows.data(2) + offset_within_row; + offsets = rows.offsets() + start_row; + } + + using col1_type_const = typename std::add_const::type; + using col2_type_const = typename std::add_const::type; + + if (is_row_fixed_length) { + const uint8_t* src = src_base + num_rows_to_skip * fixed_length; + for (uint32_t i = num_rows_to_skip; i < num_rows; ++i) { + reinterpret_cast(dst_A)[i] = *reinterpret_cast(src); + reinterpret_cast(dst_B)[i] = + *reinterpret_cast(src + sizeof(col1_type)); + src += fixed_length; + } + } else { + for (uint32_t i = num_rows_to_skip; i < num_rows; ++i) { + const uint8_t* src = src_base + offsets[i]; + reinterpret_cast(dst_A)[i] = *reinterpret_cast(src); + reinterpret_cast(dst_B)[i] = + *reinterpret_cast(src + sizeof(col1_type)); + } + } +} + +void KeyEncoder::EncoderOffsets::Encode(KeyRowArray* rows, + const std::vector& varbinary_cols, + KeyEncoderContext* ctx) { + ARROW_DCHECK(!varbinary_cols.empty()); + + // Rows and columns must all be varying-length + ARROW_DCHECK(!rows->metadata().is_fixed_length); + for (size_t col = 0; col < varbinary_cols.size(); ++col) { + ARROW_DCHECK(!varbinary_cols[col].metadata().is_fixed_length); + } + + uint32_t num_rows = static_cast(varbinary_cols[0].length()); + + // The space in columns must be exactly equal to a space for offsets in rows + ARROW_DCHECK(rows->length() == num_rows); + for (size_t col = 0; col < varbinary_cols.size(); ++col) { + ARROW_DCHECK(varbinary_cols[col].length() == num_rows); + } + + uint32_t num_processed = 0; +#if defined(ARROW_HAVE_AVX2) + if (ctx->has_avx2()) { + // Create a temp vector sized based on the number of columns + auto temp_buffer_holder = util::TempVectorHolder( + ctx->stack, static_cast(varbinary_cols.size()) * 8); + auto temp_buffer_32B_per_col = KeyColumnArray( + KeyColumnMetadata(true, sizeof(uint32_t)), varbinary_cols.size() * 8, nullptr, + reinterpret_cast(temp_buffer_holder.mutable_data()), nullptr); + + num_processed = EncodeImp_avx2(rows, varbinary_cols, &temp_buffer_32B_per_col); + } +#endif + if (num_processed < num_rows) { + EncodeImp(num_processed, rows, varbinary_cols); + } +} + +void KeyEncoder::EncoderOffsets::EncodeImp( + uint32_t num_rows_already_processed, KeyRowArray* rows, + const std::vector& varbinary_cols) { + ARROW_DCHECK(varbinary_cols.size() > 0); + + int row_alignment = rows->metadata().row_alignment; + int string_alignment = rows->metadata().string_alignment; + + uint32_t* row_offsets = rows->mutable_offsets(); + uint8_t* row_values = rows->mutable_data(2); + uint32_t num_rows = static_cast(varbinary_cols[0].length()); + + if (num_rows_already_processed == 0) { + row_offsets[0] = 0; + } + + uint32_t row_offset = row_offsets[num_rows_already_processed]; + for (uint32_t i = num_rows_already_processed; i < num_rows; ++i) { + uint32_t* varbinary_end = + rows->metadata().varbinary_end_array(row_values + row_offset); + + // Zero out lengths for nulls. + // Add lengths of all columns to get row size. + // Store varbinary field ends while summing their lengths. + + uint32_t offset_within_row = rows->metadata().fixed_length; + + for (size_t col = 0; col < varbinary_cols.size(); ++col) { + const uint32_t* col_offsets = varbinary_cols[col].offsets(); + uint32_t col_length = col_offsets[i + 1] - col_offsets[i]; + + const uint8_t* non_nulls = varbinary_cols[col].data(0); + if (non_nulls && BitUtil::GetBit(non_nulls, i) == 0) { + col_length = 0; + } + + offset_within_row += + KeyRowMetadata::padding_for_alignment(offset_within_row, string_alignment); + offset_within_row += col_length; + + varbinary_end[col] = offset_within_row; + } + + offset_within_row += + KeyRowMetadata::padding_for_alignment(offset_within_row, row_alignment); + row_offset += offset_within_row; + row_offsets[i + 1] = row_offset; + } +} + +void KeyEncoder::EncoderOffsets::Decode( + uint32_t start_row, uint32_t num_rows, const KeyRowArray& rows, + std::vector* varbinary_cols, + const std::vector& varbinary_cols_base_offset, KeyEncoderContext* ctx) { + ARROW_DCHECK(!varbinary_cols->empty()); + ARROW_DCHECK(varbinary_cols->size() == varbinary_cols_base_offset.size()); + + // Rows and columns must all be varying-length + ARROW_DCHECK(!rows.metadata().is_fixed_length); + for (size_t col = 0; col < varbinary_cols->size(); ++col) { + ARROW_DCHECK(!(*varbinary_cols)[col].metadata().is_fixed_length); + } + + // The space in columns must be exactly equal to a subset of rows selected + ARROW_DCHECK(rows.length() >= start_row + num_rows); + for (size_t col = 0; col < varbinary_cols->size(); ++col) { + ARROW_DCHECK((*varbinary_cols)[col].length() == num_rows); + } + + // Offsets of varbinary columns data within each encoded row are stored + // in the same encoded row as an array of 32-bit integers. + // This array follows immediately the data of fixed-length columns. + // There is one element for each varying-length column. + // The Nth element is the sum of all the lengths of varbinary columns data in + // that row, up to and including Nth varbinary column. + + const uint32_t* row_offsets = rows.offsets() + start_row; + + // Set the base offset for each column + for (size_t col = 0; col < varbinary_cols->size(); ++col) { + uint32_t* col_offsets = (*varbinary_cols)[col].mutable_offsets(); + col_offsets[0] = varbinary_cols_base_offset[col]; + } + + int string_alignment = rows.metadata().string_alignment; + + for (uint32_t i = 0; i < num_rows; ++i) { + // Find the beginning of cumulative lengths array for next row + const uint8_t* row = rows.data(2) + row_offsets[i]; + const uint32_t* varbinary_ends = rows.metadata().varbinary_end_array(row); + + // Update the offset of each column + uint32_t offset_within_row = rows.metadata().fixed_length; + for (size_t col = 0; col < varbinary_cols->size(); ++col) { + offset_within_row += + KeyRowMetadata::padding_for_alignment(offset_within_row, string_alignment); + uint32_t length = varbinary_ends[col] - offset_within_row; + offset_within_row = varbinary_ends[col]; + uint32_t* col_offsets = (*varbinary_cols)[col].mutable_offsets(); + col_offsets[i + 1] = col_offsets[i] + length; + } + } +} + +void KeyEncoder::EncoderVarBinary::Encode(uint32_t varbinary_col_id, KeyRowArray* rows, + const KeyColumnArray& col, + KeyEncoderContext* ctx) { +#if defined(ARROW_HAVE_AVX2) + if (ctx->has_avx2()) { + EncodeHelper_avx2(varbinary_col_id, rows, col); + } else { +#endif + if (varbinary_col_id == 0) { + EncodeImp(varbinary_col_id, rows, col); + } else { + EncodeImp(varbinary_col_id, rows, col); + } +#if defined(ARROW_HAVE_AVX2) + } +#endif +} + +void KeyEncoder::EncoderVarBinary::Decode(uint32_t start_row, uint32_t num_rows, + uint32_t varbinary_col_id, + const KeyRowArray& rows, KeyColumnArray* col, + KeyEncoderContext* ctx) { + // Output column varbinary buffer needs an extra 32B + // at the end in avx2 version and 8B otherwise. +#if defined(ARROW_HAVE_AVX2) + if (ctx->has_avx2()) { + DecodeHelper_avx2(start_row, num_rows, varbinary_col_id, rows, col); + } else { +#endif + if (varbinary_col_id == 0) { + DecodeImp(start_row, num_rows, varbinary_col_id, rows, col); + } else { + DecodeImp(start_row, num_rows, varbinary_col_id, rows, col); + } +#if defined(ARROW_HAVE_AVX2) + } +#endif +} + +template +void KeyEncoder::EncoderVarBinary::EncodeImp(uint32_t varbinary_col_id, KeyRowArray* rows, + const KeyColumnArray& col) { + EncodeDecodeHelper( + 0, static_cast(col.length()), varbinary_col_id, rows, rows, &col, nullptr, + [](uint8_t* dst, const uint8_t* src, int64_t length) { + uint64_t* dst64 = reinterpret_cast(dst); + const uint64_t* src64 = reinterpret_cast(src); + uint32_t istripe; + for (istripe = 0; istripe < length / 8; ++istripe) { + dst64[istripe] = util::SafeLoad(src64 + istripe); + } + if ((length % 8) > 0) { + uint64_t mask_last = ~0ULL >> (8 * (8 * (istripe + 1) - length)); + dst64[istripe] = (dst64[istripe] & ~mask_last) | + (util::SafeLoad(src64 + istripe) & mask_last); + } + }); +} + +template +void KeyEncoder::EncoderVarBinary::DecodeImp(uint32_t start_row, uint32_t num_rows, + uint32_t varbinary_col_id, + const KeyRowArray& rows, + KeyColumnArray* col) { + EncodeDecodeHelper( + start_row, num_rows, varbinary_col_id, &rows, nullptr, col, col, + [](uint8_t* dst, const uint8_t* src, int64_t length) { + for (uint32_t istripe = 0; istripe < (length + 7) / 8; ++istripe) { + uint64_t* dst64 = reinterpret_cast(dst); + const uint64_t* src64 = reinterpret_cast(src); + util::SafeStore(dst64 + istripe, src64[istripe]); + } + }); +} + +void KeyEncoder::EncoderNulls::Encode(KeyRowArray* rows, + const std::vector& cols, + KeyEncoderContext* ctx, + KeyColumnArray* temp_vector_16bit) { + ARROW_DCHECK(cols.size() > 0); + uint32_t num_rows = static_cast(rows->length()); + + // All input columns should have the same number of rows. + // They may or may not have non-nulls bit-vectors allocated. + for (size_t col = 0; col < cols.size(); ++col) { + ARROW_DCHECK(cols[col].length() == num_rows); + } + + // Temp vector needs space for the required number of rows + ARROW_DCHECK(temp_vector_16bit->length() >= num_rows); + ARROW_DCHECK(temp_vector_16bit->metadata().is_fixed_length && + temp_vector_16bit->metadata().fixed_length == sizeof(uint16_t)); + + uint8_t* null_masks = rows->null_masks(); + uint32_t null_masks_bytes_per_row = rows->metadata().null_masks_bytes_per_row; + memset(null_masks, 0, null_masks_bytes_per_row * num_rows); + for (size_t col = 0; col < cols.size(); ++col) { + const uint8_t* non_nulls = cols[col].data(0); + if (!non_nulls) { + continue; + } + int num_selected; + util::BitUtil::bits_to_indexes( + 0, ctx->cpu_info, num_rows, non_nulls, &num_selected, + reinterpret_cast(temp_vector_16bit->mutable_data(1))); + for (int i = 0; i < num_selected; ++i) { + uint16_t row_id = reinterpret_cast(temp_vector_16bit->data(1))[i]; + int64_t null_masks_bit_id = row_id * null_masks_bytes_per_row * 8 + col; + BitUtil::SetBit(null_masks, null_masks_bit_id); + } + } +} + +void KeyEncoder::EncoderNulls::Decode(uint32_t start_row, uint32_t num_rows, + const KeyRowArray& rows, + std::vector* cols) { + // Every output column needs to have a space for exactly the required number + // of rows. It also needs to have non-nulls bit-vector allocated and mutable. + ARROW_DCHECK(cols->size() > 0); + for (size_t col = 0; col < cols->size(); ++col) { + ARROW_DCHECK((*cols)[col].length() == num_rows); + ARROW_DCHECK((*cols)[col].mutable_data(0)); + } + + const uint8_t* null_masks = rows.null_masks(); + uint32_t null_masks_bytes_per_row = rows.metadata().null_masks_bytes_per_row; + for (size_t col = 0; col < cols->size(); ++col) { + uint8_t* non_nulls = (*cols)[col].mutable_data(0); + memset(non_nulls, 0xff, BitUtil::BytesForBits(num_rows)); + for (uint32_t row = 0; row < num_rows; ++row) { + uint32_t null_masks_bit_id = + (start_row + row) * null_masks_bytes_per_row * 8 + static_cast(col); + bool is_set = BitUtil::GetBit(null_masks, null_masks_bit_id); + if (is_set) { + BitUtil::ClearBit(non_nulls, row); + } + } + } +} + +uint32_t KeyEncoder::KeyRowMetadata::num_varbinary_cols() const { + uint32_t result = 0; + for (size_t i = 0; i < column_metadatas.size(); ++i) { + if (!column_metadatas[i].is_fixed_length) { + ++result; + } + } + return result; +} + +bool KeyEncoder::KeyRowMetadata::is_compatible(const KeyRowMetadata& other) const { + if (other.num_cols() != num_cols()) { + return false; + } + if (row_alignment != other.row_alignment || + string_alignment != other.string_alignment) { + return false; + } + for (size_t i = 0; i < column_metadatas.size(); ++i) { + if (column_metadatas[i].is_fixed_length != + other.column_metadatas[i].is_fixed_length) { + return false; + } + if (column_metadatas[i].fixed_length != other.column_metadatas[i].fixed_length) { + return false; + } + } + return true; +} + +void KeyEncoder::KeyRowMetadata::FromColumnMetadataVector( + const std::vector& cols, int in_row_alignment, + int in_string_alignment) { + column_metadatas.resize(cols.size()); + for (size_t i = 0; i < cols.size(); ++i) { + column_metadatas[i] = cols[i]; + } + + uint32_t num_cols = static_cast(cols.size()); + + // Sort columns. + // Columns are sorted based on the size in bytes of their fixed-length part. + // For the varying-length column, the fixed-length part is the 32-bit field storing + // cumulative length of varying-length fields. + // The rules are: + // a) Boolean column, marked with fixed-length 0, is considered to have fixed-length + // part of 1 byte. b) Columns with fixed-length part being power of 2 or multiple of row + // alignment precede other columns. They are sorted among themselves based on size of + // fixed-length part. c) Fixed-length columns precede varying-length columns when both + // have the same size fixed-length part. + column_order.resize(num_cols); + for (uint32_t i = 0; i < num_cols; ++i) { + column_order[i] = i; + } + std::sort( + column_order.begin(), column_order.end(), [&cols](uint32_t left, uint32_t right) { + bool is_left_pow2 = + !cols[left].is_fixed_length || ARROW_POPCOUNT64(cols[left].fixed_length) <= 1; + bool is_right_pow2 = !cols[right].is_fixed_length || + ARROW_POPCOUNT64(cols[right].fixed_length) <= 1; + bool is_left_fixedlen = cols[left].is_fixed_length; + bool is_right_fixedlen = cols[right].is_fixed_length; + uint32_t width_left = + cols[left].is_fixed_length ? cols[left].fixed_length : sizeof(uint32_t); + uint32_t width_right = + cols[right].is_fixed_length ? cols[right].fixed_length : sizeof(uint32_t); + if (is_left_pow2 != is_right_pow2) { + return is_left_pow2; + } + if (!is_left_pow2) { + return left < right; + } + if (width_left != width_right) { + return width_left > width_right; + } + if (is_left_fixedlen != is_right_fixedlen) { + return is_left_fixedlen; + } + return left < right; + }); + + row_alignment = in_row_alignment; + string_alignment = in_string_alignment; + varbinary_end_array_offset = 0; + + column_offsets.resize(num_cols); + uint32_t num_varbinary_cols = 0; + uint32_t offset_within_row = 0; + for (uint32_t i = 0; i < num_cols; ++i) { + const KeyColumnMetadata& col = cols[column_order[i]]; + offset_within_row += + KeyRowMetadata::padding_for_alignment(offset_within_row, string_alignment, col); + column_offsets[i] = offset_within_row; + if (!col.is_fixed_length) { + if (num_varbinary_cols == 0) { + varbinary_end_array_offset = offset_within_row; + } + ARROW_DCHECK(column_offsets[i] - varbinary_end_array_offset == + num_varbinary_cols * sizeof(uint32_t)); + ++num_varbinary_cols; + offset_within_row += sizeof(uint32_t); + } else { + // Boolean column is a bit-vector, which is indicated by + // setting fixed length in column metadata to zero. + // It will be stored as a byte in output row. + if (col.fixed_length == 0) { + offset_within_row += 1; + } else { + offset_within_row += col.fixed_length; + } + } + } + + is_fixed_length = (num_varbinary_cols == 0); + fixed_length = + offset_within_row + + KeyRowMetadata::padding_for_alignment( + offset_within_row, num_varbinary_cols == 0 ? row_alignment : string_alignment); + + // We set the number of bytes per row storing null masks of individual key columns + // to be a power of two. This is not required. It could be also set to the minimal + // number of bytes required for a given number of bits (one bit per column). + null_masks_bytes_per_row = 1; + while (static_cast(null_masks_bytes_per_row * 8) < num_cols) { + null_masks_bytes_per_row *= 2; + } +} + +void KeyEncoder::Init(const std::vector& cols, KeyEncoderContext* ctx, + int row_alignment, int string_alignment) { + ctx_ = ctx; + row_metadata_.FromColumnMetadataVector(cols, row_alignment, string_alignment); + uint32_t num_cols = row_metadata_.num_cols(); + uint32_t num_varbinary_cols = row_metadata_.num_varbinary_cols(); + batch_all_cols_.resize(num_cols); + batch_varbinary_cols_.resize(num_varbinary_cols); + batch_varbinary_cols_base_offsets_.resize(num_varbinary_cols); +} + +void KeyEncoder::PrepareKeyColumnArrays(int64_t start_row, int64_t num_rows, + const std::vector& cols_in) { + uint32_t num_cols = static_cast(cols_in.size()); + ARROW_DCHECK(batch_all_cols_.size() == num_cols); + + uint32_t num_varbinary_visited = 0; + for (uint32_t i = 0; i < num_cols; ++i) { + const KeyColumnArray& col = cols_in[row_metadata_.column_order[i]]; + KeyColumnArray col_window(col, start_row, num_rows); + batch_all_cols_[i] = col_window; + if (!col.metadata().is_fixed_length) { + ARROW_DCHECK(num_varbinary_visited < batch_varbinary_cols_.size()); + // If start row is zero, then base offset of varbinary column is also zero. + if (start_row == 0) { + batch_varbinary_cols_base_offsets_[num_varbinary_visited] = 0; + } else { + batch_varbinary_cols_base_offsets_[num_varbinary_visited] = + col.offsets()[start_row]; + } + batch_varbinary_cols_[num_varbinary_visited++] = col_window; + } + } +} + +Status KeyEncoder::PrepareOutputForEncode(int64_t start_row, int64_t num_rows, + KeyRowArray* rows, + const std::vector& all_cols) { + int64_t num_bytes_required = 0; + + int64_t fixed_part = row_metadata_.fixed_length * num_rows; + int64_t var_part = 0; + for (size_t i = 0; i < all_cols.size(); ++i) { + const KeyColumnArray& col = all_cols[i]; + if (!col.metadata().is_fixed_length) { + ARROW_DCHECK(col.length() >= start_row + num_rows); + const uint32_t* offsets = col.offsets(); + var_part += offsets[start_row + num_rows] - offsets[start_row]; + // Include maximum padding that can be added to align the start of varbinary fields. + var_part += num_rows * row_metadata_.string_alignment; + } + } + // Include maximum padding that can be added to align the start of the rows. + if (!row_metadata_.is_fixed_length) { + fixed_part += row_metadata_.row_alignment * num_rows; + } + num_bytes_required = fixed_part + var_part; + + rows->Clean(); + RETURN_NOT_OK(rows->AppendEmpty(static_cast(num_rows), + static_cast(num_bytes_required))); + + return Status::OK(); +} + +void KeyEncoder::Encode(int64_t start_row, int64_t num_rows, KeyRowArray* rows, + const std::vector& cols) { + // Prepare column array vectors + PrepareKeyColumnArrays(start_row, num_rows, cols); + + // Create two temp vectors with 16-bit elements + auto temp_buffer_holder_A = + util::TempVectorHolder(ctx_->stack, static_cast(num_rows)); + auto temp_buffer_A = KeyColumnArray( + KeyColumnMetadata(true, sizeof(uint16_t)), num_rows, nullptr, + reinterpret_cast(temp_buffer_holder_A.mutable_data()), nullptr); + auto temp_buffer_holder_B = + util::TempVectorHolder(ctx_->stack, static_cast(num_rows)); + auto temp_buffer_B = KeyColumnArray( + KeyColumnMetadata(true, sizeof(uint16_t)), num_rows, nullptr, + reinterpret_cast(temp_buffer_holder_B.mutable_data()), nullptr); + + bool is_row_fixed_length = row_metadata_.is_fixed_length; + if (!is_row_fixed_length) { + // This call will generate and fill in data for both: + // - offsets to the entire encoded arrays + // - offsets for individual varbinary fields within each row + EncoderOffsets::Encode(rows, batch_varbinary_cols_, ctx_); + + uint32_t num_varbinary_cols = static_cast(batch_varbinary_cols_.size()); + for (uint32_t i = 0; i < num_varbinary_cols; ++i) { + // Memcpy varbinary fields into precomputed in the previous step + // positions in the output row buffer. + EncoderVarBinary::Encode(i, rows, batch_varbinary_cols_[i], ctx_); + } + } + + // Process fixed length columns + uint32_t num_cols = static_cast(batch_all_cols_.size()); + for (uint32_t i = 0; i < num_cols;) { + if (!batch_all_cols_[i].metadata().is_fixed_length) { + i += 1; + continue; + } + bool can_process_pair = + (i + 1 < num_cols) && batch_all_cols_[i + 1].metadata().is_fixed_length && + EncoderBinaryPair::CanProcessPair(batch_all_cols_[i].metadata(), + batch_all_cols_[i + 1].metadata()); + if (!can_process_pair) { + EncoderBinary::Encode(row_metadata_.column_offsets[i], rows, batch_all_cols_[i], + ctx_, &temp_buffer_A); + i += 1; + } else { + EncoderBinaryPair::Encode(row_metadata_.column_offsets[i], rows, batch_all_cols_[i], + batch_all_cols_[i + 1], ctx_, &temp_buffer_A, + &temp_buffer_B); + i += 2; + } + } + + // Process nulls + EncoderNulls::Encode(rows, batch_all_cols_, ctx_, &temp_buffer_A); +} + +void KeyEncoder::DecodeFixedLengthBuffers(int64_t start_row_input, + int64_t start_row_output, int64_t num_rows, + const KeyRowArray& rows, + std::vector* cols) { + // Prepare column array vectors + PrepareKeyColumnArrays(start_row_output, num_rows, *cols); + + // Create two temp vectors with 16-bit elements + auto temp_buffer_holder_A = + util::TempVectorHolder(ctx_->stack, static_cast(num_rows)); + auto temp_buffer_A = KeyColumnArray( + KeyColumnMetadata(true, sizeof(uint16_t)), num_rows, nullptr, + reinterpret_cast(temp_buffer_holder_A.mutable_data()), nullptr); + auto temp_buffer_holder_B = + util::TempVectorHolder(ctx_->stack, static_cast(num_rows)); + auto temp_buffer_B = KeyColumnArray( + KeyColumnMetadata(true, sizeof(uint16_t)), num_rows, nullptr, + reinterpret_cast(temp_buffer_holder_B.mutable_data()), nullptr); + + bool is_row_fixed_length = row_metadata_.is_fixed_length; + if (!is_row_fixed_length) { + EncoderOffsets::Decode(static_cast(start_row_input), + static_cast(num_rows), rows, &batch_varbinary_cols_, + batch_varbinary_cols_base_offsets_, ctx_); + } + + // Process fixed length columns + uint32_t num_cols = static_cast(batch_all_cols_.size()); + for (uint32_t i = 0; i < num_cols;) { + if (!batch_all_cols_[i].metadata().is_fixed_length) { + i += 1; + continue; + } + bool can_process_pair = + (i + 1 < num_cols) && batch_all_cols_[i + 1].metadata().is_fixed_length && + EncoderBinaryPair::CanProcessPair(batch_all_cols_[i].metadata(), + batch_all_cols_[i + 1].metadata()); + if (!can_process_pair) { + EncoderBinary::Decode(static_cast(start_row_input), + static_cast(num_rows), + row_metadata_.column_offsets[i], rows, &batch_all_cols_[i], + ctx_, &temp_buffer_A); + i += 1; + } else { + EncoderBinaryPair::Decode( + static_cast(start_row_input), static_cast(num_rows), + row_metadata_.column_offsets[i], rows, &batch_all_cols_[i], + &batch_all_cols_[i + 1], ctx_, &temp_buffer_A, &temp_buffer_B); + i += 2; + } + } + + // Process nulls + EncoderNulls::Decode(static_cast(start_row_input), + static_cast(num_rows), rows, &batch_all_cols_); +} + +void KeyEncoder::DecodeVaryingLengthBuffers(int64_t start_row_input, + int64_t start_row_output, int64_t num_rows, + const KeyRowArray& rows, + std::vector* cols) { + // Prepare column array vectors + PrepareKeyColumnArrays(start_row_output, num_rows, *cols); + + bool is_row_fixed_length = row_metadata_.is_fixed_length; + if (!is_row_fixed_length) { + uint32_t num_varbinary_cols = static_cast(batch_varbinary_cols_.size()); + for (uint32_t i = 0; i < num_varbinary_cols; ++i) { + // Memcpy varbinary fields into precomputed in the previous step + // positions in the output row buffer. + EncoderVarBinary::Decode(static_cast(start_row_input), + static_cast(num_rows), i, rows, + &batch_varbinary_cols_[i], ctx_); + } + } +} + +} // namespace compute +} // namespace arrow diff --git a/cpp/src/arrow/compute/exec/key_encode.h b/cpp/src/arrow/compute/exec/key_encode.h new file mode 100644 index 00000000000..452730deb19 --- /dev/null +++ b/cpp/src/arrow/compute/exec/key_encode.h @@ -0,0 +1,627 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#pragma once + +#include +#include +#include + +#include "arrow/compute/exec/util.h" +#include "arrow/memory_pool.h" +#include "arrow/result.h" +#include "arrow/status.h" +#include "arrow/util/bit_util.h" + +namespace arrow { +namespace compute { + +class KeyColumnMetadata; + +/// Converts between key representation as a collection of arrays for +/// individual columns and another representation as a single array of rows +/// combining data from all columns into one value. +/// This conversion is reversible. +/// Row-oriented storage is beneficial when there is a need for random access +/// of individual rows and at the same time all included columns are likely to +/// be accessed together, as in the case of hash table key. +class KeyEncoder { + public: + struct KeyEncoderContext { + bool has_avx2() const { + return cpu_info->IsSupported(arrow::internal::CpuInfo::AVX2); + } + const arrow::internal::CpuInfo* cpu_info; + util::TempVectorStack* stack; + }; + + /// Description of a storage format of a single key column as needed + /// for the purpose of row encoding. + struct KeyColumnMetadata { + KeyColumnMetadata() = default; + KeyColumnMetadata(bool is_fixed_length_in, uint32_t fixed_length_in) + : is_fixed_length(is_fixed_length_in), fixed_length(fixed_length_in) {} + /// Is column storing a varying-length binary, using offsets array + /// to find a beginning of a value, or is it a fixed-length binary. + bool is_fixed_length; + /// For a fixed-length binary column: number of bytes per value. + /// Zero has a special meaning, indicating a bit vector with one bit per value. + /// For a varying-length binary column: number of bytes per offset. + uint32_t fixed_length; + }; + + /// Description of a storage format for rows produced by encoder. + struct KeyRowMetadata { + /// Is row a varying-length binary, using offsets array to find a beginning of a row, + /// or is it a fixed-length binary. + bool is_fixed_length; + + /// For a fixed-length binary row, common size of rows in bytes, + /// rounded up to the multiple of alignment. + /// + /// For a varying-length binary, size of all encoded fixed-length key columns, + /// including lengths of varying-length columns, rounded up to the multiple of string + /// alignment. + uint32_t fixed_length; + + /// Offset within a row to the array of 32-bit offsets within a row of + /// ends of varbinary fields. + /// Used only when the row is not fixed-length, zero for fixed-length row. + /// There are N elements for N varbinary fields. + /// Each element is the offset within a row of the first byte after + /// the corresponding varbinary field bytes in that row. + /// If varbinary fields begin at aligned addresses, than the end of the previous + /// varbinary field needs to be rounded up according to the specified alignment + /// to obtain the beginning of the next varbinary field. + /// The first varbinary field starts at offset specified by fixed_length, + /// which should already be aligned. + uint32_t varbinary_end_array_offset; + + /// Fixed number of bytes per row that are used to encode null masks. + /// Null masks indicate for a single row which of its key columns are null. + /// Nth bit in the sequence of bytes assigned to a row represents null + /// information for Nth field according to the order in which they are encoded. + int null_masks_bytes_per_row; + + /// Power of 2. Every row will start at the offset aligned to that number of bytes. + int row_alignment; + + /// Power of 2. Must be no greater than row alignment. + /// Every non-power-of-2 binary field and every varbinary field bytes + /// will start aligned to that number of bytes. + int string_alignment; + + /// Metadata of encoded columns in their original order. + std::vector column_metadatas; + + /// Order in which fields are encoded. + std::vector column_order; + + /// Offsets within a row to fields in their encoding order. + std::vector column_offsets; + + /// Rounding up offset to the nearest multiple of alignment value. + /// Alignment must be a power of 2. + static inline uint32_t padding_for_alignment(uint32_t offset, + int required_alignment) { + ARROW_DCHECK(ARROW_POPCOUNT64(required_alignment) == 1); + return static_cast((-static_cast(offset)) & + (required_alignment - 1)); + } + + /// Rounding up offset to the beginning of next column, + /// chosing required alignment based on the data type of that column. + static inline uint32_t padding_for_alignment(uint32_t offset, int string_alignment, + const KeyColumnMetadata& col_metadata) { + if (!col_metadata.is_fixed_length || + ARROW_POPCOUNT64(col_metadata.fixed_length) <= 1) { + return 0; + } else { + return padding_for_alignment(offset, string_alignment); + } + } + + /// Returns an array of offsets within a row of ends of varbinary fields. + inline const uint32_t* varbinary_end_array(const uint8_t* row) const { + ARROW_DCHECK(!is_fixed_length); + return reinterpret_cast(row + varbinary_end_array_offset); + } + inline uint32_t* varbinary_end_array(uint8_t* row) const { + ARROW_DCHECK(!is_fixed_length); + return reinterpret_cast(row + varbinary_end_array_offset); + } + + /// Returns the offset within the row and length of the first varbinary field. + inline void first_varbinary_offset_and_length(const uint8_t* row, uint32_t* offset, + uint32_t* length) const { + ARROW_DCHECK(!is_fixed_length); + *offset = fixed_length; + *length = varbinary_end_array(row)[0] - fixed_length; + } + + /// Returns the offset within the row and length of the second and further varbinary + /// fields. + inline void nth_varbinary_offset_and_length(const uint8_t* row, int varbinary_id, + uint32_t* out_offset, + uint32_t* out_length) const { + ARROW_DCHECK(!is_fixed_length); + ARROW_DCHECK(varbinary_id > 0); + const uint32_t* varbinary_end = varbinary_end_array(row); + uint32_t offset = varbinary_end[varbinary_id - 1]; + offset += padding_for_alignment(offset, string_alignment); + *out_offset = offset; + *out_length = varbinary_end[varbinary_id] - offset; + } + + uint32_t encoded_field_order(uint32_t icol) const { return column_order[icol]; } + + uint32_t encoded_field_offset(uint32_t icol) const { return column_offsets[icol]; } + + uint32_t num_cols() const { return static_cast(column_metadatas.size()); } + + uint32_t num_varbinary_cols() const; + + void FromColumnMetadataVector(const std::vector& cols, + int in_row_alignment, int in_string_alignment); + + bool is_compatible(const KeyRowMetadata& other) const; + }; + + class KeyRowArray { + public: + KeyRowArray(); + Status Init(MemoryPool* pool, const KeyRowMetadata& metadata); + void Clean(); + Status AppendEmpty(uint32_t num_rows_to_append, uint32_t num_extra_bytes_to_append); + Status AppendSelectionFrom(const KeyRowArray& from, uint32_t num_rows_to_append, + const uint16_t* source_row_ids); + const KeyRowMetadata& metadata() const { return metadata_; } + int64_t length() const { return num_rows_; } + const uint8_t* data(int i) const { + ARROW_DCHECK(i >= 0 && i <= max_buffers_); + return buffers_[i]; + } + uint8_t* mutable_data(int i) { + ARROW_DCHECK(i >= 0 && i <= max_buffers_); + return mutable_buffers_[i]; + } + const uint32_t* offsets() const { return reinterpret_cast(data(1)); } + uint32_t* mutable_offsets() { return reinterpret_cast(mutable_data(1)); } + const uint8_t* null_masks() const { return null_masks_->data(); } + uint8_t* null_masks() { return null_masks_->mutable_data(); } + + bool has_any_nulls(const KeyEncoderContext* ctx) const; + + private: + Status ResizeFixedLengthBuffers(int64_t num_extra_rows); + Status ResizeOptionalVaryingLengthBuffer(int64_t num_extra_bytes); + + int64_t size_null_masks(int64_t num_rows); + int64_t size_offsets(int64_t num_rows); + int64_t size_rows_fixed_length(int64_t num_rows); + int64_t size_rows_varying_length(int64_t num_bytes); + void update_buffer_pointers(); + + static constexpr int64_t padding_for_vectors = 64; + MemoryPool* pool_; + KeyRowMetadata metadata_; + /// Buffers can only expand during lifetime and never shrink. + std::unique_ptr null_masks_; + std::unique_ptr offsets_; + std::unique_ptr rows_; + static constexpr int max_buffers_ = 3; + const uint8_t* buffers_[max_buffers_]; + uint8_t* mutable_buffers_[max_buffers_]; + int64_t num_rows_; + int64_t rows_capacity_; + int64_t bytes_capacity_; + + // Mutable to allow lazy evaluation + mutable int64_t num_rows_for_has_any_nulls_; + mutable bool has_any_nulls_; + }; + + /// A lightweight description of an array representing one of key columns. + class KeyColumnArray { + public: + KeyColumnArray() = default; + /// Create as a mix of buffers according to the mask from two descriptions + /// (Nth bit is set to 0 if Nth buffer from the first input + /// should be used and is set to 1 otherwise). + /// Metadata is inherited from the first input. + KeyColumnArray(const KeyColumnMetadata& metadata, const KeyColumnArray& left, + const KeyColumnArray& right, int buffer_id_to_replace); + /// Create for reading + KeyColumnArray(const KeyColumnMetadata& metadata, int64_t length, + const uint8_t* buffer0, const uint8_t* buffer1, + const uint8_t* buffer2); + /// Create for writing + KeyColumnArray(const KeyColumnMetadata& metadata, int64_t length, uint8_t* buffer0, + uint8_t* buffer1, uint8_t* buffer2); + /// Create as a window view of original description that is offset + /// by a given number of rows. + /// The number of rows used in offset must be divisible by 8 + /// in order to not split bit vectors within a single byte. + KeyColumnArray(const KeyColumnArray& from, int64_t start, int64_t length); + uint8_t* mutable_data(int i) { + ARROW_DCHECK(i >= 0 && i <= max_buffers_); + return mutable_buffers_[i]; + } + const uint8_t* data(int i) const { + ARROW_DCHECK(i >= 0 && i <= max_buffers_); + return buffers_[i]; + } + uint32_t* mutable_offsets() { return reinterpret_cast(mutable_data(1)); } + const uint32_t* offsets() const { return reinterpret_cast(data(1)); } + const KeyColumnMetadata& metadata() const { return metadata_; } + int64_t length() const { return length_; } + + private: + static constexpr int max_buffers_ = 3; + const uint8_t* buffers_[max_buffers_]; + uint8_t* mutable_buffers_[max_buffers_]; + KeyColumnMetadata metadata_; + int64_t length_; + }; + + void Init(const std::vector& cols, KeyEncoderContext* ctx, + int row_alignment, int string_alignment); + + const KeyRowMetadata& row_metadata() { return row_metadata_; } + + /// Find out the required sizes of all buffers output buffers for encoding + /// (including varying-length buffers). + /// Use that information to resize provided row array so that it can fit + /// encoded data. + Status PrepareOutputForEncode(int64_t start_input_row, int64_t num_input_rows, + KeyRowArray* rows, + const std::vector& all_cols); + + /// Encode a window of column oriented data into the entire output + /// row oriented storage. + /// The output buffers for encoding need to be correctly sized before + /// starting encoding. + void Encode(int64_t start_input_row, int64_t num_input_rows, KeyRowArray* rows, + const std::vector& cols); + + /// Decode a window of row oriented data into a corresponding + /// window of column oriented storage. + /// The output buffers need to be correctly allocated and sized before + /// calling each method. + /// For that reason decoding is split into two functions. + /// The output of the first one, that processes everything except for + /// varying length buffers, can be used to find out required varying + /// length buffers sizes. + void DecodeFixedLengthBuffers(int64_t start_row_input, int64_t start_row_output, + int64_t num_rows, const KeyRowArray& rows, + std::vector* cols); + + void DecodeVaryingLengthBuffers(int64_t start_row_input, int64_t start_row_output, + int64_t num_rows, const KeyRowArray& rows, + std::vector* cols); + + private: + /// Prepare column array vectors. + /// Output column arrays represent a range of input column arrays + /// specified by starting row and number of rows. + /// Three vectors are generated: + /// - all columns + /// - fixed-length columns only + /// - varying-length columns only + void PrepareKeyColumnArrays(int64_t start_row, int64_t num_rows, + const std::vector& cols_in); + + class TransformBoolean { + public: + static KeyColumnArray ArrayReplace(const KeyColumnArray& column, + const KeyColumnArray& temp); + static void PreEncode(const KeyColumnArray& input, KeyColumnArray* output, + KeyEncoderContext* ctx); + static void PostDecode(const KeyColumnArray& input, KeyColumnArray* output, + KeyEncoderContext* ctx); + }; + + class EncoderInteger { + public: + static void Encode(uint32_t offset_within_row, KeyRowArray* rows, + const KeyColumnArray& col, KeyEncoderContext* ctx, + KeyColumnArray* temp); + static void Decode(uint32_t start_row, uint32_t num_rows, uint32_t offset_within_row, + const KeyRowArray& rows, KeyColumnArray* col, + KeyEncoderContext* ctx, KeyColumnArray* temp); + static bool UsesTransform(const KeyColumnArray& column); + static KeyColumnArray ArrayReplace(const KeyColumnArray& column, + const KeyColumnArray& temp); + static void PreEncode(const KeyColumnArray& input, KeyColumnArray* output, + KeyEncoderContext* ctx); + static void PostDecode(const KeyColumnArray& input, KeyColumnArray* output, + KeyEncoderContext* ctx); + + private: + static bool IsBoolean(const KeyColumnMetadata& metadata); + }; + + class EncoderBinary { + public: + static void Encode(uint32_t offset_within_row, KeyRowArray* rows, + const KeyColumnArray& col, KeyEncoderContext* ctx, + KeyColumnArray* temp); + static void Decode(uint32_t start_row, uint32_t num_rows, uint32_t offset_within_row, + const KeyRowArray& rows, KeyColumnArray* col, + KeyEncoderContext* ctx, KeyColumnArray* temp); + static bool IsInteger(const KeyColumnMetadata& metadata); + + private: + template + static inline void EncodeDecodeHelper(uint32_t start_row, uint32_t num_rows, + uint32_t offset_within_row, + const KeyRowArray* rows_const, + KeyRowArray* rows_mutable_maybe_null, + const KeyColumnArray* col_const, + KeyColumnArray* col_mutable_maybe_null, + COPY_FN copy_fn); + template + static void EncodeImp(uint32_t offset_within_row, KeyRowArray* rows, + const KeyColumnArray& col); + template + static void DecodeImp(uint32_t start_row, uint32_t num_rows, + uint32_t offset_within_row, const KeyRowArray& rows, + KeyColumnArray* col); +#if defined(ARROW_HAVE_AVX2) + static void EncodeHelper_avx2(bool is_row_fixed_length, uint32_t offset_within_row, + KeyRowArray* rows, const KeyColumnArray& col); + static void DecodeHelper_avx2(bool is_row_fixed_length, uint32_t start_row, + uint32_t num_rows, uint32_t offset_within_row, + const KeyRowArray& rows, KeyColumnArray* col); + template + static void EncodeImp_avx2(uint32_t offset_within_row, KeyRowArray* rows, + const KeyColumnArray& col); + template + static void DecodeImp_avx2(uint32_t start_row, uint32_t num_rows, + uint32_t offset_within_row, const KeyRowArray& rows, + KeyColumnArray* col); +#endif + static void ColumnMemsetNulls(uint32_t offset_within_row, KeyRowArray* rows, + const KeyColumnArray& col, KeyEncoderContext* ctx, + KeyColumnArray* temp_vector_16bit, uint8_t byte_value); + template + static void ColumnMemsetNullsImp(uint32_t offset_within_row, KeyRowArray* rows, + const KeyColumnArray& col, KeyEncoderContext* ctx, + KeyColumnArray* temp_vector_16bit, + uint8_t byte_value); + }; + + class EncoderBinaryPair { + public: + static bool CanProcessPair(const KeyColumnMetadata& col1, + const KeyColumnMetadata& col2) { + return EncoderBinary::IsInteger(col1) && EncoderBinary::IsInteger(col2); + } + static void Encode(uint32_t offset_within_row, KeyRowArray* rows, + const KeyColumnArray& col1, const KeyColumnArray& col2, + KeyEncoderContext* ctx, KeyColumnArray* temp1, + KeyColumnArray* temp2); + static void Decode(uint32_t start_row, uint32_t num_rows, uint32_t offset_within_row, + const KeyRowArray& rows, KeyColumnArray* col1, + KeyColumnArray* col2, KeyEncoderContext* ctx, + KeyColumnArray* temp1, KeyColumnArray* temp2); + + private: + template + static void EncodeImp(uint32_t num_rows_to_skip, uint32_t offset_within_row, + KeyRowArray* rows, const KeyColumnArray& col1, + const KeyColumnArray& col2); + template + static void DecodeImp(uint32_t num_rows_to_skip, uint32_t start_row, + uint32_t num_rows, uint32_t offset_within_row, + const KeyRowArray& rows, KeyColumnArray* col1, + KeyColumnArray* col2); +#if defined(ARROW_HAVE_AVX2) + static uint32_t EncodeHelper_avx2(bool is_row_fixed_length, uint32_t col_width, + uint32_t offset_within_row, KeyRowArray* rows, + const KeyColumnArray& col1, + const KeyColumnArray& col2); + static uint32_t DecodeHelper_avx2(bool is_row_fixed_length, uint32_t col_width, + uint32_t start_row, uint32_t num_rows, + uint32_t offset_within_row, const KeyRowArray& rows, + KeyColumnArray* col1, KeyColumnArray* col2); + template + static uint32_t EncodeImp_avx2(uint32_t offset_within_row, KeyRowArray* rows, + const KeyColumnArray& col1, + const KeyColumnArray& col2); + template + static uint32_t DecodeImp_avx2(uint32_t start_row, uint32_t num_rows, + uint32_t offset_within_row, const KeyRowArray& rows, + KeyColumnArray* col1, KeyColumnArray* col2); +#endif + }; + + class EncoderOffsets { + public: + // In order not to repeat work twice, + // encoding combines in a single pass computing of: + // a) row offsets for varying-length rows + // b) within each new row, the cumulative length array + // of varying-length values within a row. + static void Encode(KeyRowArray* rows, + const std::vector& varbinary_cols, + KeyEncoderContext* ctx); + static void Decode(uint32_t start_row, uint32_t num_rows, const KeyRowArray& rows, + std::vector* varbinary_cols, + const std::vector& varbinary_cols_base_offset, + KeyEncoderContext* ctx); + + private: + static void EncodeImp(uint32_t num_rows_already_processed, KeyRowArray* rows, + const std::vector& varbinary_cols); +#if defined(ARROW_HAVE_AVX2) + static uint32_t EncodeImp_avx2(KeyRowArray* rows, + const std::vector& varbinary_cols, + KeyColumnArray* temp_buffer_32B_per_col); +#endif + }; + + class EncoderVarBinary { + public: + static void Encode(uint32_t varbinary_col_id, KeyRowArray* rows, + const KeyColumnArray& col, KeyEncoderContext* ctx); + static void Decode(uint32_t start_row, uint32_t num_rows, uint32_t varbinary_col_id, + const KeyRowArray& rows, KeyColumnArray* col, + KeyEncoderContext* ctx); + + private: + template + static inline void EncodeDecodeHelper(uint32_t start_row, uint32_t num_rows, + uint32_t varbinary_col_id, + const KeyRowArray* rows_const, + KeyRowArray* rows_mutable_maybe_null, + const KeyColumnArray* col_const, + KeyColumnArray* col_mutable_maybe_null, + COPY_FN copy_fn); + template + static void EncodeImp(uint32_t varbinary_col_id, KeyRowArray* rows, + const KeyColumnArray& col); + template + static void DecodeImp(uint32_t start_row, uint32_t num_rows, + uint32_t varbinary_col_id, const KeyRowArray& rows, + KeyColumnArray* col); +#if defined(ARROW_HAVE_AVX2) + static void EncodeHelper_avx2(uint32_t varbinary_col_id, KeyRowArray* rows, + const KeyColumnArray& col); + static void DecodeHelper_avx2(uint32_t start_row, uint32_t num_rows, + uint32_t varbinary_col_id, const KeyRowArray& rows, + KeyColumnArray* col); + template + static void EncodeImp_avx2(uint32_t varbinary_col_id, KeyRowArray* rows, + const KeyColumnArray& col); + template + static void DecodeImp_avx2(uint32_t start_row, uint32_t num_rows, + uint32_t varbinary_col_id, const KeyRowArray& rows, + KeyColumnArray* col); +#endif + }; + + class EncoderNulls { + public: + static void Encode(KeyRowArray* rows, const std::vector& cols, + KeyEncoderContext* ctx, KeyColumnArray* temp_vector_16bit); + static void Decode(uint32_t start_row, uint32_t num_rows, const KeyRowArray& rows, + std::vector* cols); + }; + + KeyEncoderContext* ctx_; + + // Data initialized once, based on data types of key columns + KeyRowMetadata row_metadata_; + + // Data initialized for each input batch. + // All elements are ordered according to the order of encoded fields in a row. + std::vector batch_all_cols_; + std::vector batch_varbinary_cols_; + std::vector batch_varbinary_cols_base_offsets_; +}; + +template +inline void KeyEncoder::EncoderBinary::EncodeDecodeHelper( + uint32_t start_row, uint32_t num_rows, uint32_t offset_within_row, + const KeyRowArray* rows_const, KeyRowArray* rows_mutable_maybe_null, + const KeyColumnArray* col_const, KeyColumnArray* col_mutable_maybe_null, + COPY_FN copy_fn) { + ARROW_DCHECK(col_const && col_const->metadata().is_fixed_length); + uint32_t col_width = col_const->metadata().fixed_length; + + if (is_row_fixed_length) { + uint32_t row_width = rows_const->metadata().fixed_length; + for (uint32_t i = 0; i < num_rows; ++i) { + const uint8_t* src; + uint8_t* dst; + if (is_encoding) { + src = col_const->data(1) + col_width * i; + dst = rows_mutable_maybe_null->mutable_data(1) + row_width * (start_row + i) + + offset_within_row; + } else { + src = rows_const->data(1) + row_width * (start_row + i) + offset_within_row; + dst = col_mutable_maybe_null->mutable_data(1) + col_width * i; + } + copy_fn(dst, src, col_width); + } + } else { + const uint32_t* row_offsets = rows_const->offsets(); + for (uint32_t i = 0; i < num_rows; ++i) { + const uint8_t* src; + uint8_t* dst; + if (is_encoding) { + src = col_const->data(1) + col_width * i; + dst = rows_mutable_maybe_null->mutable_data(2) + row_offsets[start_row + i] + + offset_within_row; + } else { + src = rows_const->data(2) + row_offsets[start_row + i] + offset_within_row; + dst = col_mutable_maybe_null->mutable_data(1) + col_width * i; + } + copy_fn(dst, src, col_width); + } + } +} + +template +inline void KeyEncoder::EncoderVarBinary::EncodeDecodeHelper( + uint32_t start_row, uint32_t num_rows, uint32_t varbinary_col_id, + const KeyRowArray* rows_const, KeyRowArray* rows_mutable_maybe_null, + const KeyColumnArray* col_const, KeyColumnArray* col_mutable_maybe_null, + COPY_FN copy_fn) { + // Column and rows need to be varying length + ARROW_DCHECK(!rows_const->metadata().is_fixed_length && + !col_const->metadata().is_fixed_length); + + const uint32_t* row_offsets_for_batch = rows_const->offsets() + start_row; + const uint32_t* col_offsets = col_const->offsets(); + + uint32_t col_offset_next = col_offsets[0]; + for (uint32_t i = 0; i < num_rows; ++i) { + uint32_t col_offset = col_offset_next; + col_offset_next = col_offsets[i + 1]; + + uint32_t row_offset = row_offsets_for_batch[i]; + const uint8_t* row = rows_const->data(2) + row_offset; + + uint32_t offset_within_row; + uint32_t length; + if (first_varbinary_col) { + rows_const->metadata().first_varbinary_offset_and_length(row, &offset_within_row, + &length); + } else { + rows_const->metadata().nth_varbinary_offset_and_length(row, varbinary_col_id, + &offset_within_row, &length); + } + + row_offset += offset_within_row; + + const uint8_t* src; + uint8_t* dst; + if (is_encoding) { + src = col_const->data(2) + col_offset; + dst = rows_mutable_maybe_null->mutable_data(2) + row_offset; + } else { + src = rows_const->data(2) + row_offset; + dst = col_mutable_maybe_null->mutable_data(2) + col_offset; + } + copy_fn(dst, src, length); + } +} + +} // namespace compute +} // namespace arrow diff --git a/cpp/src/arrow/compute/exec/key_encode_avx2.cc b/cpp/src/arrow/compute/exec/key_encode_avx2.cc new file mode 100644 index 00000000000..d875412cf88 --- /dev/null +++ b/cpp/src/arrow/compute/exec/key_encode_avx2.cc @@ -0,0 +1,545 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#include + +#include "arrow/compute/exec/key_encode.h" + +namespace arrow { +namespace compute { + +#if defined(ARROW_HAVE_AVX2) + +inline __m256i set_first_n_bytes_avx2(int n) { + constexpr uint64_t kByteSequence0To7 = 0x0706050403020100ULL; + constexpr uint64_t kByteSequence8To15 = 0x0f0e0d0c0b0a0908ULL; + constexpr uint64_t kByteSequence16To23 = 0x1716151413121110ULL; + constexpr uint64_t kByteSequence24To31 = 0x1f1e1d1c1b1a1918ULL; + + return _mm256_cmpgt_epi8(_mm256_set1_epi8(n), + _mm256_setr_epi64x(kByteSequence0To7, kByteSequence8To15, + kByteSequence16To23, kByteSequence24To31)); +} + +inline __m256i inclusive_prefix_sum_32bit_avx2(__m256i x) { + x = _mm256_add_epi32( + x, _mm256_permutevar8x32_epi32( + _mm256_andnot_si256(_mm256_setr_epi32(0, 0, 0, 0, 0, 0, 0, 0xffffffff), x), + _mm256_setr_epi32(7, 0, 1, 2, 3, 4, 5, 6))); + x = _mm256_add_epi32( + x, _mm256_permute4x64_epi64( + _mm256_andnot_si256( + _mm256_setr_epi32(0, 0, 0, 0, 0, 0, 0xffffffff, 0xffffffff), x), + 0x93)); // 0b10010011 + x = _mm256_add_epi32( + x, _mm256_permute4x64_epi64( + _mm256_andnot_si256( + _mm256_setr_epi32(0, 0, 0, 0, 0, 0, 0xffffffff, 0xffffffff), x), + 0x4f)); // 0b01001111 + return x; +} + +void KeyEncoder::EncoderBinary::EncodeHelper_avx2(bool is_row_fixed_length, + uint32_t offset_within_row, + KeyRowArray* rows, + const KeyColumnArray& col) { + if (is_row_fixed_length) { + EncodeImp_avx2(offset_within_row, rows, col); + } else { + EncodeImp_avx2(offset_within_row, rows, col); + } +} + +template +void KeyEncoder::EncoderBinary::EncodeImp_avx2(uint32_t offset_within_row, + KeyRowArray* rows, + const KeyColumnArray& col) { + EncodeDecodeHelper( + 0, static_cast(col.length()), offset_within_row, rows, rows, &col, + nullptr, [](uint8_t* dst, const uint8_t* src, int64_t length) { + __m256i* dst256 = reinterpret_cast<__m256i*>(dst); + const __m256i* src256 = reinterpret_cast(src); + uint32_t istripe; + for (istripe = 0; istripe < length / 32; ++istripe) { + _mm256_storeu_si256(dst256 + istripe, _mm256_loadu_si256(src256 + istripe)); + } + if ((length % 32) > 0) { + __m256i mask = set_first_n_bytes_avx2(length % 32); + _mm256_storeu_si256( + dst256 + istripe, + _mm256_blendv_epi8(_mm256_loadu_si256(dst256 + istripe), + _mm256_loadu_si256(src256 + istripe), mask)); + } + }); +} + +void KeyEncoder::EncoderBinary::DecodeHelper_avx2(bool is_row_fixed_length, + uint32_t start_row, uint32_t num_rows, + uint32_t offset_within_row, + const KeyRowArray& rows, + KeyColumnArray* col) { + if (is_row_fixed_length) { + DecodeImp_avx2(start_row, num_rows, offset_within_row, rows, col); + } else { + DecodeImp_avx2(start_row, num_rows, offset_within_row, rows, col); + } +} + +template +void KeyEncoder::EncoderBinary::DecodeImp_avx2(uint32_t start_row, uint32_t num_rows, + uint32_t offset_within_row, + const KeyRowArray& rows, + KeyColumnArray* col) { + EncodeDecodeHelper( + start_row, num_rows, offset_within_row, &rows, nullptr, col, col, + [](uint8_t* dst, const uint8_t* src, int64_t length) { + for (uint32_t istripe = 0; istripe < (length + 31) / 32; ++istripe) { + __m256i* dst256 = reinterpret_cast<__m256i*>(dst); + const __m256i* src256 = reinterpret_cast(src); + _mm256_storeu_si256(dst256 + istripe, _mm256_loadu_si256(src256 + istripe)); + } + }); +} + +uint32_t KeyEncoder::EncoderBinaryPair::EncodeHelper_avx2( + bool is_row_fixed_length, uint32_t col_width, uint32_t offset_within_row, + KeyRowArray* rows, const KeyColumnArray& col1, const KeyColumnArray& col2) { + using EncodeImp_avx2_t = + uint32_t (*)(uint32_t, KeyRowArray*, const KeyColumnArray&, const KeyColumnArray&); + static const EncodeImp_avx2_t EncodeImp_avx2_fn[] = { + EncodeImp_avx2, EncodeImp_avx2, EncodeImp_avx2, + EncodeImp_avx2, EncodeImp_avx2, EncodeImp_avx2, + EncodeImp_avx2, EncodeImp_avx2, + }; + int log_col_width = col_width == 8 ? 3 : col_width == 4 ? 2 : col_width == 2 ? 1 : 0; + int dispatch_const = (is_row_fixed_length ? 4 : 0) + log_col_width; + return EncodeImp_avx2_fn[dispatch_const](offset_within_row, rows, col1, col2); +} + +template +uint32_t KeyEncoder::EncoderBinaryPair::EncodeImp_avx2(uint32_t offset_within_row, + KeyRowArray* rows, + const KeyColumnArray& col1, + const KeyColumnArray& col2) { + uint32_t num_rows = static_cast(col1.length()); + ARROW_DCHECK(col_width == 1 || col_width == 2 || col_width == 4 || col_width == 8); + + const uint8_t* col_vals_A = col1.data(1); + const uint8_t* col_vals_B = col2.data(1); + uint8_t* row_vals = is_row_fixed_length ? rows->mutable_data(1) : rows->mutable_data(2); + + constexpr int unroll = 32 / col_width; + + uint32_t num_processed = num_rows / unroll * unroll; + + for (uint32_t i = 0; i < num_rows / unroll; ++i) { + __m256i col_A = _mm256_loadu_si256(reinterpret_cast(col_vals_A) + i); + __m256i col_B = _mm256_loadu_si256(reinterpret_cast(col_vals_B) + i); + __m256i r0, r1; + if (col_width == 1) { + // results in 16-bit outputs in the order: 0..7, 16..23 + r0 = _mm256_unpacklo_epi8(col_A, col_B); + // results in 16-bit outputs in the order: 8..15, 24..31 + r1 = _mm256_unpackhi_epi8(col_A, col_B); + } else if (col_width == 2) { + // results in 32-bit outputs in the order: 0..3, 8..11 + r0 = _mm256_unpacklo_epi16(col_A, col_B); + // results in 32-bit outputs in the order: 4..7, 12..15 + r1 = _mm256_unpackhi_epi16(col_A, col_B); + } else if (col_width == 4) { + // results in 64-bit outputs in the order: 0..1, 4..5 + r0 = _mm256_unpacklo_epi32(col_A, col_B); + // results in 64-bit outputs in the order: 2..3, 6..7 + r1 = _mm256_unpackhi_epi32(col_A, col_B); + } else if (col_width == 8) { + // results in 128-bit outputs in the order: 0, 2 + r0 = _mm256_unpacklo_epi64(col_A, col_B); + // results in 128-bit outputs in the order: 1, 3 + r1 = _mm256_unpackhi_epi64(col_A, col_B); + } + col_A = _mm256_permute2x128_si256(r0, r1, 0x20); + col_B = _mm256_permute2x128_si256(r0, r1, 0x31); + if (col_width == 8) { + __m128i *dst0, *dst1, *dst2, *dst3; + if (is_row_fixed_length) { + uint32_t fixed_length = rows->metadata().fixed_length; + uint8_t* dst = row_vals + offset_within_row + fixed_length * i * unroll; + dst0 = reinterpret_cast<__m128i*>(dst); + dst1 = reinterpret_cast<__m128i*>(dst + fixed_length); + dst2 = reinterpret_cast<__m128i*>(dst + fixed_length * 2); + dst3 = reinterpret_cast<__m128i*>(dst + fixed_length * 3); + } else { + const uint32_t* row_offsets = rows->offsets() + i * unroll; + uint8_t* dst = row_vals + offset_within_row; + dst0 = reinterpret_cast<__m128i*>(dst + row_offsets[0]); + dst1 = reinterpret_cast<__m128i*>(dst + row_offsets[1]); + dst2 = reinterpret_cast<__m128i*>(dst + row_offsets[2]); + dst3 = reinterpret_cast<__m128i*>(dst + row_offsets[3]); + } + _mm_storeu_si128(dst0, _mm256_castsi256_si128(r0)); + _mm_storeu_si128(dst1, _mm256_castsi256_si128(r1)); + _mm_storeu_si128(dst2, _mm256_extracti128_si256(r0, 1)); + _mm_storeu_si128(dst3, _mm256_extracti128_si256(r1, 1)); + + } else { + uint8_t buffer[64]; + _mm256_storeu_si256(reinterpret_cast<__m256i*>(buffer), col_A); + _mm256_storeu_si256(reinterpret_cast<__m256i*>(buffer) + 1, col_B); + + if (is_row_fixed_length) { + uint32_t fixed_length = rows->metadata().fixed_length; + uint8_t* dst = row_vals + offset_within_row + fixed_length * i * unroll; + for (int j = 0; j < unroll; ++j) { + if (col_width == 1) { + *reinterpret_cast(dst + fixed_length * j) = + reinterpret_cast(buffer)[j]; + } else if (col_width == 2) { + *reinterpret_cast(dst + fixed_length * j) = + reinterpret_cast(buffer)[j]; + } else if (col_width == 4) { + *reinterpret_cast(dst + fixed_length * j) = + reinterpret_cast(buffer)[j]; + } + } + } else { + const uint32_t* row_offsets = rows->offsets() + i * unroll; + uint8_t* dst = row_vals + offset_within_row; + for (int j = 0; j < unroll; ++j) { + if (col_width == 1) { + *reinterpret_cast(dst + row_offsets[j]) = + reinterpret_cast(buffer)[j]; + } else if (col_width == 2) { + *reinterpret_cast(dst + row_offsets[j]) = + reinterpret_cast(buffer)[j]; + } else if (col_width == 4) { + *reinterpret_cast(dst + row_offsets[j]) = + reinterpret_cast(buffer)[j]; + } + } + } + } + } + + return num_processed; +} + +uint32_t KeyEncoder::EncoderBinaryPair::DecodeHelper_avx2( + bool is_row_fixed_length, uint32_t col_width, uint32_t start_row, uint32_t num_rows, + uint32_t offset_within_row, const KeyRowArray& rows, KeyColumnArray* col1, + KeyColumnArray* col2) { + using DecodeImp_avx2_t = + uint32_t (*)(uint32_t start_row, uint32_t num_rows, uint32_t offset_within_row, + const KeyRowArray& rows, KeyColumnArray* col1, KeyColumnArray* col2); + static const DecodeImp_avx2_t DecodeImp_avx2_fn[] = { + DecodeImp_avx2, DecodeImp_avx2, DecodeImp_avx2, + DecodeImp_avx2, DecodeImp_avx2, DecodeImp_avx2, + DecodeImp_avx2, DecodeImp_avx2}; + int log_col_width = col_width == 8 ? 3 : col_width == 4 ? 2 : col_width == 2 ? 1 : 0; + int dispatch_const = log_col_width | (is_row_fixed_length ? 4 : 0); + return DecodeImp_avx2_fn[dispatch_const](start_row, num_rows, offset_within_row, rows, + col1, col2); +} + +template +uint32_t KeyEncoder::EncoderBinaryPair::DecodeImp_avx2( + uint32_t start_row, uint32_t num_rows, uint32_t offset_within_row, + const KeyRowArray& rows, KeyColumnArray* col1, KeyColumnArray* col2) { + ARROW_DCHECK(col_width == 1 || col_width == 2 || col_width == 4 || col_width == 8); + + uint8_t* col_vals_A = col1->mutable_data(1); + uint8_t* col_vals_B = col2->mutable_data(1); + + uint32_t fixed_length = rows.metadata().fixed_length; + const uint32_t* offsets; + const uint8_t* src_base; + if (is_row_fixed_length) { + src_base = rows.data(1) + fixed_length * start_row + offset_within_row; + offsets = nullptr; + } else { + src_base = rows.data(2) + offset_within_row; + offsets = rows.offsets() + start_row; + } + + constexpr int unroll = 32 / col_width; + + uint32_t num_processed = num_rows / unroll * unroll; + + if (col_width == 8) { + for (uint32_t i = 0; i < num_rows / unroll; ++i) { + const __m128i *src0, *src1, *src2, *src3; + if (is_row_fixed_length) { + const uint8_t* src = src_base + (i * unroll) * fixed_length; + src0 = reinterpret_cast(src); + src1 = reinterpret_cast(src + fixed_length); + src2 = reinterpret_cast(src + fixed_length * 2); + src3 = reinterpret_cast(src + fixed_length * 3); + } else { + const uint32_t* row_offsets = offsets + i * unroll; + const uint8_t* src = src_base; + src0 = reinterpret_cast(src + row_offsets[0]); + src1 = reinterpret_cast(src + row_offsets[1]); + src2 = reinterpret_cast(src + row_offsets[2]); + src3 = reinterpret_cast(src + row_offsets[3]); + } + + __m256i r0 = _mm256_inserti128_si256(_mm256_castsi128_si256(_mm_loadu_si128(src0)), + _mm_loadu_si128(src1), 1); + __m256i r1 = _mm256_inserti128_si256(_mm256_castsi128_si256(_mm_loadu_si128(src2)), + _mm_loadu_si128(src3), 1); + + r0 = _mm256_permute4x64_epi64(r0, 0xd8); // 0b11011000 + r1 = _mm256_permute4x64_epi64(r1, 0xd8); + + // First 128-bit lanes from both inputs + __m256i c1 = _mm256_permute2x128_si256(r0, r1, 0x20); + // Second 128-bit lanes from both inputs + __m256i c2 = _mm256_permute2x128_si256(r0, r1, 0x31); + _mm256_storeu_si256(reinterpret_cast<__m256i*>(col_vals_A) + i, c1); + _mm256_storeu_si256(reinterpret_cast<__m256i*>(col_vals_B) + i, c2); + } + } else { + uint8_t buffer[64]; + for (uint32_t i = 0; i < num_rows / unroll; ++i) { + if (is_row_fixed_length) { + const uint8_t* src = src_base + (i * unroll) * fixed_length; + for (int j = 0; j < unroll; ++j) { + if (col_width == 1) { + reinterpret_cast(buffer)[j] = + *reinterpret_cast(src + fixed_length * j); + } else if (col_width == 2) { + reinterpret_cast(buffer)[j] = + *reinterpret_cast(src + fixed_length * j); + } else if (col_width == 4) { + reinterpret_cast(buffer)[j] = + *reinterpret_cast(src + fixed_length * j); + } + } + } else { + const uint32_t* row_offsets = offsets + i * unroll; + const uint8_t* src = src_base; + for (int j = 0; j < unroll; ++j) { + if (col_width == 1) { + reinterpret_cast(buffer)[j] = + *reinterpret_cast(src + row_offsets[j]); + } else if (col_width == 2) { + reinterpret_cast(buffer)[j] = + *reinterpret_cast(src + row_offsets[j]); + } else if (col_width == 4) { + reinterpret_cast(buffer)[j] = + *reinterpret_cast(src + row_offsets[j]); + } + } + } + + __m256i r0 = _mm256_loadu_si256(reinterpret_cast(buffer)); + __m256i r1 = _mm256_loadu_si256(reinterpret_cast(buffer) + 1); + + constexpr uint64_t kByteSequence_0_2_4_6_8_10_12_14 = 0x0e0c0a0806040200ULL; + constexpr uint64_t kByteSequence_1_3_5_7_9_11_13_15 = 0x0f0d0b0907050301ULL; + constexpr uint64_t kByteSequence_0_1_4_5_8_9_12_13 = 0x0d0c090805040100ULL; + constexpr uint64_t kByteSequence_2_3_6_7_10_11_14_15 = 0x0f0e0b0a07060302ULL; + + if (col_width == 1) { + // Collect every second byte next to each other + const __m256i shuffle_const = _mm256_setr_epi64x( + kByteSequence_0_2_4_6_8_10_12_14, kByteSequence_1_3_5_7_9_11_13_15, + kByteSequence_0_2_4_6_8_10_12_14, kByteSequence_1_3_5_7_9_11_13_15); + r0 = _mm256_shuffle_epi8(r0, shuffle_const); + r1 = _mm256_shuffle_epi8(r1, shuffle_const); + // 0b11011000 swapping second and third 64-bit lane + r0 = _mm256_permute4x64_epi64(r0, 0xd8); + r1 = _mm256_permute4x64_epi64(r1, 0xd8); + } else if (col_width == 2) { + // Collect every second 16-bit word next to each other + const __m256i shuffle_const = _mm256_setr_epi64x( + kByteSequence_0_1_4_5_8_9_12_13, kByteSequence_2_3_6_7_10_11_14_15, + kByteSequence_0_1_4_5_8_9_12_13, kByteSequence_2_3_6_7_10_11_14_15); + r0 = _mm256_shuffle_epi8(r0, shuffle_const); + r1 = _mm256_shuffle_epi8(r1, shuffle_const); + // 0b11011000 swapping second and third 64-bit lane + r0 = _mm256_permute4x64_epi64(r0, 0xd8); + r1 = _mm256_permute4x64_epi64(r1, 0xd8); + } else if (col_width == 4) { + // Collect every second 32-bit word next to each other + const __m256i permute_const = _mm256_setr_epi32(0, 2, 4, 6, 1, 3, 5, 7); + r0 = _mm256_permutevar8x32_epi32(r0, permute_const); + r1 = _mm256_permutevar8x32_epi32(r1, permute_const); + } + + // First 128-bit lanes from both inputs + __m256i c1 = _mm256_permute2x128_si256(r0, r1, 0x20); + // Second 128-bit lanes from both inputs + __m256i c2 = _mm256_permute2x128_si256(r0, r1, 0x31); + _mm256_storeu_si256(reinterpret_cast<__m256i*>(col_vals_A) + i, c1); + _mm256_storeu_si256(reinterpret_cast<__m256i*>(col_vals_B) + i, c2); + } + } + + return num_processed; +} + +uint32_t KeyEncoder::EncoderOffsets::EncodeImp_avx2( + KeyRowArray* rows, const std::vector& varbinary_cols, + KeyColumnArray* temp_buffer_32B_per_col) { + ARROW_DCHECK(temp_buffer_32B_per_col->metadata().is_fixed_length && + temp_buffer_32B_per_col->metadata().fixed_length == + static_cast(sizeof(uint32_t)) && + temp_buffer_32B_per_col->length() >= + static_cast(varbinary_cols.size()) * 8); + ARROW_DCHECK(varbinary_cols.size() > 0); + + int row_alignment = rows->metadata().row_alignment; + int string_alignment = rows->metadata().string_alignment; + + uint32_t* row_offsets = rows->mutable_offsets(); + uint8_t* row_values = rows->mutable_data(2); + uint32_t num_rows = static_cast(varbinary_cols[0].length()); + + constexpr int unroll = 8; + uint32_t num_processed = num_rows / unroll * unroll; + uint32_t* temp_varbinary_ends = + reinterpret_cast(temp_buffer_32B_per_col->mutable_data(1)); + + row_offsets[0] = 0; + + __m256i row_offset = _mm256_setzero_si256(); + for (uint32_t i = 0; i < num_rows / unroll; ++i) { + // Zero out lengths for nulls. + // Add lengths of all columns to get row size. + // Store in temp buffer varbinary field ends while summing their lengths. + + __m256i offset_within_row = _mm256_set1_epi32(rows->metadata().fixed_length); + + for (size_t col = 0; col < varbinary_cols.size(); ++col) { + const uint32_t* col_offsets = varbinary_cols[col].offsets(); + __m256i col_length = _mm256_sub_epi32( + _mm256_loadu_si256(reinterpret_cast(col_offsets + 1) + i), + _mm256_loadu_si256(reinterpret_cast(col_offsets + 0) + i)); + + const uint8_t* non_nulls = varbinary_cols[col].data(0); + if (non_nulls && non_nulls[i] != 0xff) { + // Zero out lengths for values that are not null + const __m256i individual_bits = + _mm256_setr_epi32(0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80); + __m256i null_mask = _mm256_cmpeq_epi32( + _mm256_setzero_si256(), + _mm256_and_si256(_mm256_set1_epi32(non_nulls[i]), individual_bits)); + col_length = _mm256_andnot_si256(null_mask, col_length); + } + + __m256i padding = + _mm256_and_si256(_mm256_sub_epi32(_mm256_setzero_si256(), offset_within_row), + _mm256_set1_epi32(string_alignment - 1)); + offset_within_row = _mm256_add_epi32(offset_within_row, padding); + offset_within_row = _mm256_add_epi32(offset_within_row, col_length); + + _mm256_storeu_si256(reinterpret_cast<__m256i*>(temp_varbinary_ends) + col, + offset_within_row); + } + + __m256i padding = + _mm256_and_si256(_mm256_sub_epi32(_mm256_setzero_si256(), offset_within_row), + _mm256_set1_epi32(row_alignment - 1)); + offset_within_row = _mm256_add_epi32(offset_within_row, padding); + + // Inclusive prefix sum of 32-bit elements + __m256i row_offset_delta = inclusive_prefix_sum_32bit_avx2(offset_within_row); + row_offset = _mm256_add_epi32( + _mm256_permutevar8x32_epi32(row_offset, _mm256_set1_epi32(7)), row_offset_delta); + + _mm256_storeu_si256(reinterpret_cast<__m256i*>(row_offsets + 1) + i, row_offset); + + // Output varbinary ends for all fields in each row + for (size_t col = 0; col < varbinary_cols.size(); ++col) { + for (uint32_t row = 0; row < unroll; ++row) { + uint32_t* dst = rows->metadata().varbinary_end_array( + row_values + row_offsets[i * unroll + row]) + + col; + const uint32_t* src = temp_varbinary_ends + (col * unroll + row); + *dst = *src; + } + } + } + + return num_processed; +} + +void KeyEncoder::EncoderVarBinary::EncodeHelper_avx2(uint32_t varbinary_col_id, + KeyRowArray* rows, + const KeyColumnArray& col) { + if (varbinary_col_id == 0) { + EncodeImp_avx2(varbinary_col_id, rows, col); + } else { + EncodeImp_avx2(varbinary_col_id, rows, col); + } +} + +template +void KeyEncoder::EncoderVarBinary::EncodeImp_avx2(uint32_t varbinary_col_id, + KeyRowArray* rows, + const KeyColumnArray& col) { + EncodeDecodeHelper( + 0, static_cast(col.length()), varbinary_col_id, rows, rows, &col, nullptr, + [](uint8_t* dst, const uint8_t* src, int64_t length) { + __m256i* dst256 = reinterpret_cast<__m256i*>(dst); + const __m256i* src256 = reinterpret_cast(src); + uint32_t istripe; + for (istripe = 0; istripe < length / 32; ++istripe) { + _mm256_storeu_si256(dst256 + istripe, _mm256_loadu_si256(src256 + istripe)); + } + if ((length % 32) > 0) { + __m256i mask = set_first_n_bytes_avx2(length % 32); + _mm256_storeu_si256( + dst256 + istripe, + _mm256_blendv_epi8(_mm256_loadu_si256(dst256 + istripe), + _mm256_loadu_si256(src256 + istripe), mask)); + } + }); +} + +void KeyEncoder::EncoderVarBinary::DecodeHelper_avx2(uint32_t start_row, + uint32_t num_rows, + uint32_t varbinary_col_id, + const KeyRowArray& rows, + KeyColumnArray* col) { + if (varbinary_col_id == 0) { + DecodeImp_avx2(start_row, num_rows, varbinary_col_id, rows, col); + } else { + DecodeImp_avx2(start_row, num_rows, varbinary_col_id, rows, col); + } +} + +template +void KeyEncoder::EncoderVarBinary::DecodeImp_avx2(uint32_t start_row, uint32_t num_rows, + uint32_t varbinary_col_id, + const KeyRowArray& rows, + KeyColumnArray* col) { + EncodeDecodeHelper( + start_row, num_rows, varbinary_col_id, &rows, nullptr, col, col, + [](uint8_t* dst, const uint8_t* src, int64_t length) { + for (uint32_t istripe = 0; istripe < (length + 31) / 32; ++istripe) { + __m256i* dst256 = reinterpret_cast<__m256i*>(dst); + const __m256i* src256 = reinterpret_cast(src); + _mm256_storeu_si256(dst256 + istripe, _mm256_loadu_si256(src256 + istripe)); + } + }); +} + +#endif + +} // namespace compute +} // namespace arrow diff --git a/cpp/src/arrow/compute/exec/key_hash.cc b/cpp/src/arrow/compute/exec/key_hash.cc new file mode 100644 index 00000000000..b91ef0eb763 --- /dev/null +++ b/cpp/src/arrow/compute/exec/key_hash.cc @@ -0,0 +1,247 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#include "arrow/compute/exec/key_hash.h" + +#include + +#include + +#include "arrow/compute/exec/util.h" + +#ifdef _MSC_VER +#include +#else +#include +#endif +#include + +#include + +namespace arrow { +namespace compute { + +inline uint32_t Hashing::avalanche_helper(uint32_t acc) { + acc ^= (acc >> 15); + acc *= PRIME32_2; + acc ^= (acc >> 13); + acc *= PRIME32_3; + acc ^= (acc >> 16); + return acc; +} + +void Hashing::avalanche(const arrow::internal::CpuInfo* cpu_info, uint32_t num_keys, + uint32_t* hashes) { + uint32_t processed = 0; +#if defined(ARROW_HAVE_AVX2) + if (cpu_info->IsSupported(arrow::internal::CpuInfo::AVX2)) { + int tail = num_keys % 8; + avalanche_avx2(num_keys - tail, hashes); + processed = num_keys - tail; + } +#endif + for (uint32_t i = processed; i < num_keys; ++i) { + hashes[i] = avalanche_helper(hashes[i]); + } +} + +inline uint32_t Hashing::combine_accumulators(const uint32_t acc1, const uint32_t acc2, + const uint32_t acc3, const uint32_t acc4) { + return ROTL(acc1, 1) + ROTL(acc2, 7) + ROTL(acc3, 12) + ROTL(acc4, 18); +} + +inline void Hashing::helper_8B(uint32_t key_length, uint32_t num_keys, + const uint8_t* keys, uint32_t* hashes) { + ARROW_DCHECK(key_length <= 8); + uint64_t mask = ~0ULL >> (8 * (8 - key_length)); + constexpr uint64_t multiplier = 14029467366897019727ULL; + uint32_t offset = 0; + for (uint32_t ikey = 0; ikey < num_keys; ++ikey) { + uint64_t x = *reinterpret_cast(keys + offset); + x &= mask; + hashes[ikey] = static_cast(BYTESWAP(x * multiplier)); + offset += key_length; + } +} + +inline void Hashing::helper_stripe(uint32_t offset, uint64_t mask_hi, const uint8_t* keys, + uint32_t& acc1, uint32_t& acc2, uint32_t& acc3, + uint32_t& acc4) { + uint64_t v1 = reinterpret_cast(keys + offset)[0]; + // We do not need to mask v1, because we will not process a stripe + // unless at least 9 bytes of it are part of the key. + uint64_t v2 = reinterpret_cast(keys + offset)[1]; + v2 &= mask_hi; + uint32_t x1 = static_cast(v1); + uint32_t x2 = static_cast(v1 >> 32); + uint32_t x3 = static_cast(v2); + uint32_t x4 = static_cast(v2 >> 32); + acc1 += x1 * PRIME32_2; + acc1 = ROTL(acc1, 13) * PRIME32_1; + acc2 += x2 * PRIME32_2; + acc2 = ROTL(acc2, 13) * PRIME32_1; + acc3 += x3 * PRIME32_2; + acc3 = ROTL(acc3, 13) * PRIME32_1; + acc4 += x4 * PRIME32_2; + acc4 = ROTL(acc4, 13) * PRIME32_1; +} + +void Hashing::helper_stripes(const arrow::internal::CpuInfo* cpu_info, uint32_t num_keys, + uint32_t key_length, const uint8_t* keys, uint32_t* hash) { + uint32_t processed = 0; +#if defined(ARROW_HAVE_AVX2) + if (cpu_info->IsSupported(arrow::internal::CpuInfo::AVX2)) { + int tail = num_keys % 2; + helper_stripes_avx2(num_keys - tail, key_length, keys, hash); + processed = num_keys - tail; + } +#endif + + // If length modulo stripe length is less than or equal 8, round down to the nearest 16B + // boundary (8B ending will be processed in a separate function), otherwise round up. + const uint32_t num_stripes = (key_length + 7) / 16; + uint64_t mask_hi = + ~0ULL >> + (8 * ((num_stripes * 16 > key_length) ? num_stripes * 16 - key_length : 0)); + + for (uint32_t i = processed; i < num_keys; ++i) { + uint32_t acc1, acc2, acc3, acc4; + acc1 = static_cast( + (static_cast(PRIME32_1) + static_cast(PRIME32_2)) & + 0xffffffff); + acc2 = PRIME32_2; + acc3 = 0; + acc4 = static_cast(-static_cast(PRIME32_1)); + uint32_t offset = i * key_length; + for (uint32_t stripe = 0; stripe < num_stripes - 1; ++stripe) { + helper_stripe(offset, ~0ULL, keys, acc1, acc2, acc3, acc4); + offset += 16; + } + helper_stripe(offset, mask_hi, keys, acc1, acc2, acc3, acc4); + hash[i] = combine_accumulators(acc1, acc2, acc3, acc4); + } +} + +inline uint32_t Hashing::helper_tail(uint32_t offset, uint64_t mask, const uint8_t* keys, + uint32_t acc) { + uint64_t v = reinterpret_cast(keys + offset)[0]; + v &= mask; + uint32_t x1 = static_cast(v); + uint32_t x2 = static_cast(v >> 32); + acc += x1 * PRIME32_3; + acc = ROTL(acc, 17) * PRIME32_4; + acc += x2 * PRIME32_3; + acc = ROTL(acc, 17) * PRIME32_4; + return acc; +} + +void Hashing::helper_tails(const arrow::internal::CpuInfo* cpu_info, uint32_t num_keys, + uint32_t key_length, const uint8_t* keys, uint32_t* hash) { + uint32_t processed = 0; +#if defined(ARROW_HAVE_AVX2) + if (cpu_info->IsSupported(arrow::internal::CpuInfo::AVX2)) { + int tail = num_keys % 8; + helper_tails_avx2(num_keys - tail, key_length, keys, hash); + processed = num_keys - tail; + } +#endif + uint64_t mask = ~0ULL >> (8 * (((key_length % 8) == 0) ? 0 : 8 - (key_length % 8))); + uint32_t offset = key_length / 16 * 16; + offset += processed * key_length; + for (uint32_t i = processed; i < num_keys; ++i) { + hash[i] = helper_tail(offset, mask, keys, hash[i]); + offset += key_length; + } +} + +void Hashing::hash_fixed(const arrow::internal::CpuInfo* cpu_info, uint32_t num_keys, + uint32_t length_key, const uint8_t* keys, uint32_t* hashes) { + ARROW_DCHECK(length_key > 0); + + if (length_key <= 8) { + helper_8B(length_key, num_keys, keys, hashes); + return; + } + helper_stripes(cpu_info, num_keys, length_key, keys, hashes); + if ((length_key % 16) > 0 && (length_key % 16) <= 8) { + helper_tails(cpu_info, num_keys, length_key, keys, hashes); + } + avalanche(cpu_info, num_keys, hashes); +} + +void Hashing::hash_varlen_helper(uint32_t length, const uint8_t* key, uint32_t* acc) { + for (uint32_t i = 0; i < length / 16; ++i) { + for (int j = 0; j < 4; ++j) { + uint32_t lane = reinterpret_cast(key)[i * 4 + j]; + acc[j] += (lane * PRIME32_2); + acc[j] = ROTL(acc[j], 13); + acc[j] *= PRIME32_1; + } + } + + int tail = length % 16; + if (tail) { + uint64_t last_stripe[2]; + const uint64_t* last_stripe_base = + reinterpret_cast(key + length - (length % 16)); + last_stripe[0] = last_stripe_base[0]; + uint64_t mask = ~0ULL >> (8 * ((length + 7) / 8 * 8 - length)); + if (tail <= 8) { + last_stripe[1] = 0; + last_stripe[0] &= mask; + } else { + last_stripe[1] = last_stripe_base[1]; + last_stripe[1] &= mask; + } + for (int j = 0; j < 4; ++j) { + uint32_t lane = reinterpret_cast(last_stripe)[j]; + acc[j] += (lane * PRIME32_2); + acc[j] = ROTL(acc[j], 13); + acc[j] *= PRIME32_1; + } + } +} + +void Hashing::hash_varlen(const arrow::internal::CpuInfo* cpu_info, uint32_t num_rows, + const uint32_t* offsets, const uint8_t* concatenated_keys, + uint32_t* temp_buffer, // Needs to hold 4 x 32-bit per row + uint32_t* hashes) { +#if defined(ARROW_HAVE_AVX2) + if (cpu_info->IsSupported(arrow::internal::CpuInfo::AVX2)) { + hash_varlen_avx2(num_rows, offsets, concatenated_keys, temp_buffer, hashes); + } else { +#endif + for (uint32_t i = 0; i < num_rows; ++i) { + uint32_t acc[4]; + acc[0] = static_cast( + (static_cast(PRIME32_1) + static_cast(PRIME32_2)) & + 0xffffffff); + acc[1] = PRIME32_2; + acc[2] = 0; + acc[3] = static_cast(-static_cast(PRIME32_1)); + uint32_t length = offsets[i + 1] - offsets[i]; + hash_varlen_helper(length, concatenated_keys + offsets[i], acc); + hashes[i] = combine_accumulators(acc[0], acc[1], acc[2], acc[3]); + } + avalanche(cpu_info, num_rows, hashes); +#if defined(ARROW_HAVE_AVX2) + } +#endif +} + +} // namespace compute +} // namespace arrow diff --git a/cpp/src/arrow/compute/exec/key_hash.h b/cpp/src/arrow/compute/exec/key_hash.h new file mode 100644 index 00000000000..2918f307af3 --- /dev/null +++ b/cpp/src/arrow/compute/exec/key_hash.h @@ -0,0 +1,93 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#pragma once + +#include + +#include + +#include "arrow/compute/exec/util.h" + +namespace arrow { +namespace compute { + +// Implementations are based on xxh3 32-bit algorithm description from: +// https://github.com/Cyan4973/xxHash/blob/dev/doc/xxhash_spec.md +// +class Hashing { + public: + static void hash_fixed(const arrow::internal::CpuInfo* cpu_info, uint32_t num_keys, + uint32_t length_key, const uint8_t* keys, uint32_t* hashes); + + static void hash_varlen(const arrow::internal::CpuInfo* cpu_info, uint32_t num_rows, + const uint32_t* offsets, const uint8_t* concatenated_keys, + uint32_t* temp_buffer, // Needs to hold 4 x 32-bit per row + uint32_t* hashes); + + private: + static const uint32_t PRIME32_1 = 0x9E3779B1; // 0b10011110001101110111100110110001 + static const uint32_t PRIME32_2 = 0x85EBCA77; // 0b10000101111010111100101001110111 + static const uint32_t PRIME32_3 = 0xC2B2AE3D; // 0b11000010101100101010111000111101 + static const uint32_t PRIME32_4 = 0x27D4EB2F; // 0b00100111110101001110101100101111 + static const uint32_t PRIME32_5 = 0x165667B1; // 0b00010110010101100110011110110001 + + // Avalanche + static inline uint32_t avalanche_helper(uint32_t acc); +#if defined(ARROW_HAVE_AVX2) + static void avalanche_avx2(uint32_t num_keys, uint32_t* hashes); +#endif + static void avalanche(const arrow::internal::CpuInfo* cpu_info, uint32_t num_keys, + uint32_t* hashes); + + // Accumulator combine + static inline uint32_t combine_accumulators(const uint32_t acc1, const uint32_t acc2, + const uint32_t acc3, const uint32_t acc4); +#if defined(ARROW_HAVE_AVX2) + static inline uint64_t combine_accumulators_avx2(__m256i acc); +#endif + + // Helpers + static inline void helper_8B(uint32_t key_length, uint32_t num_keys, + const uint8_t* keys, uint32_t* hashes); + static inline void helper_stripe(uint32_t offset, uint64_t mask_hi, const uint8_t* keys, + uint32_t& acc1, uint32_t& acc2, uint32_t& acc3, + uint32_t& acc4); + static inline uint32_t helper_tail(uint32_t offset, uint64_t mask, const uint8_t* keys, + uint32_t acc); +#if defined(ARROW_HAVE_AVX2) + static void helper_stripes_avx2(uint32_t num_keys, uint32_t key_length, + const uint8_t* keys, uint32_t* hash); + static void helper_tails_avx2(uint32_t num_keys, uint32_t key_length, + const uint8_t* keys, uint32_t* hash); +#endif + static void helper_stripes(const arrow::internal::CpuInfo* cpu_info, uint32_t num_keys, + uint32_t key_length, const uint8_t* keys, uint32_t* hash); + static void helper_tails(const arrow::internal::CpuInfo* cpu_info, uint32_t num_keys, + uint32_t key_length, const uint8_t* keys, uint32_t* hash); + + static void hash_varlen_helper(uint32_t length, const uint8_t* key, uint32_t* acc); +#if defined(ARROW_HAVE_AVX2) + static void hash_varlen_avx2(uint32_t num_rows, const uint32_t* offsets, + const uint8_t* concatenated_keys, + uint32_t* temp_buffer, // Needs to hold 4 x 32-bit per row + uint32_t* hashes); +#endif +}; + +} // namespace compute +} // namespace arrow diff --git a/cpp/src/arrow/compute/exec/key_hash_avx2.cc b/cpp/src/arrow/compute/exec/key_hash_avx2.cc new file mode 100644 index 00000000000..e081341a8df --- /dev/null +++ b/cpp/src/arrow/compute/exec/key_hash_avx2.cc @@ -0,0 +1,250 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#include + +#include "arrow/compute/exec/key_hash.h" + +namespace arrow { +namespace compute { + +#if defined(ARROW_HAVE_AVX2) + +void Hashing::avalanche_avx2(uint32_t num_keys, uint32_t* hashes) { + constexpr int unroll = 8; + ARROW_DCHECK(num_keys % unroll == 0); + for (uint32_t i = 0; i < num_keys / unroll; ++i) { + __m256i hash = _mm256_loadu_si256(((const __m256i*)hashes) + i); + hash = _mm256_xor_si256(hash, _mm256_srli_epi32(hash, 15)); + hash = _mm256_mullo_epi32(hash, _mm256_set1_epi32(PRIME32_2)); + hash = _mm256_xor_si256(hash, _mm256_srli_epi32(hash, 13)); + hash = _mm256_mullo_epi32(hash, _mm256_set1_epi32(PRIME32_3)); + hash = _mm256_xor_si256(hash, _mm256_srli_epi32(hash, 16)); + _mm256_storeu_si256(((__m256i*)hashes) + i, hash); + } +} + +inline uint64_t Hashing::combine_accumulators_avx2(__m256i acc) { + acc = _mm256_or_si256( + _mm256_sllv_epi32(acc, _mm256_setr_epi32(1, 7, 12, 18, 1, 7, 12, 18)), + _mm256_srlv_epi32(acc, _mm256_setr_epi32(32 - 1, 32 - 7, 32 - 12, 32 - 18, 32 - 1, + 32 - 7, 32 - 12, 32 - 18))); + acc = _mm256_add_epi32(acc, _mm256_shuffle_epi32(acc, 0xee)); // 0b11101110 + acc = _mm256_add_epi32(acc, _mm256_srli_epi64(acc, 32)); + acc = _mm256_permutevar8x32_epi32(acc, _mm256_setr_epi32(0, 4, 0, 0, 0, 0, 0, 0)); + uint64_t result = _mm256_extract_epi64(acc, 0); + return result; +} + +void Hashing::helper_stripes_avx2(uint32_t num_keys, uint32_t key_length, + const uint8_t* keys, uint32_t* hash) { + constexpr int unroll = 2; + ARROW_DCHECK(num_keys % unroll == 0); + + constexpr uint64_t kByteSequence0To7 = 0x0706050403020100ULL; + constexpr uint64_t kByteSequence8To15 = 0x0f0e0d0c0b0a0908ULL; + + const __m256i mask_last_stripe = + (key_length % 16) <= 8 + ? _mm256_set1_epi8(static_cast(0xffU)) + : _mm256_cmpgt_epi8(_mm256_set1_epi8(key_length % 16), + _mm256_setr_epi64x(kByteSequence0To7, kByteSequence8To15, + kByteSequence0To7, kByteSequence8To15)); + + // If length modulo stripe length is less than or equal 8, round down to the nearest 16B + // boundary (8B ending will be processed in a separate function), otherwise round up. + const uint32_t num_stripes = (key_length + 7) / 16; + for (uint32_t i = 0; i < num_keys / unroll; ++i) { + __m256i acc = _mm256_setr_epi32( + static_cast((static_cast(PRIME32_1) + PRIME32_2) & + 0xffffffff), + PRIME32_2, 0, static_cast(-static_cast(PRIME32_1)), + static_cast((static_cast(PRIME32_1) + PRIME32_2) & + 0xffffffff), + PRIME32_2, 0, static_cast(-static_cast(PRIME32_1))); + const __m128i* key0 = reinterpret_cast(keys + key_length * 2 * i); + const __m128i* key1 = + reinterpret_cast(keys + key_length * 2 * i + key_length); + for (uint32_t stripe = 0; stripe < num_stripes - 1; ++stripe) { + __m256i key_stripe = + _mm256_inserti128_si256(_mm256_castsi128_si256(_mm_loadu_si128(key0 + stripe)), + _mm_loadu_si128(key1 + stripe), 1); + acc = _mm256_add_epi32( + acc, _mm256_mullo_epi32(key_stripe, _mm256_set1_epi32(PRIME32_2))); + acc = _mm256_or_si256(_mm256_slli_epi32(acc, 13), _mm256_srli_epi32(acc, 32 - 13)); + acc = _mm256_mullo_epi32(acc, _mm256_set1_epi32(PRIME32_1)); + } + __m256i key_stripe = _mm256_inserti128_si256( + _mm256_castsi128_si256(_mm_loadu_si128(key0 + num_stripes - 1)), + _mm_loadu_si128(key1 + num_stripes - 1), 1); + key_stripe = _mm256_and_si256(key_stripe, mask_last_stripe); + acc = _mm256_add_epi32(acc, + _mm256_mullo_epi32(key_stripe, _mm256_set1_epi32(PRIME32_2))); + acc = _mm256_or_si256(_mm256_slli_epi32(acc, 13), _mm256_srli_epi32(acc, 32 - 13)); + acc = _mm256_mullo_epi32(acc, _mm256_set1_epi32(PRIME32_1)); + uint64_t result = combine_accumulators_avx2(acc); + reinterpret_cast(hash)[i] = result; + } +} + +void Hashing::helper_tails_avx2(uint32_t num_keys, uint32_t key_length, + const uint8_t* keys, uint32_t* hash) { + constexpr int unroll = 8; + ARROW_DCHECK(num_keys % unroll == 0); + + // Process between 1 and 8 last bytes of each key, starting from 16B boundary. + // The caller needs to make sure that there are no more than 8 bytes to process after + // that 16B boundary. + uint32_t first_offset = key_length - (key_length % 16); + __m256i mask = _mm256_set1_epi64x((~0ULL) >> (8 * (8 - (key_length % 16)))); + __m256i offset = + _mm256_setr_epi32(0, key_length, key_length * 2, key_length * 3, key_length * 4, + key_length * 5, key_length * 6, key_length * 7); + offset = _mm256_add_epi32(offset, _mm256_set1_epi32(first_offset)); + __m256i offset_incr = _mm256_set1_epi32(key_length * 8); + + for (uint32_t i = 0; i < num_keys / unroll; ++i) { + __m256i v1 = + _mm256_i32gather_epi64((const long long*)keys, _mm256_castsi256_si128(offset), 1); + __m256i v2 = _mm256_i32gather_epi64((const long long*)keys, + _mm256_extracti128_si256(offset, 1), 1); + v1 = _mm256_and_si256(v1, mask); + v2 = _mm256_and_si256(v2, mask); + v1 = _mm256_permutevar8x32_epi32(v1, _mm256_setr_epi32(0, 2, 4, 6, 1, 3, 5, 7)); + v2 = _mm256_permutevar8x32_epi32(v2, _mm256_setr_epi32(0, 2, 4, 6, 1, 3, 5, 7)); + __m256i x1 = _mm256_permute2x128_si256(v1, v2, 0x20); + __m256i x2 = _mm256_permute2x128_si256(v1, v2, 0x31); + __m256i acc = _mm256_loadu_si256(((const __m256i*)hash) + i); + + acc = _mm256_add_epi32(acc, _mm256_mullo_epi32(x1, _mm256_set1_epi32(PRIME32_3))); + acc = _mm256_or_si256(_mm256_slli_epi32(acc, 17), _mm256_srli_epi32(acc, 32 - 17)); + acc = _mm256_mullo_epi32(acc, _mm256_set1_epi32(PRIME32_4)); + + acc = _mm256_add_epi32(acc, _mm256_mullo_epi32(x2, _mm256_set1_epi32(PRIME32_3))); + acc = _mm256_or_si256(_mm256_slli_epi32(acc, 17), _mm256_srli_epi32(acc, 32 - 17)); + acc = _mm256_mullo_epi32(acc, _mm256_set1_epi32(PRIME32_4)); + + _mm256_storeu_si256(((__m256i*)hash) + i, acc); + + offset = _mm256_add_epi32(offset, offset_incr); + } +} + +void Hashing::hash_varlen_avx2(uint32_t num_rows, const uint32_t* offsets, + const uint8_t* concatenated_keys, + uint32_t* temp_buffer, // Needs to hold 4 x 32-bit per row + uint32_t* hashes) { + constexpr uint64_t kByteSequence0To7 = 0x0706050403020100ULL; + constexpr uint64_t kByteSequence8To15 = 0x0f0e0d0c0b0a0908ULL; + + const __m128i sequence = _mm_set_epi64x(kByteSequence8To15, kByteSequence0To7); + const __m128i acc_init = _mm_setr_epi32( + static_cast((static_cast(PRIME32_1) + PRIME32_2) & 0xffffffff), + PRIME32_2, 0, static_cast(-static_cast(PRIME32_1))); + + // Variable length keys are always processed as a sequence of 16B stripes, + // with the last stripe, if extending past the end of the key, having extra bytes set to + // 0 on the fly. + for (uint32_t ikey = 0; ikey < num_rows; ++ikey) { + uint32_t begin = offsets[ikey]; + uint32_t end = offsets[ikey + 1]; + uint32_t length = end - begin; + const uint8_t* base = concatenated_keys + begin; + + __m128i acc = acc_init; + + uint32_t i; + for (i = 0; i < (length - 1) / 16; ++i) { + __m128i key_stripe = _mm_loadu_si128(reinterpret_cast(base) + i); + acc = _mm_add_epi32(acc, _mm_mullo_epi32(key_stripe, _mm_set1_epi32(PRIME32_2))); + acc = _mm_or_si128(_mm_slli_epi32(acc, 13), _mm_srli_epi32(acc, 32 - 13)); + acc = _mm_mullo_epi32(acc, _mm_set1_epi32(PRIME32_1)); + } + __m128i key_stripe = _mm_loadu_si128(reinterpret_cast(base) + i); + __m128i mask = _mm_cmpgt_epi8(_mm_set1_epi8(((length - 1) % 16) + 1), sequence); + key_stripe = _mm_and_si128(key_stripe, mask); + acc = _mm_add_epi32(acc, _mm_mullo_epi32(key_stripe, _mm_set1_epi32(PRIME32_2))); + acc = _mm_or_si128(_mm_slli_epi32(acc, 13), _mm_srli_epi32(acc, 32 - 13)); + acc = _mm_mullo_epi32(acc, _mm_set1_epi32(PRIME32_1)); + + _mm_storeu_si128(reinterpret_cast<__m128i*>(temp_buffer) + ikey, acc); + } + + // Combine accumulators and perform avalanche + constexpr int unroll = 8; + for (uint32_t i = 0; i < num_rows / unroll; ++i) { + __m256i accA = + _mm256_loadu_si256(reinterpret_cast(temp_buffer) + 4 * i + 0); + __m256i accB = + _mm256_loadu_si256(reinterpret_cast(temp_buffer) + 4 * i + 1); + __m256i accC = + _mm256_loadu_si256(reinterpret_cast(temp_buffer) + 4 * i + 2); + __m256i accD = + _mm256_loadu_si256(reinterpret_cast(temp_buffer) + 4 * i + 3); + // Transpose 2x 4x4 32-bit matrices + __m256i r0 = _mm256_unpacklo_epi32(accA, accB); + __m256i r1 = _mm256_unpackhi_epi32(accA, accB); + __m256i r2 = _mm256_unpacklo_epi32(accC, accD); + __m256i r3 = _mm256_unpackhi_epi32(accC, accD); + accA = _mm256_unpacklo_epi64(r0, r2); + accB = _mm256_unpackhi_epi64(r0, r2); + accC = _mm256_unpacklo_epi64(r1, r3); + accD = _mm256_unpackhi_epi64(r1, r3); + // _rotl(accA, 1) + // _rotl(accB, 7) + // _rotl(accC, 12) + // _rotl(accD, 18) + accA = _mm256_or_si256(_mm256_slli_epi32(accA, 1), _mm256_srli_epi32(accA, 32 - 1)); + accB = _mm256_or_si256(_mm256_slli_epi32(accB, 7), _mm256_srli_epi32(accB, 32 - 7)); + accC = _mm256_or_si256(_mm256_slli_epi32(accC, 12), _mm256_srli_epi32(accC, 32 - 12)); + accD = _mm256_or_si256(_mm256_slli_epi32(accD, 18), _mm256_srli_epi32(accD, 32 - 18)); + accA = _mm256_add_epi32(_mm256_add_epi32(accA, accB), _mm256_add_epi32(accC, accD)); + // avalanche + __m256i hash = accA; + hash = _mm256_xor_si256(hash, _mm256_srli_epi32(hash, 15)); + hash = _mm256_mullo_epi32(hash, _mm256_set1_epi32(PRIME32_2)); + hash = _mm256_xor_si256(hash, _mm256_srli_epi32(hash, 13)); + hash = _mm256_mullo_epi32(hash, _mm256_set1_epi32(PRIME32_3)); + hash = _mm256_xor_si256(hash, _mm256_srli_epi32(hash, 16)); + // Store. + // At this point, because of way 2x 4x4 transposition was done, output hashes are in + // order: 0, 2, 4, 6, 1, 3, 5, 7. Bring back the original order. + _mm256_storeu_si256( + reinterpret_cast<__m256i*>(hashes) + i, + _mm256_permutevar8x32_epi32(hash, _mm256_setr_epi32(0, 4, 1, 5, 2, 6, 3, 7))); + } + // Process the tail of up to 7 hashes + for (uint32_t i = num_rows - num_rows % unroll; i < num_rows; ++i) { + uint32_t* temp_buffer_base = temp_buffer + i * 4; + uint32_t acc = ROTL(temp_buffer_base[0], 1) + ROTL(temp_buffer_base[1], 7) + + ROTL(temp_buffer_base[2], 12) + ROTL(temp_buffer_base[3], 18); + + // avalanche + acc ^= (acc >> 15); + acc *= PRIME32_2; + acc ^= (acc >> 13); + acc *= PRIME32_3; + acc ^= (acc >> 16); + + hashes[i] = acc; + } +} + +#endif + +} // namespace compute +} // namespace arrow diff --git a/cpp/src/arrow/compute/exec/key_map.cc b/cpp/src/arrow/compute/exec/key_map.cc new file mode 100644 index 00000000000..fbd68fbb95d --- /dev/null +++ b/cpp/src/arrow/compute/exec/key_map.cc @@ -0,0 +1,490 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#include "arrow/compute/exec/key_map.h" + +#include +#include + +#include +#include + +#include "arrow/util/bit_util.h" +#include "arrow/util/bitmap_ops.h" + +namespace arrow { + +using BitUtil::CountLeadingZeros; + +namespace compute { + +constexpr uint64_t kHighBitOfEachByte = 0x8080808080808080ULL; + +// +template +inline void SwissTable::search_block(uint64_t block, int stamp, int start_slot, + int* out_slot, int* out_match_found) { + // Filled slot bytes have the highest bit set to 0 and empty slots are equal to 0x80. + // Replicate 7-bit stamp to all non-empty slots: + uint64_t block_high_bits = block & kHighBitOfEachByte; + uint64_t stamp_pattern = stamp * ((block_high_bits ^ kHighBitOfEachByte) >> 7); + // If we xor this pattern with block bytes we get: + // a) 0x00, for filled slots matching the stamp, + // b) 0x00 < x < 0x80, for filled slots not matching the stamp, + // c) 0x80, for empty slots. + // If we then add 0x7f to every byte, negate the result and leave only the highest bits + // in each byte, we get 0x00 for non-match slot and 0x80 for match slot. + uint64_t matches = ~((block ^ stamp_pattern) + ~kHighBitOfEachByte); + if (use_start_slot) { + matches &= kHighBitOfEachByte >> (8 * start_slot); + } else { + matches &= kHighBitOfEachByte; + } + // We get 0 if there are no matches + *out_match_found = (matches == 0 ? 0 : 1); + // Now if we or with the highest bits of the block and scan zero bits in reverse, + // we get 8x slot index that we were looking for. + *out_slot = static_cast(CountLeadingZeros(matches | block_high_bits) >> 3); +} + +inline uint64_t SwissTable::extract_group_id(const uint8_t* block_ptr, int slot, + uint64_t group_id_mask) { + // TODO: Explain why slot can be from 0 to 8 (inclusive) as input and in case of 8 we + // just need to output any valid group id, so we take the one from slot 0 in the block. + int num_groupid_bits = num_groupid_bits_from_log_blocks(log_blocks_); + int bit_offset = (slot & 7) * num_groupid_bits; + const uint64_t* group_id_bytes = + reinterpret_cast(block_ptr) + 1 + (bit_offset >> 6); + uint64_t group_id = (*group_id_bytes >> (bit_offset & 63)) & group_id_mask; + return group_id; +} + +inline uint64_t SwissTable::next_slot_to_visit(uint64_t block_index, int slot, + int match_found) { + return block_index * 8 + slot + match_found; +} + +template +void SwissTable::lookup_1(const uint16_t* selection, const int num_keys, + const uint32_t* hashes, uint8_t* out_match_bitvector, + uint32_t* out_groupids, uint32_t* out_slot_ids) { + memset(out_match_bitvector, 0, (num_keys + 7) / 8); + + uint32_t stamp_mask = (1 << bits_stamp_) - 1; + int num_groupid_bits = num_groupid_bits_from_log_blocks(log_blocks_); + uint32_t groupid_mask = (1 << num_groupid_bits) - 1; + + for (int i = 0; i < num_keys; ++i) { + int id; + if (use_selection) { + id = selection[i]; + } else { + id = i; + } + + // Calculate block index and hash stamp for a byte in a block + // + uint32_t hash = hashes[id]; + uint32_t iblock = hash >> (bits_hash_ - bits_stamp_ - log_blocks_); + uint32_t stamp = iblock & stamp_mask; + iblock >>= bits_stamp_; + + const uint8_t* blockbase = reinterpret_cast(blocks_) + + static_cast(iblock) * (num_groupid_bits + 8); + uint64_t block = *reinterpret_cast(blockbase); + + int match_found; + int islot_in_block; + search_block(block, stamp, 0, &islot_in_block, &match_found); + uint64_t groupid = extract_group_id(blockbase, islot_in_block, groupid_mask); + ARROW_DCHECK(groupid < num_inserted_ || num_inserted_ == 0); + uint64_t islot = next_slot_to_visit(iblock, islot_in_block, match_found); + + out_match_bitvector[id / 8] |= match_found << (id & 7); + + out_groupids[id] = static_cast(groupid); + out_slot_ids[id] = static_cast(islot); + } +} + +// Run a single round of slot search - comparison / insert - filter unprocessed. +// Update selection vector to reflect which items have been processed. +// Ids in selection vector do not have to be sorted. +// +Status SwissTable::lookup_2(const uint32_t* hashes, uint32_t* inout_num_selected, + uint16_t* inout_selection, bool* out_need_resize, + uint32_t* out_group_ids, uint32_t* inout_next_slot_ids) { + // How many groups we can keep in hash table without resizing. + // When we reach this limit, we need to break processing of any further rows. + // Resize small hash tables when 50% full (up to 8KB). + // Resize large hash tables when 75% full. + constexpr int log_blocks_small_ = 9; + uint32_t max_groupid = (log_blocks_ <= log_blocks_small_) ? (8 << log_blocks_) / 2 + : 3 * (8 << log_blocks_) / 4; + ARROW_DCHECK(num_inserted_ <= max_groupid); + + // Temporary arrays are of limited size. + // The input needs to be split into smaller portions if it exceeds that limit. + // + ARROW_DCHECK(*inout_num_selected <= static_cast(1 << log_minibatch_)); + + // We will split input row ids into three categories: + // - needing to visit next block [0] + // - needing comparison [1] + // - inserted [2] + // + auto ids_inserted_buf = + util::TempVectorHolder(temp_stack_, *inout_num_selected); + auto ids_for_comparison_buf = + util::TempVectorHolder(temp_stack_, *inout_num_selected); + constexpr int category_nomatch = 0; + constexpr int category_cmp = 1; + constexpr int category_inserted = 2; + int num_ids[3]; + num_ids[0] = num_ids[1] = num_ids[2] = 0; + uint16_t* ids[3]{inout_selection, ids_for_comparison_buf.mutable_data(), + ids_inserted_buf.mutable_data()}; + auto push_id = [&num_ids, &ids](int category, int id) { + ids[category][num_ids[category]++] = static_cast(id); + }; + + uint64_t slot_id_mask = (1 << (log_blocks_ + 3)) - 1; + uint64_t groupid_mask = slot_id_mask; + uint64_t num_groupid_bits = num_groupid_bits_from_log_blocks(log_blocks_); + constexpr uint64_t stamp_mask = 0x7f; + uint64_t num_block_bytes = (8 + num_groupid_bits); + + uint32_t num_processed; + for (num_processed = 0; + // Second condition in for loop: + // We need to break processing and have the caller of this function + // resize hash table if we reach max_groupid groups. + num_processed < *inout_num_selected && + num_inserted_ + num_ids[category_inserted] < max_groupid; + ++num_processed) { + // row id in original batch + int id = inout_selection[num_processed]; + + uint64_t slot_id = (inout_next_slot_ids[id] & slot_id_mask); + uint64_t block_id = slot_id >> 3; + uint32_t hash = hashes[id]; + uint8_t* blockbase = blocks_ + num_block_bytes * block_id; + uint64_t block = *reinterpret_cast(blockbase); + uint64_t stamp = (hash >> (bits_hash_ - log_blocks_ - bits_stamp_)) & stamp_mask; + int start_slot = (slot_id & 7); + + bool isempty = (blockbase[7 - start_slot] == 0x80); + if (isempty) { + blockbase[7 - start_slot] = static_cast(stamp); + int groupid_bit_offset = static_cast(start_slot * num_groupid_bits); + uint32_t group_id = num_inserted_ + num_ids[category_inserted]; + reinterpret_cast(blockbase + 8)[groupid_bit_offset >> 6] |= + (static_cast(group_id) << (groupid_bit_offset & 63)); + hashes_[slot_id] = hash; + out_group_ids[id] = group_id; + push_id(category_inserted, id); + } else { + int new_match_found; + int new_slot; + search_block(block, static_cast(stamp), start_slot, &new_slot, + &new_match_found); + auto new_groupid = + static_cast(extract_group_id(blockbase, new_slot, groupid_mask)); + ARROW_DCHECK(new_groupid < num_inserted_ + num_ids[category_inserted]); + new_slot = + static_cast(next_slot_to_visit(block_id, new_slot, new_match_found)); + inout_next_slot_ids[id] = new_slot; + out_group_ids[id] = new_groupid; + push_id(new_match_found, id); + } + } + + // Copy keys for newly inserted rows + RETURN_NOT_OK(append_impl_(num_ids[category_inserted], ids[category_inserted])); + num_inserted_ += num_ids[category_inserted]; + + // Evaluate comparisons and push ids of rows that failed + // Add 3 copies of the first id, so that SIMD processing 4 elements at a time can work. + // + { + uint32_t num_not_equal; + equal_impl_(num_ids[category_cmp], ids[category_cmp], out_group_ids, &num_not_equal, + ids[category_nomatch] + num_ids[category_nomatch]); + num_ids[category_nomatch] += num_not_equal; + } + + // Append any unprocessed entries + if (num_processed < *inout_num_selected) { + memmove(ids[category_nomatch] + num_ids[category_nomatch], + inout_selection + num_processed, + sizeof(uint16_t) * (*inout_num_selected - num_processed)); + num_ids[category_nomatch] += (*inout_num_selected - num_processed); + } + + *out_need_resize = num_processed < *inout_num_selected; + *inout_num_selected = num_ids[category_nomatch]; + return Status::OK(); +} + +Status SwissTable::map(const int num_keys, const uint32_t* hashes, + uint32_t* out_groupids) { + // Temporary buffers have limited size. + // Caller is responsible for splitting larger input arrays into smaller chunks. + ARROW_DCHECK(num_keys <= (1 << log_minibatch_)); + + // Allocate temporary buffers + auto match_bitvector_buf = util::TempVectorHolder(temp_stack_, num_keys); + uint8_t* match_bitvector = match_bitvector_buf.mutable_data(); + auto slot_ids_buf = util::TempVectorHolder(temp_stack_, num_keys); + uint32_t* slot_ids = slot_ids_buf.mutable_data(); + auto ids_buf = util::TempVectorHolder(temp_stack_, num_keys); + uint16_t* ids = ids_buf.mutable_data(); + uint32_t num_ids; + +#if defined(ARROW_HAVE_AVX2) + if (cpu_info_->IsSupported(arrow::internal::CpuInfo::AVX2)) { + if (log_blocks_ <= 4) { + int tail = num_keys % 32; + int delta = num_keys - tail; + lookup_1_avx2_x32(num_keys - tail, hashes, match_bitvector, out_groupids, slot_ids); + lookup_1_avx2_x8(tail, hashes + delta, match_bitvector + delta / 8, + out_groupids + delta, slot_ids + delta); + } else { + lookup_1_avx2_x8(num_keys, hashes, match_bitvector, out_groupids, slot_ids); + } + } else { +#endif + lookup_1(nullptr, num_keys, hashes, match_bitvector, out_groupids, slot_ids); +#if defined(ARROW_HAVE_AVX2) + } +#endif + + int64_t num_matches = + arrow::internal::CountSetBits(match_bitvector, /*offset=*/0, num_keys); + + // after first pass count rows with matches and decide based on their percentage + // whether to call dense or sparse comparison function + // + + // TODO: explain num_inserted_ > 0 condition below + if (num_inserted_ > 0 && num_matches > 0 && num_matches > 3 * num_keys / 4) { + equal_impl_(num_keys, nullptr, out_groupids, &num_ids, ids); + } else { + auto ids_cmp_buf = util::TempVectorHolder(temp_stack_, num_keys); + uint16_t* ids_cmp = ids_cmp_buf.mutable_data(); + int num_ids_result; + util::BitUtil::bits_split_indexes(cpu_info_, num_keys, match_bitvector, + &num_ids_result, ids, ids_cmp); + num_ids = num_ids_result; + uint32_t num_not_equal; + equal_impl_(num_keys - num_ids, ids_cmp, out_groupids, &num_not_equal, ids + num_ids); + num_ids += num_not_equal; + } + + do { + bool out_of_capacity; + RETURN_NOT_OK( + lookup_2(hashes, &num_ids, ids, &out_of_capacity, out_groupids, slot_ids)); + if (out_of_capacity) { + RETURN_NOT_OK(grow_double()); + // Set slot_ids for selected vectors to first slot in new initial block. + for (uint32_t i = 0; i < num_ids; ++i) { + slot_ids[ids[i]] = (hashes[ids[i]] >> (bits_hash_ - log_blocks_)) * 8; + } + } + } while (num_ids > 0); + + return Status::OK(); +} + +Status SwissTable::grow_double() { + // Before and after metadata + int num_group_id_bits_before = num_groupid_bits_from_log_blocks(log_blocks_); + int num_group_id_bits_after = num_groupid_bits_from_log_blocks(log_blocks_ + 1); + uint64_t group_id_mask_before = ~0ULL >> (64 - num_group_id_bits_before); + int log_blocks_before = log_blocks_; + int log_blocks_after = log_blocks_ + 1; + uint64_t block_size_before = (8 + num_group_id_bits_before); + uint64_t block_size_after = (8 + num_group_id_bits_after); + uint64_t block_size_total_before = (block_size_before << log_blocks_before) + padding_; + uint64_t block_size_total_after = (block_size_after << log_blocks_after) + padding_; + uint64_t hashes_size_total_before = + (bits_hash_ / 8 * (1 << (log_blocks_before + 3))) + padding_; + uint64_t hashes_size_total_after = + (bits_hash_ / 8 * (1 << (log_blocks_after + 3))) + padding_; + constexpr uint32_t stamp_mask = (1 << bits_stamp_) - 1; + + // Allocate new buffers + uint8_t* blocks_new; + RETURN_NOT_OK(pool_->Allocate(block_size_total_after, &blocks_new)); + memset(blocks_new, 0, block_size_total_after); + uint8_t* hashes_new_8B; + uint32_t* hashes_new; + RETURN_NOT_OK(pool_->Allocate(hashes_size_total_after, &hashes_new_8B)); + hashes_new = reinterpret_cast(hashes_new_8B); + + // First pass over all old blocks. + // Reinsert entries that were not in the overflow block + // (block other than selected by hash bits corresponding to the entry). + for (int i = 0; i < (1 << log_blocks_); ++i) { + // How many full slots in this block + uint8_t* block_base = blocks_ + i * block_size_before; + uint8_t* double_block_base_new = blocks_new + 2 * i * block_size_after; + uint64_t block = *reinterpret_cast(block_base); + + auto full_slots = + static_cast(CountLeadingZeros(block & kHighBitOfEachByte) >> 3); + int full_slots_new[2]; + full_slots_new[0] = full_slots_new[1] = 0; + *reinterpret_cast(double_block_base_new) = kHighBitOfEachByte; + *reinterpret_cast(double_block_base_new + block_size_after) = + kHighBitOfEachByte; + + for (int j = 0; j < full_slots; ++j) { + uint64_t slot_id = i * 8 + j; + uint32_t hash = hashes_[slot_id]; + uint64_t block_id_new = hash >> (bits_hash_ - log_blocks_after); + bool is_overflow_entry = ((block_id_new >> 1) != static_cast(i)); + if (is_overflow_entry) { + continue; + } + + int ihalf = block_id_new & 1; + uint8_t stamp_new = + hash >> ((bits_hash_ - log_blocks_after - bits_stamp_)) & stamp_mask; + uint64_t group_id_bit_offs = j * num_group_id_bits_before; + uint64_t group_id = (*reinterpret_cast(block_base + 8 + + (group_id_bit_offs >> 3)) >> + (group_id_bit_offs & 7)) & + group_id_mask_before; + + uint64_t slot_id_new = i * 16 + ihalf * 8 + full_slots_new[ihalf]; + hashes_new[slot_id_new] = hash; + uint8_t* block_base_new = double_block_base_new + ihalf * block_size_after; + block_base_new[7 - full_slots_new[ihalf]] = stamp_new; + int group_id_bit_offs_new = full_slots_new[ihalf] * num_group_id_bits_after; + *reinterpret_cast(block_base_new + 8 + (group_id_bit_offs_new >> 3)) |= + (group_id << (group_id_bit_offs_new & 7)); + full_slots_new[ihalf]++; + } + } + + // Second pass over all old blocks. + // Reinsert entries that were in an overflow block. + for (int i = 0; i < (1 << log_blocks_); ++i) { + // How many full slots in this block + uint8_t* block_base = blocks_ + i * block_size_before; + uint64_t block = *reinterpret_cast(block_base); + int full_slots = static_cast(CountLeadingZeros(block & kHighBitOfEachByte) >> 3); + + for (int j = 0; j < full_slots; ++j) { + uint64_t slot_id = i * 8 + j; + uint32_t hash = hashes_[slot_id]; + uint64_t block_id_new = hash >> (bits_hash_ - log_blocks_after); + bool is_overflow_entry = ((block_id_new >> 1) != static_cast(i)); + if (!is_overflow_entry) { + continue; + } + + uint64_t group_id_bit_offs = j * num_group_id_bits_before; + uint64_t group_id = (*reinterpret_cast(block_base + 8 + + (group_id_bit_offs >> 3)) >> + (group_id_bit_offs & 7)) & + group_id_mask_before; + uint8_t stamp_new = + hash >> ((bits_hash_ - log_blocks_after - bits_stamp_)) & stamp_mask; + + uint8_t* block_base_new = blocks_new + block_id_new * block_size_after; + uint64_t block_new = *reinterpret_cast(block_base_new); + int full_slots_new = + static_cast(CountLeadingZeros(block_new & kHighBitOfEachByte) >> 3); + while (full_slots_new == 8) { + block_id_new = (block_id_new + 1) & ((1 << log_blocks_after) - 1); + block_base_new = blocks_new + block_id_new * block_size_after; + block_new = *reinterpret_cast(block_base_new); + full_slots_new = + static_cast(CountLeadingZeros(block_new & kHighBitOfEachByte) >> 3); + } + + hashes_new[block_id_new * 8 + full_slots_new] = hash; + block_base_new[7 - full_slots_new] = stamp_new; + int group_id_bit_offs_new = full_slots_new * num_group_id_bits_after; + *reinterpret_cast(block_base_new + 8 + (group_id_bit_offs_new >> 3)) |= + (group_id << (group_id_bit_offs_new & 7)); + } + } + + pool_->Free(blocks_, block_size_total_before); + pool_->Free(reinterpret_cast(hashes_), hashes_size_total_before); + log_blocks_ = log_blocks_after; + blocks_ = blocks_new; + hashes_ = hashes_new; + + return Status::OK(); +} + +Status SwissTable::init(const arrow::internal::CpuInfo* cpu_info, MemoryPool* pool, + util::TempVectorStack* temp_stack, int log_minibatch, + EqualImpl equal_impl, AppendImpl append_impl) { + cpu_info_ = cpu_info; + pool_ = pool; + temp_stack_ = temp_stack; + log_minibatch_ = log_minibatch; + equal_impl_ = equal_impl; + append_impl_ = append_impl; + + log_blocks_ = 0; + int num_groupid_bits = num_groupid_bits_from_log_blocks(log_blocks_); + num_inserted_ = 0; + + const uint64_t cbblocks = ((8 + num_groupid_bits) << log_blocks_) + padding_; + RETURN_NOT_OK(pool_->Allocate(cbblocks, &blocks_)); + memset(blocks_, 0, cbblocks); + + for (uint64_t i = 0; i < (static_cast(1) << log_blocks_); ++i) { + *reinterpret_cast(blocks_ + i * (8 + num_groupid_bits)) = + kHighBitOfEachByte; + } + + int log_slots = log_blocks_ + 3; + const uint64_t cbhashes = (sizeof(uint32_t) << log_slots) + padding_; + uint8_t* hashes8; + RETURN_NOT_OK(pool_->Allocate(cbhashes, &hashes8)); + hashes_ = reinterpret_cast(hashes8); + + return Status::OK(); +} + +void SwissTable::cleanup() { + if (blocks_) { + int num_groupid_bits = num_groupid_bits_from_log_blocks(log_blocks_); + const uint64_t cbblocks = ((8 + num_groupid_bits) << log_blocks_) + padding_; + pool_->Free(blocks_, cbblocks); + blocks_ = nullptr; + } + if (hashes_) { + uint64_t num_slots = 1ULL << (log_blocks_ + 3); + const uint64_t cbhashes = sizeof(uint32_t) * num_slots + padding_; + pool_->Free(reinterpret_cast(hashes_), cbhashes); + hashes_ = nullptr; + } + log_blocks_ = 0; + num_inserted_ = 0; +} + +} // namespace compute +} // namespace arrow diff --git a/cpp/src/arrow/compute/exec/key_map.h b/cpp/src/arrow/compute/exec/key_map.h new file mode 100644 index 00000000000..c72da33b392 --- /dev/null +++ b/cpp/src/arrow/compute/exec/key_map.h @@ -0,0 +1,173 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#pragma once + +#include + +#include "arrow/compute/exec/util.h" +#include "arrow/memory_pool.h" +#include "arrow/result.h" +#include "arrow/status.h" + +namespace arrow { +namespace compute { + +// +// 0 byte - 7 bucket | 1. byte - 6 bucket | ... +// --------------------------------------------------- +// | Empty bit* | Empty bit | +// --------------------------------------------------- +// | 7-bit hash | 7-bit hash | +// --------------------------------------------------- +// * Empty bucket has value 0x80. Non-empty bucket has highest bit set to 0. +// ** The order of bytes is reversed - highest byte represents 0th bucket. +// No other part of data structure uses this reversed order. +// +class SwissTable { + public: + SwissTable() = default; + ~SwissTable() { cleanup(); } + + using EqualImpl = + std::function; + using AppendImpl = std::function; + + Status init(const arrow::internal::CpuInfo* cpu_info, MemoryPool* pool, + util::TempVectorStack* temp_stack, int log_minibatch, EqualImpl equal_impl, + AppendImpl append_impl); + void cleanup(); + + Status map(const int ckeys, const uint32_t* hashes, uint32_t* outgroupids); + + private: + // Lookup helpers + + /// \brief Scan bytes in block in reverse and stop as soon + /// as a position of interest is found. + /// + /// Positions of interest: + /// a) slot with a matching stamp is encountered, + /// b) first empty slot is encountered, + /// c) we reach the end of the block. + /// + /// \param[in] block 8 byte block of hash table + /// \param[in] stamp 7 bits of hash used as a stamp + /// \param[in] start_slot Index of the first slot in the block to start search from. We + /// assume that this index always points to a non-empty slot, equivalently + /// that it comes before any empty slots. (Used only by one template + /// variant.) + /// \param[out] out_slot index corresponding to the discovered position of interest (8 + /// represents end of block). + /// \param[out] out_match_found an integer flag (0 or 1) indicating if we found a + /// matching stamp. + template + inline void search_block(uint64_t block, int stamp, int start_slot, int* out_slot, + int* out_match_found); + + /// \brief Extract group id for a given slot in a given block. + /// + /// Group ids follow in memory after 64-bit block data. + /// Maximum number of groups inserted is equal to the number + /// of all slots in all blocks, which is 8 * the number of blocks. + /// Group ids are bit packed using that maximum to determine the necessary number of + /// bits. + inline uint64_t extract_group_id(const uint8_t* block_ptr, int slot, + uint64_t group_id_mask); + + inline uint64_t next_slot_to_visit(uint64_t block_index, int slot, int match_found); + + inline void insert(uint8_t* block_base, uint64_t slot_id, uint32_t hash, uint8_t stamp, + uint32_t group_id); + + // + // First hash table access + // Find first match in the first block. + // Possible cases: + // 1. Stamp match in a block + // 2. No stamp match in a block, no empty buckets in a block + // 3. No stamp match in a block, empty buckets in a block + // + template + void lookup_1(const uint16_t* selection, const int num_keys, const uint32_t* hashes, + uint8_t* out_match_bitvector, uint32_t* out_group_ids, + uint32_t* out_slot_ids); +#if defined(ARROW_HAVE_AVX2) + void lookup_1_avx2_x8(const int num_hashes, const uint32_t* hashes, + uint8_t* out_match_bitvector, uint32_t* out_group_ids, + uint32_t* out_next_slot_ids); + void lookup_1_avx2_x32(const int num_hashes, const uint32_t* hashes, + uint8_t* out_match_bitvector, uint32_t* out_group_ids, + uint32_t* out_next_slot_ids); +#endif + + // Completing hash table lookup post first access + Status lookup_2(const uint32_t* hashes, uint32_t* inout_num_selected, + uint16_t* inout_selection, bool* out_need_resize, + uint32_t* out_group_ids, uint32_t* out_next_slot_ids); + + // Resize small hash tables when 50% full (up to 8KB). + // Resize large hash tables when 75% full. + Status grow_double(); + + static int num_groupid_bits_from_log_blocks(int log_blocks) { + int required_bits = log_blocks + 3; + return required_bits <= 8 ? 8 + : required_bits <= 16 ? 16 : required_bits <= 32 ? 32 : 64; + } + + // Use 32-bit hash for now + static constexpr int bits_hash_ = 32; + + // Number of hash bits stored in slots in a block. + // The highest bits of hash determine block id. + // The next set of highest bits is a "stamp" stored in a slot in a block. + static constexpr int bits_stamp_ = 7; + + // Padding bytes added at the end of buffers for ease of SIMD access + static constexpr int padding_ = 64; + + int log_minibatch_; + // Base 2 log of the number of blocks + int log_blocks_ = 0; + // Number of keys inserted into hash table + uint32_t num_inserted_ = 0; + + // Data for blocks. + // Each block has 8x of one byte stamp slots, followed by 8x of bit packed group ids. + // In 8B stamp word, the order of bytes is reversed. Group ids are in normal order. + // There is 64B padding at the end. + uint8_t* blocks_; + + // Array of hashes of values inserted into slots. + // Undefined if the corresponding slot is empty. + // There is 64B padding at the end. + uint32_t* hashes_; + + MemoryPool* pool_; + util::TempVectorStack* temp_stack_; + + EqualImpl equal_impl_; + AppendImpl append_impl_; + + const arrow::internal::CpuInfo* cpu_info_; +}; + +} // namespace compute +} // namespace arrow diff --git a/cpp/src/arrow/compute/exec/key_map_avx2.cc b/cpp/src/arrow/compute/exec/key_map_avx2.cc new file mode 100644 index 00000000000..edccae70189 --- /dev/null +++ b/cpp/src/arrow/compute/exec/key_map_avx2.cc @@ -0,0 +1,406 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#include + +#include "arrow/compute/exec/key_map.h" + +namespace arrow { +namespace compute { + +#if defined(ARROW_HAVE_AVX2) + +// TODO: Why it is OK to round up number of rows internally: +// All of the buffers: hashes, out_match_bitvector, out_group_ids, out_next_slot_ids +// are temporary buffers of group id mapping. +// Temporary buffers are buffers that live only within the boundaries of a single +// minibatch. Temporary buffers add 64B at the end, so that SIMD code does not have to +// worry about reading and writing outside of the end of the buffer up to 64B. If the +// hashes array contains garbage after the last element, it cannot cause computation to +// fail, since any random data is a valid hash for the purpose of lookup. +// +// This is more or less translation of equivalent scalar code, adjusted for a different +// instruction set (missing lzcnt for instance). +// +void SwissTable::lookup_1_avx2_x8(const int num_hashes, const uint32_t* hashes, + uint8_t* out_match_bitvector, uint32_t* out_group_ids, + uint32_t* out_next_slot_ids) { + // Number of inputs processed together in a loop + constexpr int unroll = 8; + + const int num_group_id_bits = num_groupid_bits_from_log_blocks(log_blocks_); + uint32_t group_id_mask = ~static_cast(0) >> (32 - num_group_id_bits); + const __m256i* vhash_ptr = reinterpret_cast(hashes); + const __m256i vstamp_mask = _mm256_set1_epi32((1 << bits_stamp_) - 1); + + // TODO: explain why it is ok to process hashes outside of buffer boundaries + for (int i = 0; i < ((num_hashes + unroll - 1) / unroll); ++i) { + constexpr uint64_t kEachByteIs8 = 0x0808080808080808ULL; + constexpr uint64_t kByteSequenceOfPowersOf2 = 0x8040201008040201ULL; + + // Calculate block index and hash stamp for a byte in a block + // + __m256i vhash = _mm256_loadu_si256(vhash_ptr + i); + __m256i vblock_id = _mm256_srlv_epi32( + vhash, _mm256_set1_epi32(bits_hash_ - bits_stamp_ - log_blocks_)); + __m256i vstamp = _mm256_and_si256(vblock_id, vstamp_mask); + vblock_id = _mm256_srli_epi32(vblock_id, bits_stamp_); + + // We now split inputs and process 4 at a time, + // in order to process 64-bit blocks + // + __m256i vblock_offset = + _mm256_mullo_epi32(vblock_id, _mm256_set1_epi32(num_group_id_bits + 8)); + __m256i voffset_A = _mm256_and_si256(vblock_offset, _mm256_set1_epi64x(0xffffffff)); + __m256i vstamp_A = _mm256_and_si256(vstamp, _mm256_set1_epi64x(0xffffffff)); + __m256i voffset_B = _mm256_srli_epi64(vblock_offset, 32); + __m256i vstamp_B = _mm256_srli_epi64(vstamp, 32); + __m256i vblock_A = + _mm256_i64gather_epi64(reinterpret_cast(blocks_), voffset_A, 1); + __m256i vblock_B = + _mm256_i64gather_epi64(reinterpret_cast(blocks_), voffset_B, 1); + __m256i vblock_highbits_A = + _mm256_cmpeq_epi8(vblock_A, _mm256_set1_epi8(static_cast(0x80))); + __m256i vblock_highbits_B = + _mm256_cmpeq_epi8(vblock_B, _mm256_set1_epi8(static_cast(0x80))); + __m256i vbyte_repeat_pattern = + _mm256_setr_epi64x(0ULL, kEachByteIs8, 0ULL, kEachByteIs8); + vstamp_A = _mm256_shuffle_epi8( + vstamp_A, _mm256_or_si256(vbyte_repeat_pattern, vblock_highbits_A)); + vstamp_B = _mm256_shuffle_epi8( + vstamp_B, _mm256_or_si256(vbyte_repeat_pattern, vblock_highbits_B)); + __m256i vmatches_A = _mm256_cmpeq_epi8(vblock_A, vstamp_A); + __m256i vmatches_B = _mm256_cmpeq_epi8(vblock_B, vstamp_B); + __m256i vmatch_found = _mm256_andnot_si256( + _mm256_blend_epi32(_mm256_cmpeq_epi64(vmatches_A, _mm256_setzero_si256()), + _mm256_cmpeq_epi64(vmatches_B, _mm256_setzero_si256()), + 0xaa), // 0b10101010 + _mm256_set1_epi8(static_cast(0xff))); + vmatches_A = + _mm256_sad_epu8(_mm256_and_si256(_mm256_or_si256(vmatches_A, vblock_highbits_A), + _mm256_set1_epi64x(kByteSequenceOfPowersOf2)), + _mm256_setzero_si256()); + vmatches_B = + _mm256_sad_epu8(_mm256_and_si256(_mm256_or_si256(vmatches_B, vblock_highbits_B), + _mm256_set1_epi64x(kByteSequenceOfPowersOf2)), + _mm256_setzero_si256()); + __m256i vmatches = _mm256_or_si256(vmatches_A, _mm256_slli_epi64(vmatches_B, 32)); + + // We are now back to processing 8 at a time. + // Each lane contains 8-bit bit vector marking slots that are matches. + // We need to find leading zeroes count for all slots. + // + // Emulating lzcnt in lowest bytes of 32-bit elements + __m256i vgt = _mm256_cmpgt_epi32(_mm256_set1_epi32(16), vmatches); + __m256i vnext_slot_id = + _mm256_blendv_epi8(_mm256_srli_epi32(vmatches, 4), + _mm256_and_si256(vmatches, _mm256_set1_epi32(0x0f)), vgt); + vnext_slot_id = _mm256_shuffle_epi8( + _mm256_setr_epi8(4, 3, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 4, 3, 2, 2, 1, 1, + 1, 1, 0, 0, 0, 0, 0, 0, 0, 0), + vnext_slot_id); + vnext_slot_id = + _mm256_add_epi32(_mm256_and_si256(vnext_slot_id, _mm256_set1_epi32(0xff)), + _mm256_and_si256(vgt, _mm256_set1_epi32(4))); + + // Lookup group ids + // + __m256i vgroupid_bit_offset = + _mm256_mullo_epi32(_mm256_and_si256(vnext_slot_id, _mm256_set1_epi32(7)), + _mm256_set1_epi32(num_group_id_bits)); + + // This only works for up to 25 bits per group id, since it uses 32-bit gather + // TODO: make sure this will never get called when there are more than 2^25 groups. + __m256i vgroupid = + _mm256_add_epi32(_mm256_srli_epi32(vgroupid_bit_offset, 3), + _mm256_add_epi32(vblock_offset, _mm256_set1_epi32(8))); + vgroupid = _mm256_i32gather_epi32(reinterpret_cast(blocks_), vgroupid, 1); + vgroupid = _mm256_srlv_epi32( + vgroupid, _mm256_and_si256(vgroupid_bit_offset, _mm256_set1_epi32(7))); + vgroupid = _mm256_and_si256(vgroupid, _mm256_set1_epi32(group_id_mask)); + + // Convert slot id relative to the block to slot id relative to the beginnning of the + // table + // + vnext_slot_id = _mm256_add_epi32( + _mm256_add_epi32(vnext_slot_id, + _mm256_and_si256(vmatch_found, _mm256_set1_epi32(1))), + _mm256_slli_epi32(vblock_id, 3)); + + // Convert match found vector from 32-bit elements to bit vector + out_match_bitvector[i] = _pext_u32(_mm256_movemask_epi8(vmatch_found), + 0x11111111); // 0b00010001 repeated 4x + _mm256_storeu_si256(reinterpret_cast<__m256i*>(out_group_ids) + i, vgroupid); + _mm256_storeu_si256(reinterpret_cast<__m256i*>(out_next_slot_ids) + i, vnext_slot_id); + } +} + +// Take a set of 16 64-bit elements, +// Output one AVX2 register per byte (0 to 7), containing a sequence of 16 bytes, +// one from each input 64-bit word, all from the same position in 64-bit word. +// 16 bytes are replicated in lower and upper half of each output register. +// +inline void split_bytes_avx2(__m256i word0, __m256i word1, __m256i word2, __m256i word3, + __m256i& byte0, __m256i& byte1, __m256i& byte2, + __m256i& byte3, __m256i& byte4, __m256i& byte5, + __m256i& byte6, __m256i& byte7) { + __m256i word01lo = _mm256_unpacklo_epi8( + word0, word1); // {a0, e0, a1, e1, ... a7, e7, c0, g0, c1, g1, ... c7, g7} + __m256i word23lo = _mm256_unpacklo_epi8( + word2, word3); // {i0, m0, i1, m1, ... i7, m7, k0, o0, k1, o1, ... k7, o7} + __m256i word01hi = _mm256_unpackhi_epi8( + word0, word1); // {b0, f0, b1, f1, ... b7, f1, d0, h0, d1, h1, ... d7, h7} + __m256i word23hi = _mm256_unpackhi_epi8( + word2, word3); // {j0, n0, j1, n1, ... j7, n7, l0, p0, l1, p1, ... l7, p7} + + __m256i a = + _mm256_unpacklo_epi16(word01lo, word01hi); // {a0, e0, b0, f0, ... a3, e3, b3, f3, + // c0, g0, d0, h0, ... c3, g3, d3, h3} + __m256i b = + _mm256_unpacklo_epi16(word23lo, word23hi); // {i0, m0, j0, n0, ... i3, m3, j3, n3, + // k0, o0, l0, p0, ... k3, o3, l3, p3} + __m256i c = + _mm256_unpackhi_epi16(word01lo, word01hi); // {a4, e4, b4, f4, ... a7, e7, b7, f7, + // c4, g4, d4, h4, ... c7, g7, d7, h7} + __m256i d = + _mm256_unpackhi_epi16(word23lo, word23hi); // {i4, m4, j4, n4, ... i7, m7, j7, n7, + // k4, o4, l4, p4, ... k7, o7, l7, p7} + + __m256i byte01 = _mm256_unpacklo_epi32( + a, b); // {a0, e0, b0, f0, i0, m0, j0, n0, a1, e1, b1, f1, i1, m1, j1, n1, c0, g0, + // d0, h0, k0, o0, l0, p0, ...} + __m256i shuffle_const = + _mm256_setr_epi8(0, 2, 8, 10, 1, 3, 9, 11, 4, 6, 12, 14, 5, 7, 13, 15, 0, 2, 8, 10, + 1, 3, 9, 11, 4, 6, 12, 14, 5, 7, 13, 15); + byte01 = _mm256_permute4x64_epi64( + byte01, 0xd8); // 11011000 b - swapping middle two 64-bit elements + byte01 = _mm256_shuffle_epi8(byte01, shuffle_const); + __m256i byte23 = _mm256_unpackhi_epi32(a, b); + byte23 = _mm256_permute4x64_epi64(byte23, 0xd8); + byte23 = _mm256_shuffle_epi8(byte23, shuffle_const); + __m256i byte45 = _mm256_unpacklo_epi32(c, d); + byte45 = _mm256_permute4x64_epi64(byte45, 0xd8); + byte45 = _mm256_shuffle_epi8(byte45, shuffle_const); + __m256i byte67 = _mm256_unpackhi_epi32(c, d); + byte67 = _mm256_permute4x64_epi64(byte67, 0xd8); + byte67 = _mm256_shuffle_epi8(byte67, shuffle_const); + + byte0 = _mm256_permute4x64_epi64(byte01, 0x44); // 01000100 b + byte1 = _mm256_permute4x64_epi64(byte01, 0xee); // 11101110 b + byte2 = _mm256_permute4x64_epi64(byte23, 0x44); // 01000100 b + byte3 = _mm256_permute4x64_epi64(byte23, 0xee); // 11101110 b + byte4 = _mm256_permute4x64_epi64(byte45, 0x44); // 01000100 b + byte5 = _mm256_permute4x64_epi64(byte45, 0xee); // 11101110 b + byte6 = _mm256_permute4x64_epi64(byte67, 0x44); // 01000100 b + byte7 = _mm256_permute4x64_epi64(byte67, 0xee); // 11101110 b +} + +// This one can only process a multiple of 32 values. +// The caller needs to process the remaining tail, if the input is not divisible by 32, +// using a different method. +// TODO: Explain the idea behind storing arrays in SIMD registers. +// Explain why it is faster with SIMD than using memory loads. +void SwissTable::lookup_1_avx2_x32(const int num_hashes, const uint32_t* hashes, + uint8_t* out_match_bitvector, uint32_t* out_group_ids, + uint32_t* out_next_slot_ids) { + constexpr int unroll = 32; + + // TODO: consider adding the support for 5 + ARROW_DCHECK(log_blocks_ <= 4); + + // Remember that block bytes and group id bytes are in opposite orders in memory of hash + // table. We put them in the same order. + __m256i vblock_byte0, vblock_byte1, vblock_byte2, vblock_byte3, vblock_byte4, + vblock_byte5, vblock_byte6, vblock_byte7; + __m256i vgroupid_byte0, vgroupid_byte1, vgroupid_byte2, vgroupid_byte3, vgroupid_byte4, + vgroupid_byte5, vgroupid_byte6, vgroupid_byte7; + // What we output if there is no match in the block + __m256i vslot_empty_or_end; + + constexpr uint32_t k4ByteSequence_0_4_8_12 = 0x0c080400; + constexpr uint32_t k4ByteSequence_1_5_9_13 = 0x0d090501; + constexpr uint32_t k4ByteSequence_2_6_10_14 = 0x0e0a0602; + constexpr uint32_t k4ByteSequence_3_7_11_15 = 0x0f0b0703; + constexpr uint64_t kEachByteIs1 = 0x0101010101010101ULL; + constexpr uint64_t kByteSequence7DownTo0 = 0x0001020304050607ULL; + constexpr uint64_t kByteSequence15DownTo8 = 0x08090A0B0C0D0E0FULL; + + // Bit unpack group ids into 1B. + // Assemble the sequence of block bytes. + uint64_t block_bytes[16]; + uint64_t groupid_bytes[16]; + const int num_groupid_bits = num_groupid_bits_from_log_blocks(log_blocks_); + uint64_t bit_unpack_mask = ((1 << num_groupid_bits) - 1) * kEachByteIs1; + for (int i = 0; i < (1 << log_blocks_); ++i) { + uint64_t in_groupids = + *reinterpret_cast(blocks_ + (8 + num_groupid_bits) * i + 8); + uint64_t in_blockbytes = + *reinterpret_cast(blocks_ + (8 + num_groupid_bits) * i); + groupid_bytes[i] = _pdep_u64(in_groupids, bit_unpack_mask); + block_bytes[i] = in_blockbytes; + } + + // Split a sequence of 64-bit words into SIMD vectors holding individual bytes + __m256i vblock_words0 = + _mm256_loadu_si256(reinterpret_cast(block_bytes) + 0); + __m256i vblock_words1 = + _mm256_loadu_si256(reinterpret_cast(block_bytes) + 1); + __m256i vblock_words2 = + _mm256_loadu_si256(reinterpret_cast(block_bytes) + 2); + __m256i vblock_words3 = + _mm256_loadu_si256(reinterpret_cast(block_bytes) + 3); + // Reverse the bytes in blocks + __m256i vshuffle_const = + _mm256_setr_epi64x(kByteSequence7DownTo0, kByteSequence15DownTo8, + kByteSequence7DownTo0, kByteSequence15DownTo8); + vblock_words0 = _mm256_shuffle_epi8(vblock_words0, vshuffle_const); + vblock_words1 = _mm256_shuffle_epi8(vblock_words1, vshuffle_const); + vblock_words2 = _mm256_shuffle_epi8(vblock_words2, vshuffle_const); + vblock_words3 = _mm256_shuffle_epi8(vblock_words3, vshuffle_const); + split_bytes_avx2(vblock_words0, vblock_words1, vblock_words2, vblock_words3, + vblock_byte0, vblock_byte1, vblock_byte2, vblock_byte3, vblock_byte4, + vblock_byte5, vblock_byte6, vblock_byte7); + split_bytes_avx2( + _mm256_loadu_si256(reinterpret_cast(groupid_bytes) + 0), + _mm256_loadu_si256(reinterpret_cast(groupid_bytes) + 1), + _mm256_loadu_si256(reinterpret_cast(groupid_bytes) + 2), + _mm256_loadu_si256(reinterpret_cast(groupid_bytes) + 3), + vgroupid_byte0, vgroupid_byte1, vgroupid_byte2, vgroupid_byte3, vgroupid_byte4, + vgroupid_byte5, vgroupid_byte6, vgroupid_byte7); + + // Calculate the slot to output when there is no match in a block. + // It will be the index of the first empty slot or 8 (the number of slots in block) + // if there are no empty slots. + vslot_empty_or_end = _mm256_set1_epi8(8); + { + __m256i vis_empty; +#define CMP(VBLOCKBYTE, BYTENUM) \ + vis_empty = \ + _mm256_cmpeq_epi8(VBLOCKBYTE, _mm256_set1_epi8(static_cast(0x80))); \ + vslot_empty_or_end = \ + _mm256_blendv_epi8(vslot_empty_or_end, _mm256_set1_epi8(BYTENUM), vis_empty); + CMP(vblock_byte7, 7); + CMP(vblock_byte6, 6); + CMP(vblock_byte5, 5); + CMP(vblock_byte4, 4); + CMP(vblock_byte3, 3); + CMP(vblock_byte2, 2); + CMP(vblock_byte1, 1); + CMP(vblock_byte0, 0); +#undef CMP + } + + const int block_id_mask = (1 << log_blocks_) - 1; + + for (int i = 0; i < num_hashes / unroll; ++i) { + __m256i vhash0 = + _mm256_loadu_si256(reinterpret_cast(hashes) + 4 * i + 0); + __m256i vhash1 = + _mm256_loadu_si256(reinterpret_cast(hashes) + 4 * i + 1); + __m256i vhash2 = + _mm256_loadu_si256(reinterpret_cast(hashes) + 4 * i + 2); + __m256i vhash3 = + _mm256_loadu_si256(reinterpret_cast(hashes) + 4 * i + 3); + + // We will get input in byte lanes in the order: [0, 8, 16, 24, 1, 9, 17, 25, 2, 10, + // 18, 26, ...] + vhash0 = _mm256_or_si256(_mm256_srli_epi32(vhash0, 16), + _mm256_and_si256(vhash2, _mm256_set1_epi32(0xffff0000))); + vhash1 = _mm256_or_si256(_mm256_srli_epi32(vhash1, 16), + _mm256_and_si256(vhash3, _mm256_set1_epi32(0xffff0000))); + __m256i vstamp_A = _mm256_and_si256( + _mm256_srlv_epi32(vhash0, _mm256_set1_epi32(16 - log_blocks_ - 7)), + _mm256_set1_epi16(0x7f)); + __m256i vstamp_B = _mm256_and_si256( + _mm256_srlv_epi32(vhash1, _mm256_set1_epi32(16 - log_blocks_ - 7)), + _mm256_set1_epi16(0x7f)); + __m256i vstamp = _mm256_or_si256(vstamp_A, _mm256_slli_epi16(vstamp_B, 8)); + __m256i vblock_id_A = + _mm256_and_si256(_mm256_srlv_epi32(vhash0, _mm256_set1_epi32(16 - log_blocks_)), + _mm256_set1_epi16(block_id_mask)); + __m256i vblock_id_B = + _mm256_and_si256(_mm256_srlv_epi32(vhash1, _mm256_set1_epi32(16 - log_blocks_)), + _mm256_set1_epi16(block_id_mask)); + __m256i vblock_id = _mm256_or_si256(vblock_id_A, _mm256_slli_epi16(vblock_id_B, 8)); + + // Visit all block bytes in reverse order (overwriting data on multiple matches) + __m256i vmatch_found = _mm256_setzero_si256(); + __m256i vslot_id = _mm256_shuffle_epi8(vslot_empty_or_end, vblock_id); + __m256i vgroup_id = _mm256_setzero_si256(); +#define CMP(VBLOCK_BYTE, VGROUPID_BYTE, BYTENUM) \ + { \ + __m256i vcmp = \ + _mm256_cmpeq_epi8(_mm256_shuffle_epi8(VBLOCK_BYTE, vblock_id), vstamp); \ + vmatch_found = _mm256_or_si256(vmatch_found, vcmp); \ + vgroup_id = _mm256_blendv_epi8(vgroup_id, \ + _mm256_shuffle_epi8(VGROUPID_BYTE, vblock_id), vcmp); \ + vslot_id = _mm256_blendv_epi8(vslot_id, _mm256_set1_epi8(BYTENUM + 1), vcmp); \ + } + CMP(vblock_byte7, vgroupid_byte7, 7); + CMP(vblock_byte6, vgroupid_byte6, 6); + CMP(vblock_byte5, vgroupid_byte5, 5); + CMP(vblock_byte4, vgroupid_byte4, 4); + CMP(vblock_byte3, vgroupid_byte3, 3); + CMP(vblock_byte2, vgroupid_byte2, 2); + CMP(vblock_byte1, vgroupid_byte1, 1); + CMP(vblock_byte0, vgroupid_byte0, 0); +#undef CMP + + vslot_id = _mm256_add_epi8(vslot_id, _mm256_slli_epi32(vblock_id, 3)); + // So far the output is in the order: [0, 8, 16, 24, 1, 9, 17, 25, 2, 10, 18, 26, ...] + vmatch_found = _mm256_shuffle_epi8( + vmatch_found, + _mm256_setr_epi32(k4ByteSequence_0_4_8_12, k4ByteSequence_1_5_9_13, + k4ByteSequence_2_6_10_14, k4ByteSequence_3_7_11_15, + k4ByteSequence_0_4_8_12, k4ByteSequence_1_5_9_13, + k4ByteSequence_2_6_10_14, k4ByteSequence_3_7_11_15)); + // Now it is: [0, 1, 2, 3, 8, 9, 10, 11, 16, 17, 18, 19, 24, 25, 26, 27, | 4, 5, 6, 7, + // 12, 13, 14, 15, ...] + vmatch_found = _mm256_permutevar8x32_epi32(vmatch_found, + _mm256_setr_epi32(0, 4, 1, 5, 2, 6, 3, 7)); + + reinterpret_cast(out_match_bitvector)[i] = + _mm256_movemask_epi8(vmatch_found); + _mm256_storeu_si256(reinterpret_cast<__m256i*>(out_group_ids) + 4 * i + 0, + _mm256_and_si256(vgroup_id, _mm256_set1_epi32(0xff))); + _mm256_storeu_si256( + reinterpret_cast<__m256i*>(out_group_ids) + 4 * i + 1, + _mm256_and_si256(_mm256_srli_epi32(vgroup_id, 8), _mm256_set1_epi32(0xff))); + _mm256_storeu_si256( + reinterpret_cast<__m256i*>(out_group_ids) + 4 * i + 2, + _mm256_and_si256(_mm256_srli_epi32(vgroup_id, 16), _mm256_set1_epi32(0xff))); + _mm256_storeu_si256( + reinterpret_cast<__m256i*>(out_group_ids) + 4 * i + 3, + _mm256_and_si256(_mm256_srli_epi32(vgroup_id, 24), _mm256_set1_epi32(0xff))); + _mm256_storeu_si256(reinterpret_cast<__m256i*>(out_next_slot_ids) + 4 * i + 0, + _mm256_and_si256(vslot_id, _mm256_set1_epi32(0xff))); + _mm256_storeu_si256( + reinterpret_cast<__m256i*>(out_next_slot_ids) + 4 * i + 1, + _mm256_and_si256(_mm256_srli_epi32(vslot_id, 8), _mm256_set1_epi32(0xff))); + _mm256_storeu_si256( + reinterpret_cast<__m256i*>(out_next_slot_ids) + 4 * i + 2, + _mm256_and_si256(_mm256_srli_epi32(vslot_id, 16), _mm256_set1_epi32(0xff))); + _mm256_storeu_si256( + reinterpret_cast<__m256i*>(out_next_slot_ids) + 4 * i + 3, + _mm256_and_si256(_mm256_srli_epi32(vslot_id, 24), _mm256_set1_epi32(0xff))); + } +} + +#endif + +} // namespace compute +} // namespace arrow diff --git a/cpp/src/arrow/compute/exec/util.cc b/cpp/src/arrow/compute/exec/util.cc new file mode 100644 index 00000000000..0100367f3f5 --- /dev/null +++ b/cpp/src/arrow/compute/exec/util.cc @@ -0,0 +1,237 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#include "arrow/compute/exec/util.h" + +#include "arrow/util/bit_util.h" +#include "arrow/util/bitmap_ops.h" + +namespace arrow { + +using BitUtil::CountTrailingZeros; + +namespace util { + +inline void BitUtil::bits_to_indexes_helper(uint64_t word, uint16_t base_index, + int* num_indexes, uint16_t* indexes) { + int n = *num_indexes; + while (word) { + indexes[n++] = base_index + static_cast(CountTrailingZeros(word)); + word &= word - 1; + } + *num_indexes = n; +} + +inline void BitUtil::bits_filter_indexes_helper(uint64_t word, + const uint16_t* input_indexes, + int* num_indexes, uint16_t* indexes) { + int n = *num_indexes; + while (word) { + indexes[n++] = input_indexes[CountTrailingZeros(word)]; + word &= word - 1; + } + *num_indexes = n; +} + +template +void BitUtil::bits_to_indexes_internal(const arrow::internal::CpuInfo* cpu_info, + const int num_bits, const uint8_t* bits, + const uint16_t* input_indexes, int* num_indexes, + uint16_t* indexes) { + // 64 bits at a time + constexpr int unroll = 64; + int tail = num_bits % unroll; +#if defined(ARROW_HAVE_AVX2) + if (cpu_info->IsSupported(arrow::internal::CpuInfo::AVX2)) { + if (filter_input_indexes) { + bits_filter_indexes_avx2(bit_to_search, num_bits - tail, bits, input_indexes, + num_indexes, indexes); + } else { + bits_to_indexes_avx2(bit_to_search, num_bits - tail, bits, num_indexes, indexes); + } + } else { +#endif + *num_indexes = 0; + for (int i = 0; i < num_bits / unroll; ++i) { + uint64_t word = reinterpret_cast(bits)[i]; + if (bit_to_search == 0) { + word = ~word; + } + if (filter_input_indexes) { + bits_filter_indexes_helper(word, input_indexes + i * 64, num_indexes, indexes); + } else { + bits_to_indexes_helper(word, i * 64, num_indexes, indexes); + } + } +#if defined(ARROW_HAVE_AVX2) + } +#endif + // Optionally process the last partial word with masking out bits outside range + if (tail) { + uint64_t word = reinterpret_cast(bits)[num_bits / unroll]; + if (bit_to_search == 0) { + word = ~word; + } + word &= ~0ULL >> (64 - tail); + if (filter_input_indexes) { + bits_filter_indexes_helper(word, input_indexes + num_bits - tail, num_indexes, + indexes); + } else { + bits_to_indexes_helper(word, num_bits - tail, num_indexes, indexes); + } + } +} + +void BitUtil::bits_to_indexes(int bit_to_search, const arrow::internal::CpuInfo* cpu_info, + const int num_bits, const uint8_t* bits, int* num_indexes, + uint16_t* indexes) { + if (bit_to_search == 0) { + bits_to_indexes_internal<0, false>(cpu_info, num_bits, bits, nullptr, num_indexes, + indexes); + } else { + ARROW_DCHECK(bit_to_search == 1); + bits_to_indexes_internal<1, false>(cpu_info, num_bits, bits, nullptr, num_indexes, + indexes); + } +} + +void BitUtil::bits_filter_indexes(int bit_to_search, + const arrow::internal::CpuInfo* cpu_info, + const int num_bits, const uint8_t* bits, + const uint16_t* input_indexes, int* num_indexes, + uint16_t* indexes) { + if (bit_to_search == 0) { + bits_to_indexes_internal<0, true>(cpu_info, num_bits, bits, input_indexes, + num_indexes, indexes); + } else { + ARROW_DCHECK(bit_to_search == 1); + bits_to_indexes_internal<1, true>(cpu_info, num_bits, bits, input_indexes, + num_indexes, indexes); + } +} + +void BitUtil::bits_split_indexes(const arrow::internal::CpuInfo* cpu_info, + const int num_bits, const uint8_t* bits, + int* num_indexes_bit0, uint16_t* indexes_bit0, + uint16_t* indexes_bit1) { + bits_to_indexes(0, cpu_info, num_bits, bits, num_indexes_bit0, indexes_bit0); + int num_indexes_bit1; + bits_to_indexes(1, cpu_info, num_bits, bits, &num_indexes_bit1, indexes_bit1); +} + +void BitUtil::bits_to_bytes_internal(const int num_bits, const uint8_t* bits, + uint8_t* bytes) { + constexpr int unroll = 8; + // Processing 8 bits at a time + for (int i = 0; i < (num_bits + unroll - 1) / unroll; ++i) { + uint8_t bits_next = bits[i]; + // Clear the lowest bit and then make 8 copies of remaining 7 bits, each 7 bits apart + // from the previous. + uint64_t unpacked = static_cast(bits_next & 0xfe) * + ((1ULL << 7) | (1ULL << 14) | (1ULL << 21) | (1ULL << 28) | + (1ULL << 35) | (1ULL << 42) | (1ULL << 49)); + unpacked |= (bits_next & 1); + unpacked &= 0x0101010101010101ULL; + unpacked *= 255; + reinterpret_cast(bytes)[i] = unpacked; + } +} + +void BitUtil::bytes_to_bits_internal(const int num_bits, const uint8_t* bytes, + uint8_t* bits) { + constexpr int unroll = 8; + // Process 8 bits at a time + for (int i = 0; i < (num_bits + unroll - 1) / unroll; ++i) { + uint64_t bytes_next = reinterpret_cast(bytes)[i]; + bytes_next &= 0x0101010101010101ULL; + bytes_next |= (bytes_next >> 7); // Pairs of adjacent output bits in individual bytes + bytes_next |= (bytes_next >> 14); // 4 adjacent output bits in individual bytes + bytes_next |= (bytes_next >> 28); // All 8 output bits in the lowest byte + bits[i] = static_cast(bytes_next & 0xff); + } +} + +void BitUtil::bits_to_bytes(const arrow::internal::CpuInfo* cpu_info, const int num_bits, + const uint8_t* bits, uint8_t* bytes) { + int num_processed = 0; +#if defined(ARROW_HAVE_AVX2) + if (cpu_info->IsSupported(arrow::internal::CpuInfo::AVX2)) { + // The function call below processes whole 32 bit chunks together. + num_processed = num_bits - (num_bits % 32); + bits_to_bytes_avx2(num_processed, bits, bytes); + } +#endif + // Processing 8 bits at a time + constexpr int unroll = 8; + for (int i = num_processed / unroll; i < (num_bits + unroll - 1) / unroll; ++i) { + uint8_t bits_next = bits[i]; + // Clear the lowest bit and then make 8 copies of remaining 7 bits, each 7 bits apart + // from the previous. + uint64_t unpacked = static_cast(bits_next & 0xfe) * + ((1ULL << 7) | (1ULL << 14) | (1ULL << 21) | (1ULL << 28) | + (1ULL << 35) | (1ULL << 42) | (1ULL << 49)); + unpacked |= (bits_next & 1); + unpacked &= 0x0101010101010101ULL; + unpacked *= 255; + reinterpret_cast(bytes)[i] = unpacked; + } +} + +void BitUtil::bytes_to_bits(const arrow::internal::CpuInfo* cpu_info, const int num_bits, + const uint8_t* bytes, uint8_t* bits) { + int num_processed = 0; +#if defined(ARROW_HAVE_AVX2) + if (cpu_info->IsSupported(arrow::internal::CpuInfo::AVX2)) { + // The function call below processes whole 32 bit chunks together. + num_processed = num_bits - (num_bits % 32); + bytes_to_bits_avx2(num_processed, bytes, bits); + } +#endif + // Process 8 bits at a time + constexpr int unroll = 8; + for (int i = num_processed / unroll; i < (num_bits + unroll - 1) / unroll; ++i) { + uint64_t bytes_next = reinterpret_cast(bytes)[i]; + bytes_next &= 0x0101010101010101ULL; + bytes_next |= (bytes_next >> 7); // Pairs of adjacent output bits in individual bytes + bytes_next |= (bytes_next >> 14); // 4 adjacent output bits in individual bytes + bytes_next |= (bytes_next >> 28); // All 8 output bits in the lowest byte + bits[i] = static_cast(bytes_next & 0xff); + } +} + +bool BitUtil::are_all_bytes_zero(const arrow::internal::CpuInfo* cpu_info, + const uint8_t* bytes, uint32_t num_bytes) { +#if defined(ARROW_HAVE_AVX2) + if (cpu_info->IsSupported(arrow::internal::CpuInfo::AVX2)) { + return are_all_bytes_zero_avx2(bytes, num_bytes); + } +#endif + uint64_t result_or = 0; + uint32_t i; + for (i = 0; i < num_bytes / 8; ++i) { + uint64_t x = reinterpret_cast(bytes)[i]; + result_or |= x; + } + if (num_bytes % 8 > 0) { + uint64_t tail = 0; + result_or |= memcmp(bytes + i * 8, &tail, num_bytes % 8); + } + return result_or == 0; +} + +} // namespace util +} // namespace arrow diff --git a/cpp/src/arrow/compute/exec/util.h b/cpp/src/arrow/compute/exec/util.h new file mode 100644 index 00000000000..62f1bc70ab4 --- /dev/null +++ b/cpp/src/arrow/compute/exec/util.h @@ -0,0 +1,168 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#pragma once + +#include +#include + +#include "arrow/buffer.h" +#include "arrow/memory_pool.h" +#include "arrow/result.h" +#include "arrow/status.h" +#include "arrow/util/cpu_info.h" +#include "arrow/util/logging.h" + +#if defined(__clang__) || defined(__GNUC__) +#define BYTESWAP(x) __builtin_bswap64(x) +#define ROTL(x, n) (((x) << (n)) | ((x) >> (32 - (n)))) +#elif defined(_MSC_VER) +#include +#define BYTESWAP(x) _byteswap_uint64(x) +#define ROTL(x, n) _rotl((x), (n)) +#endif + +namespace arrow { +namespace util { + +/// Storage used to allocate temporary vectors of a batch size. +/// Temporary vectors should resemble allocating temporary variables on the stack +/// but in the context of vectorized processing where we need to store a vector of +/// temporaries instead of a single value. +class TempVectorStack { + template + friend class TempVectorHolder; + + public: + Status Init(MemoryPool* pool, int64_t size) { + num_vectors_ = 0; + top_ = 0; + buffer_size_ = size; + ARROW_ASSIGN_OR_RAISE(auto buffer, AllocateResizableBuffer(size, pool)); + buffer_ = std::move(buffer); + return Status::OK(); + } + + private: + void alloc(uint32_t num_bytes, uint8_t** data, int* id) { + int64_t old_top = top_; + top_ += num_bytes + padding; + // Stack overflow check + ARROW_DCHECK(top_ <= buffer_size_); + *data = buffer_->mutable_data() + old_top; + *id = num_vectors_++; + } + void release(int id, uint32_t num_bytes) { + ARROW_DCHECK(num_vectors_ == id + 1); + int64_t size = num_bytes + padding; + ARROW_DCHECK(top_ >= size); + top_ -= size; + --num_vectors_; + } + static constexpr int64_t padding = 64; + int num_vectors_; + int64_t top_; + std::unique_ptr buffer_; + int64_t buffer_size_; +}; + +template +class TempVectorHolder { + friend class TempVectorStack; + + public: + ~TempVectorHolder() { stack_->release(id_, num_elements_ * sizeof(T)); } + T* mutable_data() { return reinterpret_cast(data_); } + TempVectorHolder(TempVectorStack* stack, uint32_t num_elements) { + stack_ = stack; + num_elements_ = num_elements; + stack_->alloc(num_elements * sizeof(T), &data_, &id_); + } + + private: + TempVectorStack* stack_; + uint8_t* data_; + int id_; + uint32_t num_elements_; +}; + +class BitUtil { + public: + static void bits_to_indexes(int bit_to_search, const arrow::internal::CpuInfo* cpu_info, + const int num_bits, const uint8_t* bits, int* num_indexes, + uint16_t* indexes); + + static void bits_filter_indexes(int bit_to_search, + const arrow::internal::CpuInfo* cpu_info, + const int num_bits, const uint8_t* bits, + const uint16_t* input_indexes, int* num_indexes, + uint16_t* indexes); + + // Input and output indexes may be pointing to the same data (in-place filtering). + static void bits_split_indexes(const arrow::internal::CpuInfo* cpu_info, + const int num_bits, const uint8_t* bits, + int* num_indexes_bit0, uint16_t* indexes_bit0, + uint16_t* indexes_bit1); + + // Bit 1 is replaced with byte 0xFF. + static void bits_to_bytes(const arrow::internal::CpuInfo* cpu_info, const int num_bits, + const uint8_t* bits, uint8_t* bytes); + // Return highest bit of each byte. + static void bytes_to_bits(const arrow::internal::CpuInfo* cpu_info, const int num_bits, + const uint8_t* bytes, uint8_t* bits); + + static bool are_all_bytes_zero(const arrow::internal::CpuInfo* cpu_info, + const uint8_t* bytes, uint32_t num_bytes); + + private: + inline static void bits_to_indexes_helper(uint64_t word, uint16_t base_index, + int* num_indexes, uint16_t* indexes); + inline static void bits_filter_indexes_helper(uint64_t word, + const uint16_t* input_indexes, + int* num_indexes, uint16_t* indexes); + template + static void bits_to_indexes_internal(const arrow::internal::CpuInfo* cpu_info, + const int num_bits, const uint8_t* bits, + const uint16_t* input_indexes, int* num_indexes, + uint16_t* indexes); + static void bits_to_bytes_internal(const int num_bits, const uint8_t* bits, + uint8_t* bytes); + static void bytes_to_bits_internal(const int num_bits, const uint8_t* bytes, + uint8_t* bits); + +#if defined(ARROW_HAVE_AVX2) + static void bits_to_indexes_avx2(int bit_to_search, const int num_bits, + const uint8_t* bits, int* num_indexes, + uint16_t* indexes); + static void bits_filter_indexes_avx2(int bit_to_search, const int num_bits, + const uint8_t* bits, const uint16_t* input_indexes, + int* num_indexes, uint16_t* indexes); + template + static void bits_to_indexes_imp_avx2(const int num_bits, const uint8_t* bits, + int* num_indexes, uint16_t* indexes); + template + static void bits_filter_indexes_imp_avx2(const int num_bits, const uint8_t* bits, + const uint16_t* input_indexes, + int* num_indexes, uint16_t* indexes); + static void bits_to_bytes_avx2(const int num_bits, const uint8_t* bits, uint8_t* bytes); + static void bytes_to_bits_avx2(const int num_bits, const uint8_t* bytes, uint8_t* bits); + static bool are_all_bytes_zero_avx2(const uint8_t* bytes, uint32_t num_bytes); +#endif +}; + +} // namespace util +} // namespace arrow diff --git a/cpp/src/arrow/compute/exec/util_avx2.cc b/cpp/src/arrow/compute/exec/util_avx2.cc new file mode 100644 index 00000000000..8cf0104db46 --- /dev/null +++ b/cpp/src/arrow/compute/exec/util_avx2.cc @@ -0,0 +1,217 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +#include + +#include "arrow/compute/exec/util.h" +#include "arrow/util/bit_util.h" + +namespace arrow { +namespace util { + +#if defined(ARROW_HAVE_AVX2) + +void BitUtil::bits_to_indexes_avx2(int bit_to_search, const int num_bits, + const uint8_t* bits, int* num_indexes, + uint16_t* indexes) { + if (bit_to_search == 0) { + bits_to_indexes_imp_avx2<0>(num_bits, bits, num_indexes, indexes); + } else { + ARROW_DCHECK(bit_to_search == 1); + bits_to_indexes_imp_avx2<1>(num_bits, bits, num_indexes, indexes); + } +} + +template +void BitUtil::bits_to_indexes_imp_avx2(const int num_bits, const uint8_t* bits, + int* num_indexes, uint16_t* indexes) { + // 64 bits at a time + constexpr int unroll = 64; + + // The caller takes care of processing the remaining bits at the end outside of the + // multiples of 64 + ARROW_DCHECK(num_bits % unroll == 0); + + constexpr uint64_t kEachByteIs1 = 0X0101010101010101ULL; + constexpr uint64_t kEachByteIs8 = 0x0808080808080808ULL; + constexpr uint64_t kByteSequence0To7 = 0x0706050403020100ULL; + + uint8_t byte_indexes[64]; + const uint64_t incr = kEachByteIs8; + const uint64_t mask = kByteSequence0To7; + *num_indexes = 0; + for (int i = 0; i < num_bits / unroll; ++i) { + uint64_t word = reinterpret_cast(bits)[i]; + if (bit_to_search == 0) { + word = ~word; + } + uint64_t base = 0; + int num_indexes_loop = 0; + while (word) { + uint64_t byte_indexes_next = + _pext_u64(mask, _pdep_u64(word, kEachByteIs1) * 0xff) + base; + *reinterpret_cast(byte_indexes + num_indexes_loop) = byte_indexes_next; + base += incr; + num_indexes_loop += static_cast(arrow::BitUtil::PopCount(word & 0xff)); + word >>= 8; + } + // Unpack indexes to 16-bits and either add the base of i * 64 or shuffle input + // indexes + for (int j = 0; j < (num_indexes_loop + 15) / 16; ++j) { + __m256i output = _mm256_cvtepi8_epi16( + _mm_loadu_si128(reinterpret_cast(byte_indexes) + j)); + output = _mm256_add_epi16(output, _mm256_set1_epi16(i * 64)); + _mm256_storeu_si256(((__m256i*)(indexes + *num_indexes)) + j, output); + } + *num_indexes += num_indexes_loop; + } +} + +void BitUtil::bits_filter_indexes_avx2(int bit_to_search, const int num_bits, + const uint8_t* bits, const uint16_t* input_indexes, + int* num_indexes, uint16_t* indexes) { + if (bit_to_search == 0) { + bits_filter_indexes_imp_avx2<0>(num_bits, bits, input_indexes, num_indexes, indexes); + } else { + bits_filter_indexes_imp_avx2<1>(num_bits, bits, input_indexes, num_indexes, indexes); + } +} + +template +void BitUtil::bits_filter_indexes_imp_avx2(const int num_bits, const uint8_t* bits, + const uint16_t* input_indexes, + int* out_num_indexes, uint16_t* indexes) { + // 64 bits at a time + constexpr int unroll = 64; + + // The caller takes care of processing the remaining bits at the end outside of the + // multiples of 64 + ARROW_DCHECK(num_bits % unroll == 0); + + constexpr uint64_t kRepeatedBitPattern0001 = 0x1111111111111111ULL; + constexpr uint64_t k4BitSequence0To15 = 0xfedcba9876543210ULL; + constexpr uint64_t kByteSequence_0_0_1_1_2_2_3_3 = 0x0303020201010000ULL; + constexpr uint64_t kByteSequence_4_4_5_5_6_6_7_7 = 0x0707060605050404ULL; + constexpr uint64_t kByteSequence_0_2_4_6_8_10_12_14 = 0x0e0c0a0806040200ULL; + constexpr uint64_t kByteSequence_1_3_5_7_9_11_13_15 = 0x0f0d0b0907050301ULL; + constexpr uint64_t kByteSequence_0_8_1_9_2_10_3_11 = 0x0b030a0209010800ULL; + constexpr uint64_t kByteSequence_4_12_5_13_6_14_7_15 = 0x0f070e060d050c04ULL; + + const uint64_t mask = k4BitSequence0To15; + int num_indexes = 0; + for (int i = 0; i < num_bits / unroll; ++i) { + uint64_t word = reinterpret_cast(bits)[i]; + if (bit_to_search == 0) { + word = ~word; + } + + int loop_id = 0; + while (word) { + uint64_t indexes_4bit = + _pext_u64(mask, _pdep_u64(word, kRepeatedBitPattern0001) * 0xf); + // Unpack 4 bit indexes to 8 bits + __m256i indexes_8bit = _mm256_set1_epi64x(indexes_4bit); + indexes_8bit = _mm256_shuffle_epi8( + indexes_8bit, + _mm256_setr_epi64x(kByteSequence_0_0_1_1_2_2_3_3, kByteSequence_4_4_5_5_6_6_7_7, + kByteSequence_0_0_1_1_2_2_3_3, + kByteSequence_4_4_5_5_6_6_7_7)); + indexes_8bit = _mm256_blendv_epi8( + _mm256_and_si256(indexes_8bit, _mm256_set1_epi8(0x0f)), + _mm256_and_si256(_mm256_srli_epi32(indexes_8bit, 4), _mm256_set1_epi8(0x0f)), + _mm256_set1_epi16(static_cast(0xff00))); + __m256i input = + _mm256_loadu_si256(((const __m256i*)input_indexes) + 4 * i + loop_id); + // Shuffle bytes to get low bytes in the first 128-bit lane and high bytes in the + // second + input = _mm256_shuffle_epi8( + input, _mm256_setr_epi64x( + kByteSequence_0_2_4_6_8_10_12_14, kByteSequence_1_3_5_7_9_11_13_15, + kByteSequence_0_2_4_6_8_10_12_14, kByteSequence_1_3_5_7_9_11_13_15)); + input = _mm256_permute4x64_epi64(input, 0xd8); // 0b11011000 + // Apply permutation + __m256i output = _mm256_shuffle_epi8(input, indexes_8bit); + // Move low and high bytes across 128-bit lanes to assemble back 16-bit indexes. + // (This is the reverse of the byte permutation we did on the input) + output = _mm256_permute4x64_epi64(output, + 0xd8); // The reverse of swapping 2nd and 3rd + // 64-bit element is the same permutation + output = _mm256_shuffle_epi8(output, + _mm256_setr_epi64x(kByteSequence_0_8_1_9_2_10_3_11, + kByteSequence_4_12_5_13_6_14_7_15, + kByteSequence_0_8_1_9_2_10_3_11, + kByteSequence_4_12_5_13_6_14_7_15)); + _mm256_storeu_si256((__m256i*)(indexes + num_indexes), output); + num_indexes += static_cast(arrow::BitUtil::PopCount(word & 0xffff)); + word >>= 16; + ++loop_id; + } + } + + *out_num_indexes = num_indexes; +} + +void BitUtil::bits_to_bytes_avx2(const int num_bits, const uint8_t* bits, + uint8_t* bytes) { + constexpr int unroll = 32; + + constexpr uint64_t kEachByteIs1 = 0x0101010101010101ULL; + constexpr uint64_t kEachByteIs2 = 0x0202020202020202ULL; + constexpr uint64_t kEachByteIs3 = 0x0303030303030303ULL; + constexpr uint64_t kByteSequencePowersOf2 = 0x8040201008040201ULL; + + // Processing 32 bits at a time + for (int i = 0; i < num_bits / unroll; ++i) { + __m256i unpacked = _mm256_set1_epi32(reinterpret_cast(bits)[i]); + unpacked = _mm256_shuffle_epi8( + unpacked, _mm256_setr_epi64x(0ULL, kEachByteIs1, kEachByteIs2, kEachByteIs3)); + __m256i bits_in_bytes = _mm256_set1_epi64x(kByteSequencePowersOf2); + unpacked = + _mm256_cmpeq_epi8(bits_in_bytes, _mm256_and_si256(unpacked, bits_in_bytes)); + _mm256_storeu_si256(reinterpret_cast<__m256i*>(bytes) + i, unpacked); + } +} + +void BitUtil::bytes_to_bits_avx2(const int num_bits, const uint8_t* bytes, + uint8_t* bits) { + constexpr int unroll = 32; + // Processing 32 bits at a time + for (int i = 0; i < num_bits / unroll; ++i) { + reinterpret_cast(bits)[i] = _mm256_movemask_epi8( + _mm256_loadu_si256(reinterpret_cast(bytes) + i)); + } +} + +bool BitUtil::are_all_bytes_zero_avx2(const uint8_t* bytes, uint32_t num_bytes) { + __m256i result_or = _mm256_setzero_si256(); + uint32_t i; + for (i = 0; i < num_bytes / 32; ++i) { + __m256i x = _mm256_loadu_si256(reinterpret_cast(bytes) + i); + result_or = _mm256_or_si256(result_or, x); + } + uint32_t result_or32 = _mm256_movemask_epi8(result_or); + if (num_bytes % 32 > 0) { + uint64_t tail[4] = {0, 0, 0, 0}; + result_or32 |= memcmp(bytes + i * 32, tail, num_bytes % 32); + } + return result_or32 == 0; +} + +#endif // ARROW_HAVE_AVX2 + +} // namespace util +} // namespace arrow diff --git a/cpp/src/arrow/compute/kernels/hash_aggregate.cc b/cpp/src/arrow/compute/kernels/hash_aggregate.cc index ae7bf9324db..2ac2c142450 100644 --- a/cpp/src/arrow/compute/kernels/hash_aggregate.cc +++ b/cpp/src/arrow/compute/kernels/hash_aggregate.cc @@ -15,8 +15,6 @@ // specific language governing permissions and limitations // under the License. -#include "arrow/compute/api_aggregate.h" - #include #include #include @@ -24,7 +22,13 @@ #include #include "arrow/buffer_builder.h" +#include "arrow/compute/api_aggregate.h" #include "arrow/compute/api_vector.h" +#include "arrow/compute/exec/key_compare.h" +#include "arrow/compute/exec/key_encode.h" +#include "arrow/compute/exec/key_hash.h" +#include "arrow/compute/exec/key_map.h" +#include "arrow/compute/exec/util.h" #include "arrow/compute/exec_internal.h" #include "arrow/compute/kernel.h" #include "arrow/compute/kernels/aggregate_internal.h" @@ -33,6 +37,7 @@ #include "arrow/util/bitmap_ops.h" #include "arrow/util/bitmap_writer.h" #include "arrow/util/checked_cast.h" +#include "arrow/util/cpu_info.h" #include "arrow/util/make_unique.h" #include "arrow/visitor_inline.h" @@ -436,6 +441,292 @@ struct GrouperImpl : Grouper { std::vector> encoders_; }; +struct GrouperFastImpl : Grouper { + static bool CanUse(const std::vector& keys) { + for (size_t i = 0; i < keys.size(); ++i) { + const auto& key = keys[i].type; + if (is_large_binary_like(key->id())) { + return false; + } + } + return true; + } + + static Result> Make( + const std::vector& keys, ExecContext* ctx) { + auto impl = ::arrow::internal::make_unique(); + impl->ctx_ = ctx; + + RETURN_NOT_OK(impl->temp_stack_.Init(ctx->memory_pool(), 64 * minibatch_size_max_)); + impl->encode_ctx_.cpu_info = arrow::internal::CpuInfo::GetInstance(); + impl->encode_ctx_.stack = &impl->temp_stack_; + + auto num_columns = keys.size(); + impl->col_metadata_.resize(num_columns); + impl->key_types_.resize(num_columns); + impl->dictionaries_.resize(num_columns); + for (size_t icol = 0; icol < num_columns; ++icol) { + const auto& key = keys[icol].type; + if (key->id() == Type::DICTIONARY) { + auto bit_width = checked_cast(*key).bit_width(); + ARROW_DCHECK(bit_width % 8 == 0); + impl->col_metadata_[icol] = + arrow::compute::KeyEncoder::KeyColumnMetadata(true, bit_width / 8); + } else if (key->id() == Type::BOOL) { + impl->col_metadata_[icol] = + arrow::compute::KeyEncoder::KeyColumnMetadata(true, 0); + } else if (is_fixed_width(key->id())) { + impl->col_metadata_[icol] = arrow::compute::KeyEncoder::KeyColumnMetadata( + true, checked_cast(*key).bit_width() / 8); + } else if (is_binary_like(key->id())) { + impl->col_metadata_[icol] = + arrow::compute::KeyEncoder::KeyColumnMetadata(false, sizeof(uint32_t)); + } else { + return Status::NotImplemented("Keys of type ", *key); + } + impl->key_types_[icol] = key; + } + + impl->encoder_.Init(impl->col_metadata_, &impl->encode_ctx_, + /* row_alignment = */ sizeof(uint64_t), + /* string_alignment = */ sizeof(uint64_t)); + RETURN_NOT_OK(impl->rows_.Init(ctx->memory_pool(), impl->encoder_.row_metadata())); + RETURN_NOT_OK( + impl->rows_minibatch_.Init(ctx->memory_pool(), impl->encoder_.row_metadata())); + impl->minibatch_size_ = impl->minibatch_size_min_; + GrouperFastImpl* impl_ptr = impl.get(); + auto equal_func = [impl_ptr]( + int num_keys_to_compare, const uint16_t* selection_may_be_null, + const uint32_t* group_ids, uint32_t* out_num_keys_mismatch, + uint16_t* out_selection_mismatch) { + arrow::compute::KeyCompare::CompareRows( + num_keys_to_compare, selection_may_be_null, group_ids, &impl_ptr->encode_ctx_, + out_num_keys_mismatch, out_selection_mismatch, impl_ptr->rows_minibatch_, + impl_ptr->rows_); + }; + auto append_func = [impl_ptr](int num_keys, const uint16_t* selection) { + return impl_ptr->rows_.AppendSelectionFrom(impl_ptr->rows_minibatch_, num_keys, + selection); + }; + RETURN_NOT_OK(impl->map_.init(impl->encode_ctx_.cpu_info, ctx->memory_pool(), + impl->encode_ctx_.stack, impl->log_minibatch_max_, + equal_func, append_func)); + impl->cols_.resize(num_columns); + constexpr int padding_for_SIMD = 32; + impl->minibatch_hashes_.resize(impl->minibatch_size_max_ + + padding_for_SIMD / sizeof(uint32_t)); + + return std::move(impl); + } + + ~GrouperFastImpl() { map_.cleanup(); } + + Result Consume(const ExecBatch& batch) override { + int64_t num_rows = batch.length; + int num_columns = batch.num_values(); + + // Process dictionaries + for (int icol = 0; icol < num_columns; ++icol) { + if (key_types_[icol]->id() == Type::DICTIONARY) { + auto data = batch[icol].array(); + auto dict = MakeArray(data->dictionary); + if (dictionaries_[icol]) { + if (!dictionaries_[icol]->Equals(dict)) { + // TODO(bkietz) unify if necessary. For now, just error if any batch's + // dictionary differs from the first we saw for this key + return Status::NotImplemented("Unifying differing dictionaries"); + } + } else { + dictionaries_[icol] = std::move(dict); + } + } + } + + std::shared_ptr group_ids; + ARROW_ASSIGN_OR_RAISE( + group_ids, AllocateBuffer(sizeof(uint32_t) * num_rows, ctx_->memory_pool())); + + for (int icol = 0; icol < num_columns; ++icol) { + const uint8_t* non_nulls = nullptr; + if (batch[icol].array()->buffers[0] != NULLPTR) { + non_nulls = batch[icol].array()->buffers[0]->data(); + } + const uint8_t* fixedlen = batch[icol].array()->buffers[1]->data(); + const uint8_t* varlen = nullptr; + if (!col_metadata_[icol].is_fixed_length) { + varlen = batch[icol].array()->buffers[2]->data(); + } + + cols_[icol] = arrow::compute::KeyEncoder::KeyColumnArray( + col_metadata_[icol], num_rows, non_nulls, fixedlen, varlen); + } + + // Split into smaller mini-batches + // + for (uint32_t start_row = 0; start_row < num_rows;) { + uint32_t batch_size_next = std::min(static_cast(minibatch_size_), + static_cast(num_rows) - start_row); + + // Encode + rows_minibatch_.Clean(); + RETURN_NOT_OK(encoder_.PrepareOutputForEncode(start_row, batch_size_next, + &rows_minibatch_, cols_)); + encoder_.Encode(start_row, batch_size_next, &rows_minibatch_, cols_); + + // Compute hash + if (encoder_.row_metadata().is_fixed_length) { + Hashing::hash_fixed(encode_ctx_.cpu_info, batch_size_next, + encoder_.row_metadata().fixed_length, rows_minibatch_.data(1), + minibatch_hashes_.data()); + } else { + auto hash_temp_buf = + util::TempVectorHolder(&temp_stack_, 4 * batch_size_next); + Hashing::hash_varlen(encode_ctx_.cpu_info, batch_size_next, + rows_minibatch_.offsets(), rows_minibatch_.data(2), + hash_temp_buf.mutable_data(), minibatch_hashes_.data()); + } + + // Map + RETURN_NOT_OK( + map_.map(batch_size_next, minibatch_hashes_.data(), + reinterpret_cast(group_ids->mutable_data()) + start_row)); + + start_row += batch_size_next; + + if (minibatch_size_ * 2 <= minibatch_size_max_) { + minibatch_size_ *= 2; + } + } + + return Datum(UInt32Array(batch.length, std::move(group_ids))); + } + + uint32_t num_groups() const override { return static_cast(rows_.length()); } + + Result GetUniques() override { + auto num_columns = static_cast(col_metadata_.size()); + int64_t num_groups = rows_.length(); + + std::vector> non_null_bufs(num_columns); + std::vector> fixedlen_bufs(num_columns); + std::vector> varlen_bufs(num_columns); + + constexpr int padding_bits = 64; + constexpr int padding_for_SIMD = 32; + for (size_t i = 0; i < num_columns; ++i) { + ARROW_ASSIGN_OR_RAISE(non_null_bufs[i], AllocateBitmap(num_groups + padding_bits, + ctx_->memory_pool())); + if (col_metadata_[i].is_fixed_length) { + if (col_metadata_[i].fixed_length == 0) { + ARROW_ASSIGN_OR_RAISE( + fixedlen_bufs[i], + AllocateBitmap(num_groups + padding_bits, ctx_->memory_pool())); + } else { + ARROW_ASSIGN_OR_RAISE( + fixedlen_bufs[i], + AllocateBuffer( + num_groups * col_metadata_[i].fixed_length + padding_for_SIMD, + ctx_->memory_pool())); + } + } else { + ARROW_ASSIGN_OR_RAISE( + fixedlen_bufs[i], + AllocateBuffer((num_groups + 1) * sizeof(uint32_t) + padding_for_SIMD, + ctx_->memory_pool())); + } + cols_[i] = arrow::compute::KeyEncoder::KeyColumnArray( + col_metadata_[i], num_groups, non_null_bufs[i]->mutable_data(), + fixedlen_bufs[i]->mutable_data(), nullptr); + } + + for (int64_t start_row = 0; start_row < num_groups;) { + int64_t batch_size_next = + std::min(num_groups - start_row, static_cast(minibatch_size_max_)); + encoder_.DecodeFixedLengthBuffers(start_row, start_row, batch_size_next, rows_, + &cols_); + start_row += batch_size_next; + } + + if (!rows_.metadata().is_fixed_length) { + for (size_t i = 0; i < num_columns; ++i) { + if (!col_metadata_[i].is_fixed_length) { + auto varlen_size = + reinterpret_cast(fixedlen_bufs[i]->data())[num_groups]; + ARROW_ASSIGN_OR_RAISE( + varlen_bufs[i], + AllocateBuffer(varlen_size + padding_for_SIMD, ctx_->memory_pool())); + cols_[i] = arrow::compute::KeyEncoder::KeyColumnArray( + col_metadata_[i], num_groups, non_null_bufs[i]->mutable_data(), + fixedlen_bufs[i]->mutable_data(), varlen_bufs[i]->mutable_data()); + } + } + + for (int64_t start_row = 0; start_row < num_groups;) { + int64_t batch_size_next = + std::min(num_groups - start_row, static_cast(minibatch_size_max_)); + encoder_.DecodeVaryingLengthBuffers(start_row, start_row, batch_size_next, rows_, + &cols_); + start_row += batch_size_next; + } + } + + ExecBatch out({}, num_groups); + out.values.resize(num_columns); + for (size_t i = 0; i < num_columns; ++i) { + auto valid_count = arrow::internal::CountSetBits( + non_null_bufs[i]->data(), /*offset=*/0, static_cast(num_groups)); + int null_count = static_cast(num_groups) - static_cast(valid_count); + + if (col_metadata_[i].is_fixed_length) { + out.values[i] = ArrayData::Make( + key_types_[i], num_groups, + {std::move(non_null_bufs[i]), std::move(fixedlen_bufs[i])}, null_count); + } else { + out.values[i] = + ArrayData::Make(key_types_[i], num_groups, + {std::move(non_null_bufs[i]), std::move(fixedlen_bufs[i]), + std::move(varlen_bufs[i])}, + null_count); + } + } + + // Process dictionaries + for (size_t icol = 0; icol < num_columns; ++icol) { + if (key_types_[icol]->id() == Type::DICTIONARY) { + if (dictionaries_[icol]) { + out.values[icol].array()->dictionary = dictionaries_[icol]->data(); + } else { + ARROW_ASSIGN_OR_RAISE(auto dict, MakeArrayOfNull(key_types_[icol], 0)); + out.values[icol].array()->dictionary = dict->data(); + } + } + } + + return out; + } + + static constexpr int log_minibatch_max_ = 10; + static constexpr int minibatch_size_max_ = 1 << log_minibatch_max_; + static constexpr int minibatch_size_min_ = 128; + int minibatch_size_; + + ExecContext* ctx_; + arrow::util::TempVectorStack temp_stack_; + arrow::compute::KeyEncoder::KeyEncoderContext encode_ctx_; + + std::vector> key_types_; + std::vector col_metadata_; + std::vector cols_; + std::vector minibatch_hashes_; + + std::vector> dictionaries_; + + arrow::compute::KeyEncoder::KeyRowArray rows_; + arrow::compute::KeyEncoder::KeyRowArray rows_minibatch_; + arrow::compute::KeyEncoder encoder_; + arrow::compute::SwissTable map_; +}; + /// C++ abstract base class for the HashAggregateKernel interface. /// Implementations should be default constructible and perform initialization in /// Init(). @@ -884,6 +1175,9 @@ Result ResolveKernels( Result> Grouper::Make(const std::vector& descrs, ExecContext* ctx) { + if (GrouperFastImpl::CanUse(descrs)) { + return GrouperFastImpl::Make(descrs, ctx); + } return GrouperImpl::Make(descrs, ctx); } diff --git a/cpp/src/arrow/compute/kernels/hash_aggregate_test.cc b/cpp/src/arrow/compute/kernels/hash_aggregate_test.cc index 507f1716110..a0d2fd208a9 100644 --- a/cpp/src/arrow/compute/kernels/hash_aggregate_test.cc +++ b/cpp/src/arrow/compute/kernels/hash_aggregate_test.cc @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +#include + #include #include #include @@ -22,8 +24,6 @@ #include #include -#include - #include "arrow/array.h" #include "arrow/chunked_array.h" #include "arrow/compute/api_aggregate.h" @@ -182,10 +182,52 @@ struct TestGrouper { ExpectConsume(*ExecBatch::Make(key_batch), expected); } + void AssertEquivalentIds(const Datum& expected, const Datum& actual) { + auto left = expected.make_array(); + auto right = actual.make_array(); + ASSERT_EQ(left->length(), right->length()) << "#ids unequal"; + int64_t num_ids = left->length(); + auto left_data = left->data(); + auto right_data = right->data(); + const uint32_t* left_ids = + reinterpret_cast(left_data->buffers[1]->data()); + const uint32_t* right_ids = + reinterpret_cast(right_data->buffers[1]->data()); + uint32_t max_left_id = 0; + uint32_t max_right_id = 0; + for (int64_t i = 0; i < num_ids; ++i) { + if (left_ids[i] > max_left_id) { + max_left_id = left_ids[i]; + } + if (right_ids[i] > max_right_id) { + max_right_id = right_ids[i]; + } + } + std::vector right_to_left_present(max_right_id + 1, false); + std::vector left_to_right_present(max_left_id + 1, false); + std::vector right_to_left(max_right_id + 1); + std::vector left_to_right(max_left_id + 1); + for (int64_t i = 0; i < num_ids; ++i) { + uint32_t left_id = left_ids[i]; + uint32_t right_id = right_ids[i]; + if (!left_to_right_present[left_id]) { + left_to_right[left_id] = right_id; + left_to_right_present[left_id] = true; + } + if (!right_to_left_present[right_id]) { + right_to_left[right_id] = left_id; + right_to_left_present[right_id] = true; + } + ASSERT_EQ(left_id, right_to_left[right_id]); + ASSERT_EQ(right_id, left_to_right[left_id]); + } + } + void ExpectConsume(const ExecBatch& key_batch, Datum expected) { Datum ids; ConsumeAndValidate(key_batch, &ids); - AssertDatumsEqual(expected, ids, /*verbose=*/true); + AssertEquivalentIds(expected, ids); + // AssertDatumsEqual(expected, ids, /*verbose=*/true); } void ConsumeAndValidate(const ExecBatch& key_batch, Datum* ids = nullptr) { diff --git a/cpp/src/arrow/dataset/partition_test.cc b/cpp/src/arrow/dataset/partition_test.cc index 1c776f18329..7a7ffcff229 100644 --- a/cpp/src/arrow/dataset/partition_test.cc +++ b/cpp/src/arrow/dataset/partition_test.cc @@ -85,16 +85,23 @@ class TestPartitioning : public ::testing::Test { const std::vector& expected_expressions) { ASSERT_OK_AND_ASSIGN(auto partition_results, partitioning->Partition(full_batch)); std::shared_ptr rest = full_batch; + ASSERT_EQ(partition_results.batches.size(), expected_batches.size()); - auto max_index = std::min(partition_results.batches.size(), expected_batches.size()); - for (std::size_t partition_index = 0; partition_index < max_index; - partition_index++) { - std::shared_ptr actual_batch = - partition_results.batches[partition_index]; - AssertBatchesEqual(*expected_batches[partition_index], *actual_batch); - compute::Expression actual_expression = - partition_results.expressions[partition_index]; - ASSERT_EQ(expected_expressions[partition_index], actual_expression); + + for (size_t i = 0; i < partition_results.batches.size(); i++) { + std::shared_ptr actual_batch = partition_results.batches[i]; + compute::Expression actual_expression = partition_results.expressions[i]; + + auto expected_expression = std::find(expected_expressions.begin(), + expected_expressions.end(), actual_expression); + ASSERT_NE(expected_expression, expected_expressions.end()) + << "Unexpected partition expr " << actual_expression.ToString(); + + auto expected_batch = + expected_batches[expected_expression - expected_expressions.begin()]; + + SCOPED_TRACE("Batch for " + expected_expression->ToString()); + AssertBatchesEqual(*expected_batch, *actual_batch); } } From cdd053c4420799603d8652105a6fe6c7e93bd8fa Mon Sep 17 00:00:00 2001 From: Benjamin Kietzman Date: Wed, 12 May 2021 11:48:47 -0400 Subject: [PATCH 02/11] lint fixes --- .../arrow/compute/exec/key_compare_avx2.cc | 17 ++++++----- cpp/src/arrow/compute/exec/key_hash_avx2.cc | 28 +++++++++---------- cpp/src/arrow/compute/exec/key_map_avx2.cc | 8 +++--- 3 files changed, 25 insertions(+), 28 deletions(-) diff --git a/cpp/src/arrow/compute/exec/key_compare_avx2.cc b/cpp/src/arrow/compute/exec/key_compare_avx2.cc index db4d43f0e82..54e907bbc1d 100644 --- a/cpp/src/arrow/compute/exec/key_compare_avx2.cc +++ b/cpp/src/arrow/compute/exec/key_compare_avx2.cc @@ -35,13 +35,14 @@ uint32_t KeyCompare::CompareFixedLength_UpTo8B_avx2( constexpr uint32_t unroll = 4; for (uint32_t i = 0; i < num_rows / unroll; ++i) { - __m256i key_left = - _mm256_i64gather_epi64((const long long*)rows_left, offset_left, 1); + auto key_left = _mm256_i64gather_epi64(reinterpret_cast(rows_left), + offset_left, 1); offset_left = _mm256_add_epi64(offset_left, offset_left_incr); __m128i offset_right = _mm_loadu_si128(reinterpret_cast(left_to_right_map) + i); offset_right = _mm_mullo_epi32(offset_right, _mm_set1_epi32(length)); - __m256i key_right = + + auto key_right = _mm256_i32gather_epi64((const long long*)rows_right, offset_right, 1); uint32_t cmp = _mm256_movemask_epi8(_mm256_cmpeq_epi64( _mm256_and_si256(key_left, mask), _mm256_and_si256(key_right, mask))); @@ -68,12 +69,12 @@ uint32_t KeyCompare::CompareFixedLength_UpTo16B_avx2( constexpr uint32_t unroll = 2; for (uint32_t i = 0; i < num_rows / unroll; ++i) { - __m256i key_left = _mm256_inserti128_si256( + auto key_left = _mm256_inserti128_si256( _mm256_castsi128_si256( _mm_loadu_si128(reinterpret_cast(key_left_ptr))), _mm_loadu_si128(reinterpret_cast(key_left_ptr + length)), 1); key_left_ptr += length * 2; - __m256i key_right = _mm256_inserti128_si256( + auto key_right = _mm256_inserti128_si256( _mm256_castsi128_si256(_mm_loadu_si128(reinterpret_cast( rows_right + length * left_to_right_map[2 * i]))), _mm_loadu_si128(reinterpret_cast( @@ -151,10 +152,8 @@ void KeyCompare::CompareVaryingLength_avx2( uint32_t length_left = offsets_left[irow_left + 1] - begin_left; uint32_t length_right = offsets_right[irow_right + 1] - begin_right; uint32_t length = std::min(length_left, length_right); - const __m256i* key_left_ptr = - reinterpret_cast(rows_left + begin_left); - const __m256i* key_right_ptr = - reinterpret_cast(rows_right + begin_right); + auto key_left_ptr = reinterpret_cast(rows_left + begin_left); + auto key_right_ptr = reinterpret_cast(rows_right + begin_right); __m256i result_or = _mm256_setzero_si256(); int32_t i; // length can be zero diff --git a/cpp/src/arrow/compute/exec/key_hash_avx2.cc b/cpp/src/arrow/compute/exec/key_hash_avx2.cc index e081341a8df..68f1d11095d 100644 --- a/cpp/src/arrow/compute/exec/key_hash_avx2.cc +++ b/cpp/src/arrow/compute/exec/key_hash_avx2.cc @@ -28,13 +28,13 @@ void Hashing::avalanche_avx2(uint32_t num_keys, uint32_t* hashes) { constexpr int unroll = 8; ARROW_DCHECK(num_keys % unroll == 0); for (uint32_t i = 0; i < num_keys / unroll; ++i) { - __m256i hash = _mm256_loadu_si256(((const __m256i*)hashes) + i); + __m256i hash = _mm256_loadu_si256(reinterpret_cast(hashes) + i); hash = _mm256_xor_si256(hash, _mm256_srli_epi32(hash, 15)); hash = _mm256_mullo_epi32(hash, _mm256_set1_epi32(PRIME32_2)); hash = _mm256_xor_si256(hash, _mm256_srli_epi32(hash, 13)); hash = _mm256_mullo_epi32(hash, _mm256_set1_epi32(PRIME32_3)); hash = _mm256_xor_si256(hash, _mm256_srli_epi32(hash, 16)); - _mm256_storeu_si256(((__m256i*)hashes) + i, hash); + _mm256_storeu_si256((reinterpret_cast<__m256i*>(hashes)) + i, hash); } } @@ -76,11 +76,10 @@ void Hashing::helper_stripes_avx2(uint32_t num_keys, uint32_t key_length, static_cast((static_cast(PRIME32_1) + PRIME32_2) & 0xffffffff), PRIME32_2, 0, static_cast(-static_cast(PRIME32_1))); - const __m128i* key0 = reinterpret_cast(keys + key_length * 2 * i); - const __m128i* key1 = - reinterpret_cast(keys + key_length * 2 * i + key_length); + auto key0 = reinterpret_cast(keys + key_length * 2 * i); + auto key1 = reinterpret_cast(keys + key_length * 2 * i + key_length); for (uint32_t stripe = 0; stripe < num_stripes - 1; ++stripe) { - __m256i key_stripe = + auto key_stripe = _mm256_inserti128_si256(_mm256_castsi128_si256(_mm_loadu_si128(key0 + stripe)), _mm_loadu_si128(key1 + stripe), 1); acc = _mm256_add_epi32( @@ -88,7 +87,7 @@ void Hashing::helper_stripes_avx2(uint32_t num_keys, uint32_t key_length, acc = _mm256_or_si256(_mm256_slli_epi32(acc, 13), _mm256_srli_epi32(acc, 32 - 13)); acc = _mm256_mullo_epi32(acc, _mm256_set1_epi32(PRIME32_1)); } - __m256i key_stripe = _mm256_inserti128_si256( + auto key_stripe = _mm256_inserti128_si256( _mm256_castsi128_si256(_mm_loadu_si128(key0 + num_stripes - 1)), _mm_loadu_si128(key1 + num_stripes - 1), 1); key_stripe = _mm256_and_si256(key_stripe, mask_last_stripe); @@ -105,6 +104,7 @@ void Hashing::helper_tails_avx2(uint32_t num_keys, uint32_t key_length, const uint8_t* keys, uint32_t* hash) { constexpr int unroll = 8; ARROW_DCHECK(num_keys % unroll == 0); + auto keys_i64 = reinterpret_cast(keys); // Process between 1 and 8 last bytes of each key, starting from 16B boundary. // The caller needs to make sure that there are no more than 8 bytes to process after @@ -118,17 +118,15 @@ void Hashing::helper_tails_avx2(uint32_t num_keys, uint32_t key_length, __m256i offset_incr = _mm256_set1_epi32(key_length * 8); for (uint32_t i = 0; i < num_keys / unroll; ++i) { - __m256i v1 = - _mm256_i32gather_epi64((const long long*)keys, _mm256_castsi256_si128(offset), 1); - __m256i v2 = _mm256_i32gather_epi64((const long long*)keys, - _mm256_extracti128_si256(offset, 1), 1); + auto v1 = _mm256_i32gather_epi64(keys_i64, _mm256_castsi256_si128(offset), 1); + auto v2 = _mm256_i32gather_epi64(keys_i64, _mm256_extracti128_si256(offset, 1), 1); v1 = _mm256_and_si256(v1, mask); v2 = _mm256_and_si256(v2, mask); v1 = _mm256_permutevar8x32_epi32(v1, _mm256_setr_epi32(0, 2, 4, 6, 1, 3, 5, 7)); v2 = _mm256_permutevar8x32_epi32(v2, _mm256_setr_epi32(0, 2, 4, 6, 1, 3, 5, 7)); - __m256i x1 = _mm256_permute2x128_si256(v1, v2, 0x20); - __m256i x2 = _mm256_permute2x128_si256(v1, v2, 0x31); - __m256i acc = _mm256_loadu_si256(((const __m256i*)hash) + i); + auto x1 = _mm256_permute2x128_si256(v1, v2, 0x20); + auto x2 = _mm256_permute2x128_si256(v1, v2, 0x31); + __m256i acc = _mm256_loadu_si256((reinterpret_cast(hash)) + i); acc = _mm256_add_epi32(acc, _mm256_mullo_epi32(x1, _mm256_set1_epi32(PRIME32_3))); acc = _mm256_or_si256(_mm256_slli_epi32(acc, 17), _mm256_srli_epi32(acc, 32 - 17)); @@ -138,7 +136,7 @@ void Hashing::helper_tails_avx2(uint32_t num_keys, uint32_t key_length, acc = _mm256_or_si256(_mm256_slli_epi32(acc, 17), _mm256_srli_epi32(acc, 32 - 17)); acc = _mm256_mullo_epi32(acc, _mm256_set1_epi32(PRIME32_4)); - _mm256_storeu_si256(((__m256i*)hash) + i, acc); + _mm256_storeu_si256((reinterpret_cast<__m256i*>(hash)) + i, acc); offset = _mm256_add_epi32(offset, offset_incr); } diff --git a/cpp/src/arrow/compute/exec/key_map_avx2.cc b/cpp/src/arrow/compute/exec/key_map_avx2.cc index edccae70189..b540c9a5841 100644 --- a/cpp/src/arrow/compute/exec/key_map_avx2.cc +++ b/cpp/src/arrow/compute/exec/key_map_avx2.cc @@ -69,10 +69,10 @@ void SwissTable::lookup_1_avx2_x8(const int num_hashes, const uint32_t* hashes, __m256i vstamp_A = _mm256_and_si256(vstamp, _mm256_set1_epi64x(0xffffffff)); __m256i voffset_B = _mm256_srli_epi64(vblock_offset, 32); __m256i vstamp_B = _mm256_srli_epi64(vstamp, 32); - __m256i vblock_A = - _mm256_i64gather_epi64(reinterpret_cast(blocks_), voffset_A, 1); - __m256i vblock_B = - _mm256_i64gather_epi64(reinterpret_cast(blocks_), voffset_B, 1); + + auto blocks_i64 = reinterpret_cast(blocks_); + auto vblock_A = _mm256_i64gather_epi64(blocks_i64, voffset_A, 1); + auto vblock_B = _mm256_i64gather_epi64(blocks_i64, voffset_B, 1); __m256i vblock_highbits_A = _mm256_cmpeq_epi8(vblock_A, _mm256_set1_epi8(static_cast(0x80))); __m256i vblock_highbits_B = From ee87e86ab1f98c85116778d484a4849cf3452f51 Mon Sep 17 00:00:00 2001 From: michalursa Date: Thu, 13 May 2021 00:07:47 -0700 Subject: [PATCH 03/11] GrouperFastImpl: adding comments in the code --- cpp/src/arrow/compute/exec/key_map.cc | 226 ++++++++++++++++----- cpp/src/arrow/compute/exec/key_map.h | 33 +-- cpp/src/arrow/compute/exec/key_map_avx2.cc | 7 +- 3 files changed, 191 insertions(+), 75 deletions(-) diff --git a/cpp/src/arrow/compute/exec/key_map.cc b/cpp/src/arrow/compute/exec/key_map.cc index fbd68fbb95d..a31fad5662e 100644 --- a/cpp/src/arrow/compute/exec/key_map.cc +++ b/cpp/src/arrow/compute/exec/key_map.cc @@ -34,56 +34,119 @@ namespace compute { constexpr uint64_t kHighBitOfEachByte = 0x8080808080808080ULL; +// Search status bytes inside a block of 8 slots (64-bit word). +// Try to find a slot that contains a 7-bit stamp matching the one provided. +// There are three possible outcomes: +// 1. A matching slot is found. +// -> Return its index between 0 and 7 and set match found flag. +// 2. A matching slot is not found and there is an empty slot in the block. +// -> Return the index of the first empty slot and clear match found flag. +// 3. A matching slot is not found and there are no empty slots in the block. +// -> Return 8 as the output slot index and clear match found flag. +// +// Optionally an index of the first slot to start the search from can be specified. +// In this case slots before it will be ignored. // template inline void SwissTable::search_block(uint64_t block, int stamp, int start_slot, int* out_slot, int* out_match_found) { // Filled slot bytes have the highest bit set to 0 and empty slots are equal to 0x80. - // Replicate 7-bit stamp to all non-empty slots: uint64_t block_high_bits = block & kHighBitOfEachByte; + + // Replicate 7-bit stamp to all non-empty slots, leaving zeroes for empty slots. uint64_t stamp_pattern = stamp * ((block_high_bits ^ kHighBitOfEachByte) >> 7); - // If we xor this pattern with block bytes we get: + + // If we xor this pattern with block status bytes we get in individual bytes: // a) 0x00, for filled slots matching the stamp, // b) 0x00 < x < 0x80, for filled slots not matching the stamp, // c) 0x80, for empty slots. - // If we then add 0x7f to every byte, negate the result and leave only the highest bits - // in each byte, we get 0x00 for non-match slot and 0x80 for match slot. - uint64_t matches = ~((block ^ stamp_pattern) + ~kHighBitOfEachByte); + uint64_t block_xor_pattern = block ^ stamp_pattern; + + // If we then add 0x7f to every byte, we get: + // a) 0x7F + // b) 0x80 <= x < 0xFF + // c) 0xFF + uint64_t match_base = block_xor_pattern + ~kHighBitOfEachByte; + + // The highest bit now tells us if we have a match (0) or not (1). + // We will negate the bits so that match is represented by a set bit. + uint64_t matches = ~match_base; + + // Clear 7 non-relevant bits in each byte. + // Also clear bytes that correspond to slots that we were supposed to + // skip due to provided start slot index. + // Note: the highest byte corresponds to the first slot. if (use_start_slot) { matches &= kHighBitOfEachByte >> (8 * start_slot); } else { matches &= kHighBitOfEachByte; } + // We get 0 if there are no matches *out_match_found = (matches == 0 ? 0 : 1); + // Now if we or with the highest bits of the block and scan zero bits in reverse, // we get 8x slot index that we were looking for. + // This formula works in all three cases a), b) and c). *out_slot = static_cast(CountLeadingZeros(matches | block_high_bits) >> 3); } +// This call follows the call to search_block. +// The input slot index is the output returned by it, which is a value from 0 to 8, +// with 8 indicating that both: no match was found and there were no empty slots. +// +// If the slot corresponds to a non-empty slot return a group id associated with it. +// Otherwise return any group id from any of the slots or +// zero, which is the default value stored in empty slots. +// inline uint64_t SwissTable::extract_group_id(const uint8_t* block_ptr, int slot, uint64_t group_id_mask) { - // TODO: Explain why slot can be from 0 to 8 (inclusive) as input and in case of 8 we - // just need to output any valid group id, so we take the one from slot 0 in the block. - int num_groupid_bits = num_groupid_bits_from_log_blocks(log_blocks_); - int bit_offset = (slot & 7) * num_groupid_bits; + // Input slot can be equal to 8, in which case we need to output any valid group id + // value, so we take the one from slot 0 in the block. + int clamped_slot = slot & 7; + + // Group id values for all 8 slots in the block are bit-packed and follow the status + // bytes. We assume here that the number of bits is rounded up to 8, 16, 32 or 64. In + // that case we can extract group id using aligned 64-bit word access. + int num_groupid_bits = arrow::BitUtil::PopCount(group_id_mask); + ARROW_DCHECK(num_groupid_bits == 8 || num_groupid_bits == 16 || + num_groupid_bits == 32 || num_groupid_bits == 64); + + int bit_offset = clamped_slot * num_groupid_bits; const uint64_t* group_id_bytes = reinterpret_cast(block_ptr) + 1 + (bit_offset >> 6); uint64_t group_id = (*group_id_bytes >> (bit_offset & 63)) & group_id_mask; + return group_id; } +// Return global slot id (the index including the information about the block) +// where the search should continue if the first comparison fails. +// This function always follows search_block and receives the slot id returned by it. +// inline uint64_t SwissTable::next_slot_to_visit(uint64_t block_index, int slot, int match_found) { + // The result should be taken modulo the number of all slots in all blocks, + // but here we allow it to take a value one above the last slot index. + // Modulo operation is postponed to later. return block_index * 8 + slot + match_found; } +// Implements first (fast-path, optimistic) lookup. +// Searches for a match only within the start block and +// trying only the first slot with a matching stamp. +// +// Comparison callback needed for match verification is done outside of this function. +// Match bit vector filled by it only indicates finding a matching stamp in a slot. +// template void SwissTable::lookup_1(const uint16_t* selection, const int num_keys, const uint32_t* hashes, uint8_t* out_match_bitvector, uint32_t* out_groupids, uint32_t* out_slot_ids) { + // Clear the output bit vector memset(out_match_bitvector, 0, (num_keys + 7) / 8); + // Based on the size of the table, prepare bit number constants. uint32_t stamp_mask = (1 << bits_stamp_) - 1; int num_groupid_bits = num_groupid_bits_from_log_blocks(log_blocks_); uint32_t groupid_mask = (1 << num_groupid_bits) - 1; @@ -96,17 +159,22 @@ void SwissTable::lookup_1(const uint16_t* selection, const int num_keys, id = i; } - // Calculate block index and hash stamp for a byte in a block + // Extract from hash: block index and stamp // uint32_t hash = hashes[id]; uint32_t iblock = hash >> (bits_hash_ - bits_stamp_ - log_blocks_); uint32_t stamp = iblock & stamp_mask; iblock >>= bits_stamp_; + uint32_t num_block_bytes = num_groupid_bits + 8; const uint8_t* blockbase = reinterpret_cast(blocks_) + - static_cast(iblock) * (num_groupid_bits + 8); + static_cast(iblock) * num_block_bytes; uint64_t block = *reinterpret_cast(blockbase); + // Call helper functions to obtain the output triplet: + // - match (of a stamp) found flag + // - group id for key comparison + // - slot to resume search from in case of no match or false positive int match_found; int islot_in_block; search_block(block, stamp, 0, &islot_in_block, &match_found); @@ -115,12 +183,31 @@ void SwissTable::lookup_1(const uint16_t* selection, const int num_keys, uint64_t islot = next_slot_to_visit(iblock, islot_in_block, match_found); out_match_bitvector[id / 8] |= match_found << (id & 7); - out_groupids[id] = static_cast(groupid); out_slot_ids[id] = static_cast(islot); } } +// How many groups we can keep in the hash table without the need for resizing. +// When we reach this limit, we need to break processing of any further rows and resize. +// +uint32_t SwissTable::num_groups_for_resize() const { + // Resize small hash tables when 50% full (up to 12KB). + // Resize large hash tables when 75% full. + constexpr int log_blocks_small_ = 9; + uint64_t num_slots = 1ULL << (log_blocks_ + 3); + if (log_blocks_ <= log_blocks_small_) { + return num_slots / 2; + } else { + return num_slots * 3 / 4; + } +} + +uint64_t SwissTable::wrap_global_slot_id(uint64_t global_slot_id) { + uint64_t global_slot_id_mask = (1 << (log_blocks_ + 3)) - 1; + return global_slot_id & global_slot_id_mask; +} + // Run a single round of slot search - comparison / insert - filter unprocessed. // Update selection vector to reflect which items have been processed. // Ids in selection vector do not have to be sorted. @@ -128,14 +215,8 @@ void SwissTable::lookup_1(const uint16_t* selection, const int num_keys, Status SwissTable::lookup_2(const uint32_t* hashes, uint32_t* inout_num_selected, uint16_t* inout_selection, bool* out_need_resize, uint32_t* out_group_ids, uint32_t* inout_next_slot_ids) { - // How many groups we can keep in hash table without resizing. - // When we reach this limit, we need to break processing of any further rows. - // Resize small hash tables when 50% full (up to 8KB). - // Resize large hash tables when 75% full. - constexpr int log_blocks_small_ = 9; - uint32_t max_groupid = (log_blocks_ <= log_blocks_small_) ? (8 << log_blocks_) / 2 - : 3 * (8 << log_blocks_) / 4; - ARROW_DCHECK(num_inserted_ <= max_groupid); + uint32_t num_groups_limit = num_groups_for_resize(); + ARROW_DCHECK(num_inserted_ < num_groups_limit); // Temporary arrays are of limited size. // The input needs to be split into smaller portions if it exceeds that limit. @@ -162,9 +243,8 @@ Status SwissTable::lookup_2(const uint32_t* hashes, uint32_t* inout_num_selected ids[category][num_ids[category]++] = static_cast(id); }; - uint64_t slot_id_mask = (1 << (log_blocks_ + 3)) - 1; - uint64_t groupid_mask = slot_id_mask; uint64_t num_groupid_bits = num_groupid_bits_from_log_blocks(log_blocks_); + uint64_t groupid_mask = (1ULL << num_groupid_bits) - 1; constexpr uint64_t stamp_mask = 0x7f; uint64_t num_block_bytes = (8 + num_groupid_bits); @@ -172,14 +252,14 @@ Status SwissTable::lookup_2(const uint32_t* hashes, uint32_t* inout_num_selected for (num_processed = 0; // Second condition in for loop: // We need to break processing and have the caller of this function - // resize hash table if we reach max_groupid groups. + // resize hash table if we reach the limit of the number of groups present. num_processed < *inout_num_selected && - num_inserted_ + num_ids[category_inserted] < max_groupid; + num_inserted_ + num_ids[category_inserted] < num_groups_limit; ++num_processed) { // row id in original batch int id = inout_selection[num_processed]; - uint64_t slot_id = (inout_next_slot_ids[id] & slot_id_mask); + uint64_t slot_id = wrap_global_slot_id(inout_next_slot_ids[id]); uint64_t block_id = slot_id >> 3; uint32_t hash = hashes[id]; uint8_t* blockbase = blocks_ + num_block_bytes * block_id; @@ -189,15 +269,27 @@ Status SwissTable::lookup_2(const uint32_t* hashes, uint32_t* inout_num_selected bool isempty = (blockbase[7 - start_slot] == 0x80); if (isempty) { + // If we reach the empty slot we insert key for new group + blockbase[7 - start_slot] = static_cast(stamp); - int groupid_bit_offset = static_cast(start_slot * num_groupid_bits); uint32_t group_id = num_inserted_ + num_ids[category_inserted]; + int groupid_bit_offset = static_cast(start_slot * num_groupid_bits); + + // We assume here that the number of bits is rounded up to 8, 16, 32 or 64. + // In that case we can insert group id value using aligned 64-bit word access. + ARROW_DCHECK(num_groupid_bits == 8 || num_groupid_bits == 16 || + num_groupid_bits == 32 || num_groupid_bits == 64); reinterpret_cast(blockbase + 8)[groupid_bit_offset >> 6] |= (static_cast(group_id) << (groupid_bit_offset & 63)); + hashes_[slot_id] = hash; out_group_ids[id] = group_id; push_id(category_inserted, id); } else { + // We search for a slot with a matching stamp within a single block. + // We append row id to the appropriate sequence of ids based on + // whether the match has been found or not. + int new_match_found; int new_slot; search_block(block, static_cast(stamp), start_slot, &new_slot, @@ -213,21 +305,18 @@ Status SwissTable::lookup_2(const uint32_t* hashes, uint32_t* inout_num_selected } } - // Copy keys for newly inserted rows + // Copy keys for newly inserted rows using callback RETURN_NOT_OK(append_impl_(num_ids[category_inserted], ids[category_inserted])); num_inserted_ += num_ids[category_inserted]; - // Evaluate comparisons and push ids of rows that failed - // Add 3 copies of the first id, so that SIMD processing 4 elements at a time can work. - // - { - uint32_t num_not_equal; - equal_impl_(num_ids[category_cmp], ids[category_cmp], out_group_ids, &num_not_equal, - ids[category_nomatch] + num_ids[category_nomatch]); - num_ids[category_nomatch] += num_not_equal; - } + // Evaluate comparisons and append ids of rows that failed it to the non-match set. + uint32_t num_not_equal; + equal_impl_(num_ids[category_cmp], ids[category_cmp], out_group_ids, &num_not_equal, + ids[category_nomatch] + num_ids[category_nomatch]); + num_ids[category_nomatch] += num_not_equal; - // Append any unprocessed entries + // Append ids of any unprocessed entries if we aborted processing due to the need + // to resize. if (num_processed < *inout_num_selected) { memmove(ids[category_nomatch] + num_ids[category_nomatch], inout_selection + num_processed, @@ -235,18 +324,21 @@ Status SwissTable::lookup_2(const uint32_t* hashes, uint32_t* inout_num_selected num_ids[category_nomatch] += (*inout_num_selected - num_processed); } - *out_need_resize = num_processed < *inout_num_selected; + *out_need_resize = (num_inserted_ == num_groups_limit); *inout_num_selected = num_ids[category_nomatch]; return Status::OK(); } +// Use hashes and callbacks to find group ids for already existing keys and +// to insert and report newly assigned group ids for new keys. +// Status SwissTable::map(const int num_keys, const uint32_t* hashes, uint32_t* out_groupids) { // Temporary buffers have limited size. // Caller is responsible for splitting larger input arrays into smaller chunks. ARROW_DCHECK(num_keys <= (1 << log_minibatch_)); - // Allocate temporary buffers + // Allocate temporary buffers with a lifetime of this function auto match_bitvector_buf = util::TempVectorHolder(temp_stack_, num_keys); uint8_t* match_bitvector = match_bitvector_buf.mutable_data(); auto slot_ids_buf = util::TempVectorHolder(temp_stack_, num_keys); @@ -255,6 +347,9 @@ Status SwissTable::map(const int num_keys, const uint32_t* hashes, uint16_t* ids = ids_buf.mutable_data(); uint32_t num_ids; + // First-pass processing. + // Optimistically use simplified lookup involving only a start block to find + // a single group id candidate for every input. #if defined(ARROW_HAVE_AVX2) if (cpu_info_->IsSupported(arrow::internal::CpuInfo::AVX2)) { if (log_blocks_ <= 4) { @@ -276,14 +371,20 @@ Status SwissTable::map(const int num_keys, const uint32_t* hashes, int64_t num_matches = arrow::internal::CountSetBits(match_bitvector, /*offset=*/0, num_keys); - // after first pass count rows with matches and decide based on their percentage - // whether to call dense or sparse comparison function + // After the first-pass processing count rows with matches (based on stamp comparison) + // and decide based on their percentage whether to call dense or sparse comparison + // function. Dense comparison means evaluating it for all inputs, even if the matching + // stamp was not found. It may be cheaper to evaluate comparison for all inputs if the + // extra cost of filtering is higher than the wasted processing of rows with no match. + // + // Dense comparison can only be used if there is at least one inserted key, + // because otherwise there is no key to compare to. // - - // TODO: explain num_inserted_ > 0 condition below if (num_inserted_ > 0 && num_matches > 0 && num_matches > 3 * num_keys / 4) { + // Dense comparisons equal_impl_(num_keys, nullptr, out_groupids, &num_ids, ids); } else { + // Sparse comparisons that involve filtering the input set of keys auto ids_cmp_buf = util::TempVectorHolder(temp_stack_, num_keys); uint16_t* ids_cmp = ids_cmp_buf.mutable_data(); int num_ids_result; @@ -296,13 +397,20 @@ Status SwissTable::map(const int num_keys, const uint32_t* hashes, } do { + // A single round of slow-pass (robust) lookup or insert. + // A single round ends with either a single comparison verifying the match candidate + // or inserting a new key. A single round of slow-pass may return early if we reach + // the limit of the number of groups due to inserts of new keys. In that case we need + // to resize and recalculating starting global slot ids for new bigger hash table. bool out_of_capacity; RETURN_NOT_OK( lookup_2(hashes, &num_ids, ids, &out_of_capacity, out_groupids, slot_ids)); if (out_of_capacity) { RETURN_NOT_OK(grow_double()); - // Set slot_ids for selected vectors to first slot in new initial block. + // Reset start slot ids for still unprocessed input keys. + // for (uint32_t i = 0; i < num_ids; ++i) { + // First slot in the new starting block slot_ids[ids[i]] = (hashes[ids[i]] >> (bits_hash_ - log_blocks_)) * 8; } } @@ -451,19 +559,23 @@ Status SwissTable::init(const arrow::internal::CpuInfo* cpu_info, MemoryPool* po int num_groupid_bits = num_groupid_bits_from_log_blocks(log_blocks_); num_inserted_ = 0; - const uint64_t cbblocks = ((8 + num_groupid_bits) << log_blocks_) + padding_; - RETURN_NOT_OK(pool_->Allocate(cbblocks, &blocks_)); - memset(blocks_, 0, cbblocks); + const uint64_t block_bytes = 8 + num_groupid_bits; + const uint64_t slot_bytes = (block_bytes << log_blocks_) + padding_; + RETURN_NOT_OK(pool_->Allocate(slot_bytes, &blocks_)); + // Make sure group ids are initially set to zero for all slots. + memset(blocks_, 0, slot_bytes); + + // Initialize all status bytes to represent an empty slot. for (uint64_t i = 0; i < (static_cast(1) << log_blocks_); ++i) { - *reinterpret_cast(blocks_ + i * (8 + num_groupid_bits)) = - kHighBitOfEachByte; + *reinterpret_cast(blocks_ + i * block_bytes) = kHighBitOfEachByte; } - int log_slots = log_blocks_ + 3; - const uint64_t cbhashes = (sizeof(uint32_t) << log_slots) + padding_; + uint64_t num_slots = 1ULL << (log_blocks_ + 3); + const uint64_t hash_size = sizeof(uint32_t); + const uint64_t hash_bytes = hash_size * num_slots + padding_; uint8_t* hashes8; - RETURN_NOT_OK(pool_->Allocate(cbhashes, &hashes8)); + RETURN_NOT_OK(pool_->Allocate(hash_bytes, &hashes8)); hashes_ = reinterpret_cast(hashes8); return Status::OK(); @@ -472,14 +584,16 @@ Status SwissTable::init(const arrow::internal::CpuInfo* cpu_info, MemoryPool* po void SwissTable::cleanup() { if (blocks_) { int num_groupid_bits = num_groupid_bits_from_log_blocks(log_blocks_); - const uint64_t cbblocks = ((8 + num_groupid_bits) << log_blocks_) + padding_; - pool_->Free(blocks_, cbblocks); + const uint64_t block_bytes = 8 + num_groupid_bits; + const uint64_t slot_bytes = (block_bytes << log_blocks_) + padding_; + pool_->Free(blocks_, slot_bytes); blocks_ = nullptr; } if (hashes_) { uint64_t num_slots = 1ULL << (log_blocks_ + 3); - const uint64_t cbhashes = sizeof(uint32_t) * num_slots + padding_; - pool_->Free(reinterpret_cast(hashes_), cbhashes); + const uint64_t hash_size = sizeof(uint32_t); + const uint64_t hash_bytes = hash_size * num_slots + padding_; + pool_->Free(reinterpret_cast(hashes_), hash_bytes); hashes_ = nullptr; } log_blocks_ = 0; diff --git a/cpp/src/arrow/compute/exec/key_map.h b/cpp/src/arrow/compute/exec/key_map.h index c72da33b392..bcf29728d06 100644 --- a/cpp/src/arrow/compute/exec/key_map.h +++ b/cpp/src/arrow/compute/exec/key_map.h @@ -27,17 +27,6 @@ namespace arrow { namespace compute { -// -// 0 byte - 7 bucket | 1. byte - 6 bucket | ... -// --------------------------------------------------- -// | Empty bit* | Empty bit | -// --------------------------------------------------- -// | 7-bit hash | 7-bit hash | -// --------------------------------------------------- -// * Empty bucket has value 0x80. Non-empty bucket has highest bit set to 0. -// ** The order of bytes is reversed - highest byte represents 0th bucket. -// No other part of data structure uses this reversed order. -// class SwissTable { public: SwissTable() = default; @@ -96,9 +85,12 @@ class SwissTable { inline void insert(uint8_t* block_base, uint64_t slot_id, uint32_t hash, uint8_t stamp, uint32_t group_id); - // + inline uint32_t num_groups_for_resize() const; + + inline uint64_t wrap_global_slot_id(uint64_t global_slot_id); + // First hash table access - // Find first match in the first block. + // Find first match in the start block if exists. // Possible cases: // 1. Stamp match in a block // 2. No stamp match in a block, no empty buckets in a block @@ -150,9 +142,18 @@ class SwissTable { uint32_t num_inserted_ = 0; // Data for blocks. - // Each block has 8x of one byte stamp slots, followed by 8x of bit packed group ids. - // In 8B stamp word, the order of bytes is reversed. Group ids are in normal order. - // There is 64B padding at the end. + // Each block has 8 status bytes for 8 slots, followed by 8 bit packed group ids for + // these slots. In 8B status word, the order of bytes is reversed. Group ids are in + // normal order. There is 64B padding at the end. + // + // 0 byte - 7 bucket | 1. byte - 6 bucket | ... + // --------------------------------------------------- + // | Empty bit* | Empty bit | + // --------------------------------------------------- + // | 7-bit hash | 7-bit hash | + // --------------------------------------------------- + // * Empty bucket has value 0x80. Non-empty bucket has highest bit set to 0. + // uint8_t* blocks_; // Array of hashes of values inserted into slots. diff --git a/cpp/src/arrow/compute/exec/key_map_avx2.cc b/cpp/src/arrow/compute/exec/key_map_avx2.cc index b540c9a5841..f78c5e179a0 100644 --- a/cpp/src/arrow/compute/exec/key_map_avx2.cc +++ b/cpp/src/arrow/compute/exec/key_map_avx2.cc @@ -24,7 +24,7 @@ namespace compute { #if defined(ARROW_HAVE_AVX2) -// TODO: Why it is OK to round up number of rows internally: +// Why it is OK to round up number of rows internally: // All of the buffers: hashes, out_match_bitvector, out_group_ids, out_next_slot_ids // are temporary buffers of group id mapping. // Temporary buffers are buffers that live only within the boundaries of a single @@ -34,7 +34,7 @@ namespace compute { // fail, since any random data is a valid hash for the purpose of lookup. // // This is more or less translation of equivalent scalar code, adjusted for a different -// instruction set (missing lzcnt for instance). +// instruction set (e.g. missing leading zero count instruction). // void SwissTable::lookup_1_avx2_x8(const int num_hashes, const uint32_t* hashes, uint8_t* out_match_bitvector, uint32_t* out_group_ids, @@ -219,7 +219,8 @@ void SwissTable::lookup_1_avx2_x32(const int num_hashes, const uint32_t* hashes, uint32_t* out_next_slot_ids) { constexpr int unroll = 32; - // TODO: consider adding the support for 5 + // There is a limit on the number of input blocks, + // because we want to store all their data in a set of AVX2 registers. ARROW_DCHECK(log_blocks_ <= 4); // Remember that block bytes and group id bytes are in opposite orders in memory of hash From 6fb7837ae7a515d5220c7d1481e69c682c39cd6c Mon Sep 17 00:00:00 2001 From: michalursa Date: Tue, 18 May 2021 00:01:04 -0700 Subject: [PATCH 04/11] GrouperFastImpl: replacing cpu_info with hardware_flags --- cpp/src/arrow/compute/exec/key_compare.cc | 6 ++-- cpp/src/arrow/compute/exec/key_encode.cc | 10 +++--- cpp/src/arrow/compute/exec/key_encode.h | 4 +-- cpp/src/arrow/compute/exec/key_hash.cc | 26 +++++++------- cpp/src/arrow/compute/exec/key_hash.h | 10 +++--- cpp/src/arrow/compute/exec/key_map.cc | 8 ++--- cpp/src/arrow/compute/exec/key_map.h | 5 ++- cpp/src/arrow/compute/exec/util.cc | 34 +++++++++---------- cpp/src/arrow/compute/exec/util.h | 14 ++++---- .../arrow/compute/kernels/hash_aggregate.cc | 8 ++--- 10 files changed, 62 insertions(+), 63 deletions(-) diff --git a/cpp/src/arrow/compute/exec/key_compare.cc b/cpp/src/arrow/compute/exec/key_compare.cc index 381f25cf36a..feb5c450c94 100644 --- a/cpp/src/arrow/compute/exec/key_compare.cc +++ b/cpp/src/arrow/compute/exec/key_compare.cc @@ -71,17 +71,17 @@ void KeyCompare::CompareRows(uint32_t num_rows_to_compare, rows_left.null_masks(), rows_right.null_masks()); } - util::BitUtil::bytes_to_bits(ctx->cpu_info, num_rows_to_compare, match_bytevector, + util::BitUtil::bytes_to_bits(ctx->hardware_flags, num_rows_to_compare, match_bytevector, match_bitvector); if (sel_left_maybe_null) { int out_num_rows_int; - util::BitUtil::bits_filter_indexes(0, ctx->cpu_info, num_rows_to_compare, + util::BitUtil::bits_filter_indexes(0, ctx->hardware_flags, num_rows_to_compare, match_bitvector, sel_left_maybe_null, &out_num_rows_int, out_sel_left_maybe_same); *out_num_rows = out_num_rows_int; } else { int out_num_rows_int; - util::BitUtil::bits_to_indexes(0, ctx->cpu_info, num_rows_to_compare, match_bitvector, + util::BitUtil::bits_to_indexes(0, ctx->hardware_flags, num_rows_to_compare, match_bitvector, &out_num_rows_int, out_sel_left_maybe_same); *out_num_rows = out_num_rows_int; } diff --git a/cpp/src/arrow/compute/exec/key_encode.cc b/cpp/src/arrow/compute/exec/key_encode.cc index eb1424a126d..0c5f27c51c1 100644 --- a/cpp/src/arrow/compute/exec/key_encode.cc +++ b/cpp/src/arrow/compute/exec/key_encode.cc @@ -266,7 +266,7 @@ bool KeyEncoder::KeyRowArray::has_any_nulls(const KeyEncoderContext* ctx) const if (num_rows_for_has_any_nulls_ < num_rows_) { auto size_per_row = metadata().null_masks_bytes_per_row; has_any_nulls_ = !util::BitUtil::are_all_bytes_zero( - ctx->cpu_info, null_masks() + size_per_row * num_rows_for_has_any_nulls_, + ctx->hardware_flags, null_masks() + size_per_row * num_rows_for_has_any_nulls_, static_cast(size_per_row * (num_rows_ - num_rows_for_has_any_nulls_))); num_rows_for_has_any_nulls_ = num_rows_; } @@ -360,7 +360,7 @@ void KeyEncoder::TransformBoolean::PreEncode(const KeyColumnArray& input, constexpr int buffer_index = 1; ARROW_DCHECK(input.data(buffer_index) != nullptr); ARROW_DCHECK(output->mutable_data(buffer_index) != nullptr); - util::BitUtil::bits_to_bytes(ctx->cpu_info, static_cast(input.length()), + util::BitUtil::bits_to_bytes(ctx->hardware_flags, static_cast(input.length()), input.data(buffer_index), output->mutable_data(buffer_index)); } @@ -377,7 +377,7 @@ void KeyEncoder::TransformBoolean::PostDecode(const KeyColumnArray& input, ARROW_DCHECK(input.data(buffer_index) != nullptr); ARROW_DCHECK(output->mutable_data(buffer_index) != nullptr); - util::BitUtil::bytes_to_bits(ctx->cpu_info, static_cast(input.length()), + util::BitUtil::bytes_to_bits(ctx->hardware_flags, static_cast(input.length()), input.data(buffer_index), output->mutable_data(buffer_index)); } @@ -745,7 +745,7 @@ void KeyEncoder::EncoderBinary::ColumnMemsetNullsImp( // Bit vector to index vector of null positions int num_selected; - util::BitUtil::bits_to_indexes(0, ctx->cpu_info, static_cast(col.length()), + util::BitUtil::bits_to_indexes(0, ctx->hardware_flags, static_cast(col.length()), col.data(0), &num_selected, temp_vector); for (int i = 0; i < num_selected; ++i) { @@ -1255,7 +1255,7 @@ void KeyEncoder::EncoderNulls::Encode(KeyRowArray* rows, } int num_selected; util::BitUtil::bits_to_indexes( - 0, ctx->cpu_info, num_rows, non_nulls, &num_selected, + 0, ctx->hardware_flags, num_rows, non_nulls, &num_selected, reinterpret_cast(temp_vector_16bit->mutable_data(1))); for (int i = 0; i < num_selected; ++i) { uint16_t row_id = reinterpret_cast(temp_vector_16bit->data(1))[i]; diff --git a/cpp/src/arrow/compute/exec/key_encode.h b/cpp/src/arrow/compute/exec/key_encode.h index 452730deb19..68fcac041cc 100644 --- a/cpp/src/arrow/compute/exec/key_encode.h +++ b/cpp/src/arrow/compute/exec/key_encode.h @@ -43,9 +43,9 @@ class KeyEncoder { public: struct KeyEncoderContext { bool has_avx2() const { - return cpu_info->IsSupported(arrow::internal::CpuInfo::AVX2); + return (hardware_flags & arrow::internal::CpuInfo::AVX2) > 0; } - const arrow::internal::CpuInfo* cpu_info; + int hardware_flags; util::TempVectorStack* stack; }; diff --git a/cpp/src/arrow/compute/exec/key_hash.cc b/cpp/src/arrow/compute/exec/key_hash.cc index b91ef0eb763..50043d51cc6 100644 --- a/cpp/src/arrow/compute/exec/key_hash.cc +++ b/cpp/src/arrow/compute/exec/key_hash.cc @@ -44,11 +44,11 @@ inline uint32_t Hashing::avalanche_helper(uint32_t acc) { return acc; } -void Hashing::avalanche(const arrow::internal::CpuInfo* cpu_info, uint32_t num_keys, +void Hashing::avalanche(int64_t hardware_flags, uint32_t num_keys, uint32_t* hashes) { uint32_t processed = 0; #if defined(ARROW_HAVE_AVX2) - if (cpu_info->IsSupported(arrow::internal::CpuInfo::AVX2)) { + if (hardware_flags & arrow::internal::CpuInfo::AVX2) { int tail = num_keys % 8; avalanche_avx2(num_keys - tail, hashes); processed = num_keys - tail; @@ -100,11 +100,11 @@ inline void Hashing::helper_stripe(uint32_t offset, uint64_t mask_hi, const uint acc4 = ROTL(acc4, 13) * PRIME32_1; } -void Hashing::helper_stripes(const arrow::internal::CpuInfo* cpu_info, uint32_t num_keys, +void Hashing::helper_stripes(int64_t hardware_flags, uint32_t num_keys, uint32_t key_length, const uint8_t* keys, uint32_t* hash) { uint32_t processed = 0; #if defined(ARROW_HAVE_AVX2) - if (cpu_info->IsSupported(arrow::internal::CpuInfo::AVX2)) { + if (hardware_flags & arrow::internal::CpuInfo::AVX2) { int tail = num_keys % 2; helper_stripes_avx2(num_keys - tail, key_length, keys, hash); processed = num_keys - tail; @@ -149,11 +149,11 @@ inline uint32_t Hashing::helper_tail(uint32_t offset, uint64_t mask, const uint8 return acc; } -void Hashing::helper_tails(const arrow::internal::CpuInfo* cpu_info, uint32_t num_keys, +void Hashing::helper_tails(int64_t hardware_flags, uint32_t num_keys, uint32_t key_length, const uint8_t* keys, uint32_t* hash) { uint32_t processed = 0; #if defined(ARROW_HAVE_AVX2) - if (cpu_info->IsSupported(arrow::internal::CpuInfo::AVX2)) { + if (hardware_flags & arrow::internal::CpuInfo::AVX2) { int tail = num_keys % 8; helper_tails_avx2(num_keys - tail, key_length, keys, hash); processed = num_keys - tail; @@ -168,7 +168,7 @@ void Hashing::helper_tails(const arrow::internal::CpuInfo* cpu_info, uint32_t nu } } -void Hashing::hash_fixed(const arrow::internal::CpuInfo* cpu_info, uint32_t num_keys, +void Hashing::hash_fixed(int64_t hardware_flags, uint32_t num_keys, uint32_t length_key, const uint8_t* keys, uint32_t* hashes) { ARROW_DCHECK(length_key > 0); @@ -176,11 +176,11 @@ void Hashing::hash_fixed(const arrow::internal::CpuInfo* cpu_info, uint32_t num_ helper_8B(length_key, num_keys, keys, hashes); return; } - helper_stripes(cpu_info, num_keys, length_key, keys, hashes); + helper_stripes(hardware_flags, num_keys, length_key, keys, hashes); if ((length_key % 16) > 0 && (length_key % 16) <= 8) { - helper_tails(cpu_info, num_keys, length_key, keys, hashes); + helper_tails(hardware_flags, num_keys, length_key, keys, hashes); } - avalanche(cpu_info, num_keys, hashes); + avalanche(hardware_flags, num_keys, hashes); } void Hashing::hash_varlen_helper(uint32_t length, const uint8_t* key, uint32_t* acc) { @@ -216,12 +216,12 @@ void Hashing::hash_varlen_helper(uint32_t length, const uint8_t* key, uint32_t* } } -void Hashing::hash_varlen(const arrow::internal::CpuInfo* cpu_info, uint32_t num_rows, +void Hashing::hash_varlen(int64_t hardware_flags, uint32_t num_rows, const uint32_t* offsets, const uint8_t* concatenated_keys, uint32_t* temp_buffer, // Needs to hold 4 x 32-bit per row uint32_t* hashes) { #if defined(ARROW_HAVE_AVX2) - if (cpu_info->IsSupported(arrow::internal::CpuInfo::AVX2)) { + if (hardware_flags & arrow::internal::CpuInfo::AVX2) { hash_varlen_avx2(num_rows, offsets, concatenated_keys, temp_buffer, hashes); } else { #endif @@ -237,7 +237,7 @@ void Hashing::hash_varlen(const arrow::internal::CpuInfo* cpu_info, uint32_t num hash_varlen_helper(length, concatenated_keys + offsets[i], acc); hashes[i] = combine_accumulators(acc[0], acc[1], acc[2], acc[3]); } - avalanche(cpu_info, num_rows, hashes); + avalanche(hardware_flags, num_rows, hashes); #if defined(ARROW_HAVE_AVX2) } #endif diff --git a/cpp/src/arrow/compute/exec/key_hash.h b/cpp/src/arrow/compute/exec/key_hash.h index 2918f307af3..4c02ed6d174 100644 --- a/cpp/src/arrow/compute/exec/key_hash.h +++ b/cpp/src/arrow/compute/exec/key_hash.h @@ -31,10 +31,10 @@ namespace compute { // class Hashing { public: - static void hash_fixed(const arrow::internal::CpuInfo* cpu_info, uint32_t num_keys, + static void hash_fixed(int64_t hardware_flags, uint32_t num_keys, uint32_t length_key, const uint8_t* keys, uint32_t* hashes); - static void hash_varlen(const arrow::internal::CpuInfo* cpu_info, uint32_t num_rows, + static void hash_varlen(int64_t hardware_flags, uint32_t num_rows, const uint32_t* offsets, const uint8_t* concatenated_keys, uint32_t* temp_buffer, // Needs to hold 4 x 32-bit per row uint32_t* hashes); @@ -51,7 +51,7 @@ class Hashing { #if defined(ARROW_HAVE_AVX2) static void avalanche_avx2(uint32_t num_keys, uint32_t* hashes); #endif - static void avalanche(const arrow::internal::CpuInfo* cpu_info, uint32_t num_keys, + static void avalanche(int64_t hardware_flags, uint32_t num_keys, uint32_t* hashes); // Accumulator combine @@ -75,9 +75,9 @@ class Hashing { static void helper_tails_avx2(uint32_t num_keys, uint32_t key_length, const uint8_t* keys, uint32_t* hash); #endif - static void helper_stripes(const arrow::internal::CpuInfo* cpu_info, uint32_t num_keys, + static void helper_stripes(int64_t hardware_flags, uint32_t num_keys, uint32_t key_length, const uint8_t* keys, uint32_t* hash); - static void helper_tails(const arrow::internal::CpuInfo* cpu_info, uint32_t num_keys, + static void helper_tails(int64_t hardware_flags, uint32_t num_keys, uint32_t key_length, const uint8_t* keys, uint32_t* hash); static void hash_varlen_helper(uint32_t length, const uint8_t* key, uint32_t* acc); diff --git a/cpp/src/arrow/compute/exec/key_map.cc b/cpp/src/arrow/compute/exec/key_map.cc index a31fad5662e..d390c3cfd4b 100644 --- a/cpp/src/arrow/compute/exec/key_map.cc +++ b/cpp/src/arrow/compute/exec/key_map.cc @@ -351,7 +351,7 @@ Status SwissTable::map(const int num_keys, const uint32_t* hashes, // Optimistically use simplified lookup involving only a start block to find // a single group id candidate for every input. #if defined(ARROW_HAVE_AVX2) - if (cpu_info_->IsSupported(arrow::internal::CpuInfo::AVX2)) { + if (hardware_flags_ & arrow::internal::CpuInfo::AVX2) { if (log_blocks_ <= 4) { int tail = num_keys % 32; int delta = num_keys - tail; @@ -388,7 +388,7 @@ Status SwissTable::map(const int num_keys, const uint32_t* hashes, auto ids_cmp_buf = util::TempVectorHolder(temp_stack_, num_keys); uint16_t* ids_cmp = ids_cmp_buf.mutable_data(); int num_ids_result; - util::BitUtil::bits_split_indexes(cpu_info_, num_keys, match_bitvector, + util::BitUtil::bits_split_indexes(hardware_flags_, num_keys, match_bitvector, &num_ids_result, ids, ids_cmp); num_ids = num_ids_result; uint32_t num_not_equal; @@ -545,10 +545,10 @@ Status SwissTable::grow_double() { return Status::OK(); } -Status SwissTable::init(const arrow::internal::CpuInfo* cpu_info, MemoryPool* pool, +Status SwissTable::init(int64_t hardware_flags, MemoryPool* pool, util::TempVectorStack* temp_stack, int log_minibatch, EqualImpl equal_impl, AppendImpl append_impl) { - cpu_info_ = cpu_info; + hardware_flags_ = hardware_flags; pool_ = pool; temp_stack_ = temp_stack; log_minibatch_ = log_minibatch; diff --git a/cpp/src/arrow/compute/exec/key_map.h b/cpp/src/arrow/compute/exec/key_map.h index bcf29728d06..d3ebca362c7 100644 --- a/cpp/src/arrow/compute/exec/key_map.h +++ b/cpp/src/arrow/compute/exec/key_map.h @@ -38,7 +38,7 @@ class SwissTable { uint16_t* out_selection_mismatch)>; using AppendImpl = std::function; - Status init(const arrow::internal::CpuInfo* cpu_info, MemoryPool* pool, + Status init(int64_t hardware_flags, MemoryPool* pool, util::TempVectorStack* temp_stack, int log_minibatch, EqualImpl equal_impl, AppendImpl append_impl); void cleanup(); @@ -161,13 +161,12 @@ class SwissTable { // There is 64B padding at the end. uint32_t* hashes_; + int64_t hardware_flags_; MemoryPool* pool_; util::TempVectorStack* temp_stack_; EqualImpl equal_impl_; AppendImpl append_impl_; - - const arrow::internal::CpuInfo* cpu_info_; }; } // namespace compute diff --git a/cpp/src/arrow/compute/exec/util.cc b/cpp/src/arrow/compute/exec/util.cc index 0100367f3f5..71e6a9d334a 100644 --- a/cpp/src/arrow/compute/exec/util.cc +++ b/cpp/src/arrow/compute/exec/util.cc @@ -48,7 +48,7 @@ inline void BitUtil::bits_filter_indexes_helper(uint64_t word, } template -void BitUtil::bits_to_indexes_internal(const arrow::internal::CpuInfo* cpu_info, +void BitUtil::bits_to_indexes_internal(int64_t hardware_flags, const int num_bits, const uint8_t* bits, const uint16_t* input_indexes, int* num_indexes, uint16_t* indexes) { @@ -56,7 +56,7 @@ void BitUtil::bits_to_indexes_internal(const arrow::internal::CpuInfo* cpu_info, constexpr int unroll = 64; int tail = num_bits % unroll; #if defined(ARROW_HAVE_AVX2) - if (cpu_info->IsSupported(arrow::internal::CpuInfo::AVX2)) { + if (hardware_flags & arrow::internal::CpuInfo::AVX2) { if (filter_input_indexes) { bits_filter_indexes_avx2(bit_to_search, num_bits - tail, bits, input_indexes, num_indexes, indexes); @@ -96,41 +96,41 @@ void BitUtil::bits_to_indexes_internal(const arrow::internal::CpuInfo* cpu_info, } } -void BitUtil::bits_to_indexes(int bit_to_search, const arrow::internal::CpuInfo* cpu_info, +void BitUtil::bits_to_indexes(int bit_to_search, int64_t hardware_flags, const int num_bits, const uint8_t* bits, int* num_indexes, uint16_t* indexes) { if (bit_to_search == 0) { - bits_to_indexes_internal<0, false>(cpu_info, num_bits, bits, nullptr, num_indexes, + bits_to_indexes_internal<0, false>(hardware_flags, num_bits, bits, nullptr, num_indexes, indexes); } else { ARROW_DCHECK(bit_to_search == 1); - bits_to_indexes_internal<1, false>(cpu_info, num_bits, bits, nullptr, num_indexes, + bits_to_indexes_internal<1, false>(hardware_flags, num_bits, bits, nullptr, num_indexes, indexes); } } void BitUtil::bits_filter_indexes(int bit_to_search, - const arrow::internal::CpuInfo* cpu_info, + int64_t hardware_flags, const int num_bits, const uint8_t* bits, const uint16_t* input_indexes, int* num_indexes, uint16_t* indexes) { if (bit_to_search == 0) { - bits_to_indexes_internal<0, true>(cpu_info, num_bits, bits, input_indexes, + bits_to_indexes_internal<0, true>(hardware_flags, num_bits, bits, input_indexes, num_indexes, indexes); } else { ARROW_DCHECK(bit_to_search == 1); - bits_to_indexes_internal<1, true>(cpu_info, num_bits, bits, input_indexes, + bits_to_indexes_internal<1, true>(hardware_flags, num_bits, bits, input_indexes, num_indexes, indexes); } } -void BitUtil::bits_split_indexes(const arrow::internal::CpuInfo* cpu_info, +void BitUtil::bits_split_indexes(int64_t hardware_flags, const int num_bits, const uint8_t* bits, int* num_indexes_bit0, uint16_t* indexes_bit0, uint16_t* indexes_bit1) { - bits_to_indexes(0, cpu_info, num_bits, bits, num_indexes_bit0, indexes_bit0); + bits_to_indexes(0, hardware_flags, num_bits, bits, num_indexes_bit0, indexes_bit0); int num_indexes_bit1; - bits_to_indexes(1, cpu_info, num_bits, bits, &num_indexes_bit1, indexes_bit1); + bits_to_indexes(1, hardware_flags, num_bits, bits, &num_indexes_bit1, indexes_bit1); } void BitUtil::bits_to_bytes_internal(const int num_bits, const uint8_t* bits, @@ -165,11 +165,11 @@ void BitUtil::bytes_to_bits_internal(const int num_bits, const uint8_t* bytes, } } -void BitUtil::bits_to_bytes(const arrow::internal::CpuInfo* cpu_info, const int num_bits, +void BitUtil::bits_to_bytes(int64_t hardware_flags, const int num_bits, const uint8_t* bits, uint8_t* bytes) { int num_processed = 0; #if defined(ARROW_HAVE_AVX2) - if (cpu_info->IsSupported(arrow::internal::CpuInfo::AVX2)) { + if (hardware_flags & arrow::internal::CpuInfo::AVX2) { // The function call below processes whole 32 bit chunks together. num_processed = num_bits - (num_bits % 32); bits_to_bytes_avx2(num_processed, bits, bytes); @@ -191,11 +191,11 @@ void BitUtil::bits_to_bytes(const arrow::internal::CpuInfo* cpu_info, const int } } -void BitUtil::bytes_to_bits(const arrow::internal::CpuInfo* cpu_info, const int num_bits, +void BitUtil::bytes_to_bits(int64_t hardware_flags, const int num_bits, const uint8_t* bytes, uint8_t* bits) { int num_processed = 0; #if defined(ARROW_HAVE_AVX2) - if (cpu_info->IsSupported(arrow::internal::CpuInfo::AVX2)) { + if (hardware_flags & arrow::internal::CpuInfo::AVX2) { // The function call below processes whole 32 bit chunks together. num_processed = num_bits - (num_bits % 32); bytes_to_bits_avx2(num_processed, bytes, bits); @@ -213,10 +213,10 @@ void BitUtil::bytes_to_bits(const arrow::internal::CpuInfo* cpu_info, const int } } -bool BitUtil::are_all_bytes_zero(const arrow::internal::CpuInfo* cpu_info, +bool BitUtil::are_all_bytes_zero(int64_t hardware_flags, const uint8_t* bytes, uint32_t num_bytes) { #if defined(ARROW_HAVE_AVX2) - if (cpu_info->IsSupported(arrow::internal::CpuInfo::AVX2)) { + if (hardware_flags & arrow::internal::CpuInfo::AVX2) { return are_all_bytes_zero_avx2(bytes, num_bytes); } #endif diff --git a/cpp/src/arrow/compute/exec/util.h b/cpp/src/arrow/compute/exec/util.h index 62f1bc70ab4..81d9b6e806f 100644 --- a/cpp/src/arrow/compute/exec/util.h +++ b/cpp/src/arrow/compute/exec/util.h @@ -102,30 +102,30 @@ class TempVectorHolder { class BitUtil { public: - static void bits_to_indexes(int bit_to_search, const arrow::internal::CpuInfo* cpu_info, + static void bits_to_indexes(int bit_to_search, int64_t hardware_flags, const int num_bits, const uint8_t* bits, int* num_indexes, uint16_t* indexes); static void bits_filter_indexes(int bit_to_search, - const arrow::internal::CpuInfo* cpu_info, + int64_t hardware_flags, const int num_bits, const uint8_t* bits, const uint16_t* input_indexes, int* num_indexes, uint16_t* indexes); // Input and output indexes may be pointing to the same data (in-place filtering). - static void bits_split_indexes(const arrow::internal::CpuInfo* cpu_info, + static void bits_split_indexes(int64_t hardware_flags, const int num_bits, const uint8_t* bits, int* num_indexes_bit0, uint16_t* indexes_bit0, uint16_t* indexes_bit1); // Bit 1 is replaced with byte 0xFF. - static void bits_to_bytes(const arrow::internal::CpuInfo* cpu_info, const int num_bits, + static void bits_to_bytes(int64_t hardware_flags, const int num_bits, const uint8_t* bits, uint8_t* bytes); // Return highest bit of each byte. - static void bytes_to_bits(const arrow::internal::CpuInfo* cpu_info, const int num_bits, + static void bytes_to_bits(int64_t hardware_flags, const int num_bits, const uint8_t* bytes, uint8_t* bits); - static bool are_all_bytes_zero(const arrow::internal::CpuInfo* cpu_info, + static bool are_all_bytes_zero(int64_t hardware_flags, const uint8_t* bytes, uint32_t num_bytes); private: @@ -135,7 +135,7 @@ class BitUtil { const uint16_t* input_indexes, int* num_indexes, uint16_t* indexes); template - static void bits_to_indexes_internal(const arrow::internal::CpuInfo* cpu_info, + static void bits_to_indexes_internal(int64_t hardware_flags, const int num_bits, const uint8_t* bits, const uint16_t* input_indexes, int* num_indexes, uint16_t* indexes); diff --git a/cpp/src/arrow/compute/kernels/hash_aggregate.cc b/cpp/src/arrow/compute/kernels/hash_aggregate.cc index 2ac2c142450..9683c779d14 100644 --- a/cpp/src/arrow/compute/kernels/hash_aggregate.cc +++ b/cpp/src/arrow/compute/kernels/hash_aggregate.cc @@ -458,7 +458,7 @@ struct GrouperFastImpl : Grouper { impl->ctx_ = ctx; RETURN_NOT_OK(impl->temp_stack_.Init(ctx->memory_pool(), 64 * minibatch_size_max_)); - impl->encode_ctx_.cpu_info = arrow::internal::CpuInfo::GetInstance(); + impl->encode_ctx_.hardware_flags = arrow::internal::CpuInfo::GetInstance()->hardware_flags(); impl->encode_ctx_.stack = &impl->temp_stack_; auto num_columns = keys.size(); @@ -508,7 +508,7 @@ struct GrouperFastImpl : Grouper { return impl_ptr->rows_.AppendSelectionFrom(impl_ptr->rows_minibatch_, num_keys, selection); }; - RETURN_NOT_OK(impl->map_.init(impl->encode_ctx_.cpu_info, ctx->memory_pool(), + RETURN_NOT_OK(impl->map_.init(impl->encode_ctx_.hardware_flags, ctx->memory_pool(), impl->encode_ctx_.stack, impl->log_minibatch_max_, equal_func, append_func)); impl->cols_.resize(num_columns); @@ -575,13 +575,13 @@ struct GrouperFastImpl : Grouper { // Compute hash if (encoder_.row_metadata().is_fixed_length) { - Hashing::hash_fixed(encode_ctx_.cpu_info, batch_size_next, + Hashing::hash_fixed(encode_ctx_.hardware_flags, batch_size_next, encoder_.row_metadata().fixed_length, rows_minibatch_.data(1), minibatch_hashes_.data()); } else { auto hash_temp_buf = util::TempVectorHolder(&temp_stack_, 4 * batch_size_next); - Hashing::hash_varlen(encode_ctx_.cpu_info, batch_size_next, + Hashing::hash_varlen(encode_ctx_.hardware_flags, batch_size_next, rows_minibatch_.offsets(), rows_minibatch_.data(2), hash_temp_buf.mutable_data(), minibatch_hashes_.data()); } From a65697ce8e026d5294f7c1dd435a56ebb6e5cbdb Mon Sep 17 00:00:00 2001 From: michalursa Date: Tue, 18 May 2021 14:46:01 -0700 Subject: [PATCH 05/11] GrouperFastImpl: fixing build errors --- cpp/src/arrow/compute/exec/key_compare.cc | 5 ++-- .../arrow/compute/exec/key_compare_avx2.cc | 4 +-- cpp/src/arrow/compute/exec/key_encode.h | 2 +- cpp/src/arrow/compute/exec/key_hash.cc | 21 ++++---------- cpp/src/arrow/compute/exec/key_hash.h | 13 +++++---- cpp/src/arrow/compute/exec/key_map.cc | 6 ++-- cpp/src/arrow/compute/exec/key_map.h | 7 ++--- cpp/src/arrow/compute/exec/util.cc | 29 +++++++++---------- cpp/src/arrow/compute/exec/util.h | 21 ++++++-------- .../arrow/compute/kernels/hash_aggregate.cc | 3 +- 10 files changed, 49 insertions(+), 62 deletions(-) diff --git a/cpp/src/arrow/compute/exec/key_compare.cc b/cpp/src/arrow/compute/exec/key_compare.cc index feb5c450c94..f8d74859b01 100644 --- a/cpp/src/arrow/compute/exec/key_compare.cc +++ b/cpp/src/arrow/compute/exec/key_compare.cc @@ -81,8 +81,9 @@ void KeyCompare::CompareRows(uint32_t num_rows_to_compare, *out_num_rows = out_num_rows_int; } else { int out_num_rows_int; - util::BitUtil::bits_to_indexes(0, ctx->hardware_flags, num_rows_to_compare, match_bitvector, - &out_num_rows_int, out_sel_left_maybe_same); + util::BitUtil::bits_to_indexes(0, ctx->hardware_flags, num_rows_to_compare, + match_bitvector, &out_num_rows_int, + out_sel_left_maybe_same); *out_num_rows = out_num_rows_int; } } diff --git a/cpp/src/arrow/compute/exec/key_compare_avx2.cc b/cpp/src/arrow/compute/exec/key_compare_avx2.cc index 54e907bbc1d..28e198a79be 100644 --- a/cpp/src/arrow/compute/exec/key_compare_avx2.cc +++ b/cpp/src/arrow/compute/exec/key_compare_avx2.cc @@ -42,8 +42,8 @@ uint32_t KeyCompare::CompareFixedLength_UpTo8B_avx2( _mm_loadu_si128(reinterpret_cast(left_to_right_map) + i); offset_right = _mm_mullo_epi32(offset_right, _mm_set1_epi32(length)); - auto key_right = - _mm256_i32gather_epi64((const long long*)rows_right, offset_right, 1); + auto key_right = _mm256_i32gather_epi64(reinterpret_cast(rows_right), + offset_right, 1); uint32_t cmp = _mm256_movemask_epi8(_mm256_cmpeq_epi64( _mm256_and_si256(key_left, mask), _mm256_and_si256(key_right, mask))); reinterpret_cast(match_bytevector)[i] &= cmp; diff --git a/cpp/src/arrow/compute/exec/key_encode.h b/cpp/src/arrow/compute/exec/key_encode.h index 68fcac041cc..3f5ef365a08 100644 --- a/cpp/src/arrow/compute/exec/key_encode.h +++ b/cpp/src/arrow/compute/exec/key_encode.h @@ -45,7 +45,7 @@ class KeyEncoder { bool has_avx2() const { return (hardware_flags & arrow::internal::CpuInfo::AVX2) > 0; } - int hardware_flags; + int64_t hardware_flags; util::TempVectorStack* stack; }; diff --git a/cpp/src/arrow/compute/exec/key_hash.cc b/cpp/src/arrow/compute/exec/key_hash.cc index 50043d51cc6..081411e708e 100644 --- a/cpp/src/arrow/compute/exec/key_hash.cc +++ b/cpp/src/arrow/compute/exec/key_hash.cc @@ -19,19 +19,11 @@ #include +#include #include #include "arrow/compute/exec/util.h" -#ifdef _MSC_VER -#include -#else -#include -#endif -#include - -#include - namespace arrow { namespace compute { @@ -44,8 +36,7 @@ inline uint32_t Hashing::avalanche_helper(uint32_t acc) { return acc; } -void Hashing::avalanche(int64_t hardware_flags, uint32_t num_keys, - uint32_t* hashes) { +void Hashing::avalanche(int64_t hardware_flags, uint32_t num_keys, uint32_t* hashes) { uint32_t processed = 0; #if defined(ARROW_HAVE_AVX2) if (hardware_flags & arrow::internal::CpuInfo::AVX2) { @@ -149,8 +140,8 @@ inline uint32_t Hashing::helper_tail(uint32_t offset, uint64_t mask, const uint8 return acc; } -void Hashing::helper_tails(int64_t hardware_flags, uint32_t num_keys, - uint32_t key_length, const uint8_t* keys, uint32_t* hash) { +void Hashing::helper_tails(int64_t hardware_flags, uint32_t num_keys, uint32_t key_length, + const uint8_t* keys, uint32_t* hash) { uint32_t processed = 0; #if defined(ARROW_HAVE_AVX2) if (hardware_flags & arrow::internal::CpuInfo::AVX2) { @@ -168,8 +159,8 @@ void Hashing::helper_tails(int64_t hardware_flags, uint32_t num_keys, } } -void Hashing::hash_fixed(int64_t hardware_flags, uint32_t num_keys, - uint32_t length_key, const uint8_t* keys, uint32_t* hashes) { +void Hashing::hash_fixed(int64_t hardware_flags, uint32_t num_keys, uint32_t length_key, + const uint8_t* keys, uint32_t* hashes) { ARROW_DCHECK(length_key > 0); if (length_key <= 8) { diff --git a/cpp/src/arrow/compute/exec/key_hash.h b/cpp/src/arrow/compute/exec/key_hash.h index 4c02ed6d174..7f8ab5185cc 100644 --- a/cpp/src/arrow/compute/exec/key_hash.h +++ b/cpp/src/arrow/compute/exec/key_hash.h @@ -17,7 +17,9 @@ #pragma once +#if defined(ARROW_HAVE_AVX2) #include +#endif #include @@ -31,8 +33,8 @@ namespace compute { // class Hashing { public: - static void hash_fixed(int64_t hardware_flags, uint32_t num_keys, - uint32_t length_key, const uint8_t* keys, uint32_t* hashes); + static void hash_fixed(int64_t hardware_flags, uint32_t num_keys, uint32_t length_key, + const uint8_t* keys, uint32_t* hashes); static void hash_varlen(int64_t hardware_flags, uint32_t num_rows, const uint32_t* offsets, const uint8_t* concatenated_keys, @@ -51,8 +53,7 @@ class Hashing { #if defined(ARROW_HAVE_AVX2) static void avalanche_avx2(uint32_t num_keys, uint32_t* hashes); #endif - static void avalanche(int64_t hardware_flags, uint32_t num_keys, - uint32_t* hashes); + static void avalanche(int64_t hardware_flags, uint32_t num_keys, uint32_t* hashes); // Accumulator combine static inline uint32_t combine_accumulators(const uint32_t acc1, const uint32_t acc2, @@ -77,8 +78,8 @@ class Hashing { #endif static void helper_stripes(int64_t hardware_flags, uint32_t num_keys, uint32_t key_length, const uint8_t* keys, uint32_t* hash); - static void helper_tails(int64_t hardware_flags, uint32_t num_keys, - uint32_t key_length, const uint8_t* keys, uint32_t* hash); + static void helper_tails(int64_t hardware_flags, uint32_t num_keys, uint32_t key_length, + const uint8_t* keys, uint32_t* hash); static void hash_varlen_helper(uint32_t length, const uint8_t* key, uint32_t* acc); #if defined(ARROW_HAVE_AVX2) diff --git a/cpp/src/arrow/compute/exec/key_map.cc b/cpp/src/arrow/compute/exec/key_map.cc index d390c3cfd4b..73a8a5b8b61 100644 --- a/cpp/src/arrow/compute/exec/key_map.cc +++ b/cpp/src/arrow/compute/exec/key_map.cc @@ -108,7 +108,7 @@ inline uint64_t SwissTable::extract_group_id(const uint8_t* block_ptr, int slot, // Group id values for all 8 slots in the block are bit-packed and follow the status // bytes. We assume here that the number of bits is rounded up to 8, 16, 32 or 64. In // that case we can extract group id using aligned 64-bit word access. - int num_groupid_bits = arrow::BitUtil::PopCount(group_id_mask); + int num_groupid_bits = ARROW_POPCOUNT64(group_id_mask); ARROW_DCHECK(num_groupid_bits == 8 || num_groupid_bits == 16 || num_groupid_bits == 32 || num_groupid_bits == 64); @@ -191,7 +191,7 @@ void SwissTable::lookup_1(const uint16_t* selection, const int num_keys, // How many groups we can keep in the hash table without the need for resizing. // When we reach this limit, we need to break processing of any further rows and resize. // -uint32_t SwissTable::num_groups_for_resize() const { +uint64_t SwissTable::num_groups_for_resize() const { // Resize small hash tables when 50% full (up to 12KB). // Resize large hash tables when 75% full. constexpr int log_blocks_small_ = 9; @@ -215,7 +215,7 @@ uint64_t SwissTable::wrap_global_slot_id(uint64_t global_slot_id) { Status SwissTable::lookup_2(const uint32_t* hashes, uint32_t* inout_num_selected, uint16_t* inout_selection, bool* out_need_resize, uint32_t* out_group_ids, uint32_t* inout_next_slot_ids) { - uint32_t num_groups_limit = num_groups_for_resize(); + auto num_groups_limit = num_groups_for_resize(); ARROW_DCHECK(num_inserted_ < num_groups_limit); // Temporary arrays are of limited size. diff --git a/cpp/src/arrow/compute/exec/key_map.h b/cpp/src/arrow/compute/exec/key_map.h index d3ebca362c7..8c472736ec4 100644 --- a/cpp/src/arrow/compute/exec/key_map.h +++ b/cpp/src/arrow/compute/exec/key_map.h @@ -38,9 +38,8 @@ class SwissTable { uint16_t* out_selection_mismatch)>; using AppendImpl = std::function; - Status init(int64_t hardware_flags, MemoryPool* pool, - util::TempVectorStack* temp_stack, int log_minibatch, EqualImpl equal_impl, - AppendImpl append_impl); + Status init(int64_t hardware_flags, MemoryPool* pool, util::TempVectorStack* temp_stack, + int log_minibatch, EqualImpl equal_impl, AppendImpl append_impl); void cleanup(); Status map(const int ckeys, const uint32_t* hashes, uint32_t* outgroupids); @@ -85,7 +84,7 @@ class SwissTable { inline void insert(uint8_t* block_base, uint64_t slot_id, uint32_t hash, uint8_t stamp, uint32_t group_id); - inline uint32_t num_groups_for_resize() const; + inline uint64_t num_groups_for_resize() const; inline uint64_t wrap_global_slot_id(uint64_t global_slot_id); diff --git a/cpp/src/arrow/compute/exec/util.cc b/cpp/src/arrow/compute/exec/util.cc index 71e6a9d334a..5f1c0776c56 100644 --- a/cpp/src/arrow/compute/exec/util.cc +++ b/cpp/src/arrow/compute/exec/util.cc @@ -48,10 +48,9 @@ inline void BitUtil::bits_filter_indexes_helper(uint64_t word, } template -void BitUtil::bits_to_indexes_internal(int64_t hardware_flags, - const int num_bits, const uint8_t* bits, - const uint16_t* input_indexes, int* num_indexes, - uint16_t* indexes) { +void BitUtil::bits_to_indexes_internal(int64_t hardware_flags, const int num_bits, + const uint8_t* bits, const uint16_t* input_indexes, + int* num_indexes, uint16_t* indexes) { // 64 bits at a time constexpr int unroll = 64; int tail = num_bits % unroll; @@ -100,17 +99,16 @@ void BitUtil::bits_to_indexes(int bit_to_search, int64_t hardware_flags, const int num_bits, const uint8_t* bits, int* num_indexes, uint16_t* indexes) { if (bit_to_search == 0) { - bits_to_indexes_internal<0, false>(hardware_flags, num_bits, bits, nullptr, num_indexes, - indexes); + bits_to_indexes_internal<0, false>(hardware_flags, num_bits, bits, nullptr, + num_indexes, indexes); } else { ARROW_DCHECK(bit_to_search == 1); - bits_to_indexes_internal<1, false>(hardware_flags, num_bits, bits, nullptr, num_indexes, - indexes); + bits_to_indexes_internal<1, false>(hardware_flags, num_bits, bits, nullptr, + num_indexes, indexes); } } -void BitUtil::bits_filter_indexes(int bit_to_search, - int64_t hardware_flags, +void BitUtil::bits_filter_indexes(int bit_to_search, int64_t hardware_flags, const int num_bits, const uint8_t* bits, const uint16_t* input_indexes, int* num_indexes, uint16_t* indexes) { @@ -124,10 +122,9 @@ void BitUtil::bits_filter_indexes(int bit_to_search, } } -void BitUtil::bits_split_indexes(int64_t hardware_flags, - const int num_bits, const uint8_t* bits, - int* num_indexes_bit0, uint16_t* indexes_bit0, - uint16_t* indexes_bit1) { +void BitUtil::bits_split_indexes(int64_t hardware_flags, const int num_bits, + const uint8_t* bits, int* num_indexes_bit0, + uint16_t* indexes_bit0, uint16_t* indexes_bit1) { bits_to_indexes(0, hardware_flags, num_bits, bits, num_indexes_bit0, indexes_bit0); int num_indexes_bit1; bits_to_indexes(1, hardware_flags, num_bits, bits, &num_indexes_bit1, indexes_bit1); @@ -213,8 +210,8 @@ void BitUtil::bytes_to_bits(int64_t hardware_flags, const int num_bits, } } -bool BitUtil::are_all_bytes_zero(int64_t hardware_flags, - const uint8_t* bytes, uint32_t num_bytes) { +bool BitUtil::are_all_bytes_zero(int64_t hardware_flags, const uint8_t* bytes, + uint32_t num_bytes) { #if defined(ARROW_HAVE_AVX2) if (hardware_flags & arrow::internal::CpuInfo::AVX2) { return are_all_bytes_zero_avx2(bytes, num_bytes); diff --git a/cpp/src/arrow/compute/exec/util.h b/cpp/src/arrow/compute/exec/util.h index 81d9b6e806f..c0e94bcc6ac 100644 --- a/cpp/src/arrow/compute/exec/util.h +++ b/cpp/src/arrow/compute/exec/util.h @@ -106,17 +106,15 @@ class BitUtil { const int num_bits, const uint8_t* bits, int* num_indexes, uint16_t* indexes); - static void bits_filter_indexes(int bit_to_search, - int64_t hardware_flags, + static void bits_filter_indexes(int bit_to_search, int64_t hardware_flags, const int num_bits, const uint8_t* bits, const uint16_t* input_indexes, int* num_indexes, uint16_t* indexes); // Input and output indexes may be pointing to the same data (in-place filtering). - static void bits_split_indexes(int64_t hardware_flags, - const int num_bits, const uint8_t* bits, - int* num_indexes_bit0, uint16_t* indexes_bit0, - uint16_t* indexes_bit1); + static void bits_split_indexes(int64_t hardware_flags, const int num_bits, + const uint8_t* bits, int* num_indexes_bit0, + uint16_t* indexes_bit0, uint16_t* indexes_bit1); // Bit 1 is replaced with byte 0xFF. static void bits_to_bytes(int64_t hardware_flags, const int num_bits, @@ -125,8 +123,8 @@ class BitUtil { static void bytes_to_bits(int64_t hardware_flags, const int num_bits, const uint8_t* bytes, uint8_t* bits); - static bool are_all_bytes_zero(int64_t hardware_flags, - const uint8_t* bytes, uint32_t num_bytes); + static bool are_all_bytes_zero(int64_t hardware_flags, const uint8_t* bytes, + uint32_t num_bytes); private: inline static void bits_to_indexes_helper(uint64_t word, uint16_t base_index, @@ -135,10 +133,9 @@ class BitUtil { const uint16_t* input_indexes, int* num_indexes, uint16_t* indexes); template - static void bits_to_indexes_internal(int64_t hardware_flags, - const int num_bits, const uint8_t* bits, - const uint16_t* input_indexes, int* num_indexes, - uint16_t* indexes); + static void bits_to_indexes_internal(int64_t hardware_flags, const int num_bits, + const uint8_t* bits, const uint16_t* input_indexes, + int* num_indexes, uint16_t* indexes); static void bits_to_bytes_internal(const int num_bits, const uint8_t* bits, uint8_t* bytes); static void bytes_to_bits_internal(const int num_bits, const uint8_t* bytes, diff --git a/cpp/src/arrow/compute/kernels/hash_aggregate.cc b/cpp/src/arrow/compute/kernels/hash_aggregate.cc index 9683c779d14..7d3a883662a 100644 --- a/cpp/src/arrow/compute/kernels/hash_aggregate.cc +++ b/cpp/src/arrow/compute/kernels/hash_aggregate.cc @@ -458,7 +458,8 @@ struct GrouperFastImpl : Grouper { impl->ctx_ = ctx; RETURN_NOT_OK(impl->temp_stack_.Init(ctx->memory_pool(), 64 * minibatch_size_max_)); - impl->encode_ctx_.hardware_flags = arrow::internal::CpuInfo::GetInstance()->hardware_flags(); + impl->encode_ctx_.hardware_flags = + arrow::internal::CpuInfo::GetInstance()->hardware_flags(); impl->encode_ctx_.stack = &impl->temp_stack_; auto num_columns = keys.size(); From dab54626bd0ab45499f5e76fbe59161a0ea50a21 Mon Sep 17 00:00:00 2001 From: michalursa Date: Tue, 18 May 2021 14:46:01 -0700 Subject: [PATCH 06/11] GrouperFastImpl: fixing build errors --- cpp/src/arrow/compute/exec/key_compare.cc | 5 ++-- .../arrow/compute/exec/key_compare_avx2.cc | 4 +-- cpp/src/arrow/compute/exec/key_encode.h | 2 +- cpp/src/arrow/compute/exec/key_hash.cc | 21 ++++---------- cpp/src/arrow/compute/exec/key_hash.h | 13 +++++---- cpp/src/arrow/compute/exec/key_hash_avx2.cc | 2 +- cpp/src/arrow/compute/exec/key_map.cc | 6 ++-- cpp/src/arrow/compute/exec/key_map.h | 7 ++--- cpp/src/arrow/compute/exec/util.cc | 29 +++++++++---------- cpp/src/arrow/compute/exec/util.h | 21 ++++++-------- .../arrow/compute/kernels/hash_aggregate.cc | 3 +- 11 files changed, 50 insertions(+), 63 deletions(-) diff --git a/cpp/src/arrow/compute/exec/key_compare.cc b/cpp/src/arrow/compute/exec/key_compare.cc index feb5c450c94..f8d74859b01 100644 --- a/cpp/src/arrow/compute/exec/key_compare.cc +++ b/cpp/src/arrow/compute/exec/key_compare.cc @@ -81,8 +81,9 @@ void KeyCompare::CompareRows(uint32_t num_rows_to_compare, *out_num_rows = out_num_rows_int; } else { int out_num_rows_int; - util::BitUtil::bits_to_indexes(0, ctx->hardware_flags, num_rows_to_compare, match_bitvector, - &out_num_rows_int, out_sel_left_maybe_same); + util::BitUtil::bits_to_indexes(0, ctx->hardware_flags, num_rows_to_compare, + match_bitvector, &out_num_rows_int, + out_sel_left_maybe_same); *out_num_rows = out_num_rows_int; } } diff --git a/cpp/src/arrow/compute/exec/key_compare_avx2.cc b/cpp/src/arrow/compute/exec/key_compare_avx2.cc index 54e907bbc1d..28e198a79be 100644 --- a/cpp/src/arrow/compute/exec/key_compare_avx2.cc +++ b/cpp/src/arrow/compute/exec/key_compare_avx2.cc @@ -42,8 +42,8 @@ uint32_t KeyCompare::CompareFixedLength_UpTo8B_avx2( _mm_loadu_si128(reinterpret_cast(left_to_right_map) + i); offset_right = _mm_mullo_epi32(offset_right, _mm_set1_epi32(length)); - auto key_right = - _mm256_i32gather_epi64((const long long*)rows_right, offset_right, 1); + auto key_right = _mm256_i32gather_epi64(reinterpret_cast(rows_right), + offset_right, 1); uint32_t cmp = _mm256_movemask_epi8(_mm256_cmpeq_epi64( _mm256_and_si256(key_left, mask), _mm256_and_si256(key_right, mask))); reinterpret_cast(match_bytevector)[i] &= cmp; diff --git a/cpp/src/arrow/compute/exec/key_encode.h b/cpp/src/arrow/compute/exec/key_encode.h index 68fcac041cc..3f5ef365a08 100644 --- a/cpp/src/arrow/compute/exec/key_encode.h +++ b/cpp/src/arrow/compute/exec/key_encode.h @@ -45,7 +45,7 @@ class KeyEncoder { bool has_avx2() const { return (hardware_flags & arrow::internal::CpuInfo::AVX2) > 0; } - int hardware_flags; + int64_t hardware_flags; util::TempVectorStack* stack; }; diff --git a/cpp/src/arrow/compute/exec/key_hash.cc b/cpp/src/arrow/compute/exec/key_hash.cc index 50043d51cc6..081411e708e 100644 --- a/cpp/src/arrow/compute/exec/key_hash.cc +++ b/cpp/src/arrow/compute/exec/key_hash.cc @@ -19,19 +19,11 @@ #include +#include #include #include "arrow/compute/exec/util.h" -#ifdef _MSC_VER -#include -#else -#include -#endif -#include - -#include - namespace arrow { namespace compute { @@ -44,8 +36,7 @@ inline uint32_t Hashing::avalanche_helper(uint32_t acc) { return acc; } -void Hashing::avalanche(int64_t hardware_flags, uint32_t num_keys, - uint32_t* hashes) { +void Hashing::avalanche(int64_t hardware_flags, uint32_t num_keys, uint32_t* hashes) { uint32_t processed = 0; #if defined(ARROW_HAVE_AVX2) if (hardware_flags & arrow::internal::CpuInfo::AVX2) { @@ -149,8 +140,8 @@ inline uint32_t Hashing::helper_tail(uint32_t offset, uint64_t mask, const uint8 return acc; } -void Hashing::helper_tails(int64_t hardware_flags, uint32_t num_keys, - uint32_t key_length, const uint8_t* keys, uint32_t* hash) { +void Hashing::helper_tails(int64_t hardware_flags, uint32_t num_keys, uint32_t key_length, + const uint8_t* keys, uint32_t* hash) { uint32_t processed = 0; #if defined(ARROW_HAVE_AVX2) if (hardware_flags & arrow::internal::CpuInfo::AVX2) { @@ -168,8 +159,8 @@ void Hashing::helper_tails(int64_t hardware_flags, uint32_t num_keys, } } -void Hashing::hash_fixed(int64_t hardware_flags, uint32_t num_keys, - uint32_t length_key, const uint8_t* keys, uint32_t* hashes) { +void Hashing::hash_fixed(int64_t hardware_flags, uint32_t num_keys, uint32_t length_key, + const uint8_t* keys, uint32_t* hashes) { ARROW_DCHECK(length_key > 0); if (length_key <= 8) { diff --git a/cpp/src/arrow/compute/exec/key_hash.h b/cpp/src/arrow/compute/exec/key_hash.h index 4c02ed6d174..7f8ab5185cc 100644 --- a/cpp/src/arrow/compute/exec/key_hash.h +++ b/cpp/src/arrow/compute/exec/key_hash.h @@ -17,7 +17,9 @@ #pragma once +#if defined(ARROW_HAVE_AVX2) #include +#endif #include @@ -31,8 +33,8 @@ namespace compute { // class Hashing { public: - static void hash_fixed(int64_t hardware_flags, uint32_t num_keys, - uint32_t length_key, const uint8_t* keys, uint32_t* hashes); + static void hash_fixed(int64_t hardware_flags, uint32_t num_keys, uint32_t length_key, + const uint8_t* keys, uint32_t* hashes); static void hash_varlen(int64_t hardware_flags, uint32_t num_rows, const uint32_t* offsets, const uint8_t* concatenated_keys, @@ -51,8 +53,7 @@ class Hashing { #if defined(ARROW_HAVE_AVX2) static void avalanche_avx2(uint32_t num_keys, uint32_t* hashes); #endif - static void avalanche(int64_t hardware_flags, uint32_t num_keys, - uint32_t* hashes); + static void avalanche(int64_t hardware_flags, uint32_t num_keys, uint32_t* hashes); // Accumulator combine static inline uint32_t combine_accumulators(const uint32_t acc1, const uint32_t acc2, @@ -77,8 +78,8 @@ class Hashing { #endif static void helper_stripes(int64_t hardware_flags, uint32_t num_keys, uint32_t key_length, const uint8_t* keys, uint32_t* hash); - static void helper_tails(int64_t hardware_flags, uint32_t num_keys, - uint32_t key_length, const uint8_t* keys, uint32_t* hash); + static void helper_tails(int64_t hardware_flags, uint32_t num_keys, uint32_t key_length, + const uint8_t* keys, uint32_t* hash); static void hash_varlen_helper(uint32_t length, const uint8_t* key, uint32_t* acc); #if defined(ARROW_HAVE_AVX2) diff --git a/cpp/src/arrow/compute/exec/key_hash_avx2.cc b/cpp/src/arrow/compute/exec/key_hash_avx2.cc index 68f1d11095d..38ab48f7245 100644 --- a/cpp/src/arrow/compute/exec/key_hash_avx2.cc +++ b/cpp/src/arrow/compute/exec/key_hash_avx2.cc @@ -104,7 +104,7 @@ void Hashing::helper_tails_avx2(uint32_t num_keys, uint32_t key_length, const uint8_t* keys, uint32_t* hash) { constexpr int unroll = 8; ARROW_DCHECK(num_keys % unroll == 0); - auto keys_i64 = reinterpret_cast(keys); + auto keys_i64 = reinterpret_cast(keys); // Process between 1 and 8 last bytes of each key, starting from 16B boundary. // The caller needs to make sure that there are no more than 8 bytes to process after diff --git a/cpp/src/arrow/compute/exec/key_map.cc b/cpp/src/arrow/compute/exec/key_map.cc index d390c3cfd4b..a6c639ecd5f 100644 --- a/cpp/src/arrow/compute/exec/key_map.cc +++ b/cpp/src/arrow/compute/exec/key_map.cc @@ -108,7 +108,7 @@ inline uint64_t SwissTable::extract_group_id(const uint8_t* block_ptr, int slot, // Group id values for all 8 slots in the block are bit-packed and follow the status // bytes. We assume here that the number of bits is rounded up to 8, 16, 32 or 64. In // that case we can extract group id using aligned 64-bit word access. - int num_groupid_bits = arrow::BitUtil::PopCount(group_id_mask); + int num_groupid_bits = static_cast(ARROW_POPCOUNT64(group_id_mask)); ARROW_DCHECK(num_groupid_bits == 8 || num_groupid_bits == 16 || num_groupid_bits == 32 || num_groupid_bits == 64); @@ -191,7 +191,7 @@ void SwissTable::lookup_1(const uint16_t* selection, const int num_keys, // How many groups we can keep in the hash table without the need for resizing. // When we reach this limit, we need to break processing of any further rows and resize. // -uint32_t SwissTable::num_groups_for_resize() const { +uint64_t SwissTable::num_groups_for_resize() const { // Resize small hash tables when 50% full (up to 12KB). // Resize large hash tables when 75% full. constexpr int log_blocks_small_ = 9; @@ -215,7 +215,7 @@ uint64_t SwissTable::wrap_global_slot_id(uint64_t global_slot_id) { Status SwissTable::lookup_2(const uint32_t* hashes, uint32_t* inout_num_selected, uint16_t* inout_selection, bool* out_need_resize, uint32_t* out_group_ids, uint32_t* inout_next_slot_ids) { - uint32_t num_groups_limit = num_groups_for_resize(); + auto num_groups_limit = num_groups_for_resize(); ARROW_DCHECK(num_inserted_ < num_groups_limit); // Temporary arrays are of limited size. diff --git a/cpp/src/arrow/compute/exec/key_map.h b/cpp/src/arrow/compute/exec/key_map.h index d3ebca362c7..8c472736ec4 100644 --- a/cpp/src/arrow/compute/exec/key_map.h +++ b/cpp/src/arrow/compute/exec/key_map.h @@ -38,9 +38,8 @@ class SwissTable { uint16_t* out_selection_mismatch)>; using AppendImpl = std::function; - Status init(int64_t hardware_flags, MemoryPool* pool, - util::TempVectorStack* temp_stack, int log_minibatch, EqualImpl equal_impl, - AppendImpl append_impl); + Status init(int64_t hardware_flags, MemoryPool* pool, util::TempVectorStack* temp_stack, + int log_minibatch, EqualImpl equal_impl, AppendImpl append_impl); void cleanup(); Status map(const int ckeys, const uint32_t* hashes, uint32_t* outgroupids); @@ -85,7 +84,7 @@ class SwissTable { inline void insert(uint8_t* block_base, uint64_t slot_id, uint32_t hash, uint8_t stamp, uint32_t group_id); - inline uint32_t num_groups_for_resize() const; + inline uint64_t num_groups_for_resize() const; inline uint64_t wrap_global_slot_id(uint64_t global_slot_id); diff --git a/cpp/src/arrow/compute/exec/util.cc b/cpp/src/arrow/compute/exec/util.cc index 71e6a9d334a..5f1c0776c56 100644 --- a/cpp/src/arrow/compute/exec/util.cc +++ b/cpp/src/arrow/compute/exec/util.cc @@ -48,10 +48,9 @@ inline void BitUtil::bits_filter_indexes_helper(uint64_t word, } template -void BitUtil::bits_to_indexes_internal(int64_t hardware_flags, - const int num_bits, const uint8_t* bits, - const uint16_t* input_indexes, int* num_indexes, - uint16_t* indexes) { +void BitUtil::bits_to_indexes_internal(int64_t hardware_flags, const int num_bits, + const uint8_t* bits, const uint16_t* input_indexes, + int* num_indexes, uint16_t* indexes) { // 64 bits at a time constexpr int unroll = 64; int tail = num_bits % unroll; @@ -100,17 +99,16 @@ void BitUtil::bits_to_indexes(int bit_to_search, int64_t hardware_flags, const int num_bits, const uint8_t* bits, int* num_indexes, uint16_t* indexes) { if (bit_to_search == 0) { - bits_to_indexes_internal<0, false>(hardware_flags, num_bits, bits, nullptr, num_indexes, - indexes); + bits_to_indexes_internal<0, false>(hardware_flags, num_bits, bits, nullptr, + num_indexes, indexes); } else { ARROW_DCHECK(bit_to_search == 1); - bits_to_indexes_internal<1, false>(hardware_flags, num_bits, bits, nullptr, num_indexes, - indexes); + bits_to_indexes_internal<1, false>(hardware_flags, num_bits, bits, nullptr, + num_indexes, indexes); } } -void BitUtil::bits_filter_indexes(int bit_to_search, - int64_t hardware_flags, +void BitUtil::bits_filter_indexes(int bit_to_search, int64_t hardware_flags, const int num_bits, const uint8_t* bits, const uint16_t* input_indexes, int* num_indexes, uint16_t* indexes) { @@ -124,10 +122,9 @@ void BitUtil::bits_filter_indexes(int bit_to_search, } } -void BitUtil::bits_split_indexes(int64_t hardware_flags, - const int num_bits, const uint8_t* bits, - int* num_indexes_bit0, uint16_t* indexes_bit0, - uint16_t* indexes_bit1) { +void BitUtil::bits_split_indexes(int64_t hardware_flags, const int num_bits, + const uint8_t* bits, int* num_indexes_bit0, + uint16_t* indexes_bit0, uint16_t* indexes_bit1) { bits_to_indexes(0, hardware_flags, num_bits, bits, num_indexes_bit0, indexes_bit0); int num_indexes_bit1; bits_to_indexes(1, hardware_flags, num_bits, bits, &num_indexes_bit1, indexes_bit1); @@ -213,8 +210,8 @@ void BitUtil::bytes_to_bits(int64_t hardware_flags, const int num_bits, } } -bool BitUtil::are_all_bytes_zero(int64_t hardware_flags, - const uint8_t* bytes, uint32_t num_bytes) { +bool BitUtil::are_all_bytes_zero(int64_t hardware_flags, const uint8_t* bytes, + uint32_t num_bytes) { #if defined(ARROW_HAVE_AVX2) if (hardware_flags & arrow::internal::CpuInfo::AVX2) { return are_all_bytes_zero_avx2(bytes, num_bytes); diff --git a/cpp/src/arrow/compute/exec/util.h b/cpp/src/arrow/compute/exec/util.h index 81d9b6e806f..c0e94bcc6ac 100644 --- a/cpp/src/arrow/compute/exec/util.h +++ b/cpp/src/arrow/compute/exec/util.h @@ -106,17 +106,15 @@ class BitUtil { const int num_bits, const uint8_t* bits, int* num_indexes, uint16_t* indexes); - static void bits_filter_indexes(int bit_to_search, - int64_t hardware_flags, + static void bits_filter_indexes(int bit_to_search, int64_t hardware_flags, const int num_bits, const uint8_t* bits, const uint16_t* input_indexes, int* num_indexes, uint16_t* indexes); // Input and output indexes may be pointing to the same data (in-place filtering). - static void bits_split_indexes(int64_t hardware_flags, - const int num_bits, const uint8_t* bits, - int* num_indexes_bit0, uint16_t* indexes_bit0, - uint16_t* indexes_bit1); + static void bits_split_indexes(int64_t hardware_flags, const int num_bits, + const uint8_t* bits, int* num_indexes_bit0, + uint16_t* indexes_bit0, uint16_t* indexes_bit1); // Bit 1 is replaced with byte 0xFF. static void bits_to_bytes(int64_t hardware_flags, const int num_bits, @@ -125,8 +123,8 @@ class BitUtil { static void bytes_to_bits(int64_t hardware_flags, const int num_bits, const uint8_t* bytes, uint8_t* bits); - static bool are_all_bytes_zero(int64_t hardware_flags, - const uint8_t* bytes, uint32_t num_bytes); + static bool are_all_bytes_zero(int64_t hardware_flags, const uint8_t* bytes, + uint32_t num_bytes); private: inline static void bits_to_indexes_helper(uint64_t word, uint16_t base_index, @@ -135,10 +133,9 @@ class BitUtil { const uint16_t* input_indexes, int* num_indexes, uint16_t* indexes); template - static void bits_to_indexes_internal(int64_t hardware_flags, - const int num_bits, const uint8_t* bits, - const uint16_t* input_indexes, int* num_indexes, - uint16_t* indexes); + static void bits_to_indexes_internal(int64_t hardware_flags, const int num_bits, + const uint8_t* bits, const uint16_t* input_indexes, + int* num_indexes, uint16_t* indexes); static void bits_to_bytes_internal(const int num_bits, const uint8_t* bits, uint8_t* bytes); static void bytes_to_bits_internal(const int num_bits, const uint8_t* bytes, diff --git a/cpp/src/arrow/compute/kernels/hash_aggregate.cc b/cpp/src/arrow/compute/kernels/hash_aggregate.cc index 9683c779d14..7d3a883662a 100644 --- a/cpp/src/arrow/compute/kernels/hash_aggregate.cc +++ b/cpp/src/arrow/compute/kernels/hash_aggregate.cc @@ -458,7 +458,8 @@ struct GrouperFastImpl : Grouper { impl->ctx_ = ctx; RETURN_NOT_OK(impl->temp_stack_.Init(ctx->memory_pool(), 64 * minibatch_size_max_)); - impl->encode_ctx_.hardware_flags = arrow::internal::CpuInfo::GetInstance()->hardware_flags(); + impl->encode_ctx_.hardware_flags = + arrow::internal::CpuInfo::GetInstance()->hardware_flags(); impl->encode_ctx_.stack = &impl->temp_stack_; auto num_columns = keys.size(); From f9581ed7df8de554d2799f43b6987a8e1e6cceeb Mon Sep 17 00:00:00 2001 From: michalursa Date: Tue, 18 May 2021 15:17:36 -0700 Subject: [PATCH 07/11] GrouperFastImpl: fixing build errors --- cpp/src/arrow/compute/exec/key_map.cc | 1 - 1 file changed, 1 deletion(-) diff --git a/cpp/src/arrow/compute/exec/key_map.cc b/cpp/src/arrow/compute/exec/key_map.cc index a6c639ecd5f..c48487793e0 100644 --- a/cpp/src/arrow/compute/exec/key_map.cc +++ b/cpp/src/arrow/compute/exec/key_map.cc @@ -17,7 +17,6 @@ #include "arrow/compute/exec/key_map.h" -#include #include #include From 6a93dffd64ff1dd29768b84ebd0c92bacf457469 Mon Sep 17 00:00:00 2001 From: michalursa Date: Tue, 18 May 2021 19:47:14 -0700 Subject: [PATCH 08/11] GrouperFastImpl: more build fixes --- cpp/src/arrow/compute/exec/doc/key_map.md | 19 +++++++++++++++++++ .../arrow/compute/exec/key_compare_avx2.cc | 4 ++-- cpp/src/arrow/compute/exec/key_hash_avx2.cc | 2 +- cpp/src/arrow/compute/exec/key_map_avx2.cc | 2 +- 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/cpp/src/arrow/compute/exec/doc/key_map.md b/cpp/src/arrow/compute/exec/doc/key_map.md index 03f28c1f803..fdedc88c4d4 100644 --- a/cpp/src/arrow/compute/exec/doc/key_map.md +++ b/cpp/src/arrow/compute/exec/doc/key_map.md @@ -1,3 +1,22 @@ + + # Swiss Table A specialized hash table implementation used to dynamically map combinations of key field values to a dense set of integer ids. Ids can later be used in place of keys to identify groups of rows with equal keys. diff --git a/cpp/src/arrow/compute/exec/key_compare_avx2.cc b/cpp/src/arrow/compute/exec/key_compare_avx2.cc index 28e198a79be..eec3ab4e6eb 100644 --- a/cpp/src/arrow/compute/exec/key_compare_avx2.cc +++ b/cpp/src/arrow/compute/exec/key_compare_avx2.cc @@ -35,14 +35,14 @@ uint32_t KeyCompare::CompareFixedLength_UpTo8B_avx2( constexpr uint32_t unroll = 4; for (uint32_t i = 0; i < num_rows / unroll; ++i) { - auto key_left = _mm256_i64gather_epi64(reinterpret_cast(rows_left), + auto key_left = _mm256_i64gather_epi64(reinterpret_cast(rows_left), offset_left, 1); offset_left = _mm256_add_epi64(offset_left, offset_left_incr); __m128i offset_right = _mm_loadu_si128(reinterpret_cast(left_to_right_map) + i); offset_right = _mm_mullo_epi32(offset_right, _mm_set1_epi32(length)); - auto key_right = _mm256_i32gather_epi64(reinterpret_cast(rows_right), + auto key_right = _mm256_i32gather_epi64(reinterpret_cast(rows_right), offset_right, 1); uint32_t cmp = _mm256_movemask_epi8(_mm256_cmpeq_epi64( _mm256_and_si256(key_left, mask), _mm256_and_si256(key_right, mask))); diff --git a/cpp/src/arrow/compute/exec/key_hash_avx2.cc b/cpp/src/arrow/compute/exec/key_hash_avx2.cc index 38ab48f7245..0244ef81968 100644 --- a/cpp/src/arrow/compute/exec/key_hash_avx2.cc +++ b/cpp/src/arrow/compute/exec/key_hash_avx2.cc @@ -104,7 +104,7 @@ void Hashing::helper_tails_avx2(uint32_t num_keys, uint32_t key_length, const uint8_t* keys, uint32_t* hash) { constexpr int unroll = 8; ARROW_DCHECK(num_keys % unroll == 0); - auto keys_i64 = reinterpret_cast(keys); + auto keys_i64 = reinterpret_cast(keys); // Process between 1 and 8 last bytes of each key, starting from 16B boundary. // The caller needs to make sure that there are no more than 8 bytes to process after diff --git a/cpp/src/arrow/compute/exec/key_map_avx2.cc b/cpp/src/arrow/compute/exec/key_map_avx2.cc index f78c5e179a0..a7cabc7491f 100644 --- a/cpp/src/arrow/compute/exec/key_map_avx2.cc +++ b/cpp/src/arrow/compute/exec/key_map_avx2.cc @@ -70,7 +70,7 @@ void SwissTable::lookup_1_avx2_x8(const int num_hashes, const uint32_t* hashes, __m256i voffset_B = _mm256_srli_epi64(vblock_offset, 32); __m256i vstamp_B = _mm256_srli_epi64(vstamp, 32); - auto blocks_i64 = reinterpret_cast(blocks_); + auto blocks_i64 = reinterpret_cast(blocks_); auto vblock_A = _mm256_i64gather_epi64(blocks_i64, voffset_A, 1); auto vblock_B = _mm256_i64gather_epi64(blocks_i64, voffset_B, 1); __m256i vblock_highbits_A = From 3cdd601827fecac87d913b79b45783fd579b432d Mon Sep 17 00:00:00 2001 From: michalursa Date: Tue, 18 May 2021 19:47:14 -0700 Subject: [PATCH 09/11] GrouperFastImpl: more build fixes --- cpp/src/arrow/compute/exec/doc/key_map.md | 19 +++++++++++++++++++ .../arrow/compute/exec/key_compare_avx2.cc | 8 ++++---- cpp/src/arrow/compute/exec/key_hash_avx2.cc | 2 +- cpp/src/arrow/compute/exec/key_map_avx2.cc | 2 +- 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/cpp/src/arrow/compute/exec/doc/key_map.md b/cpp/src/arrow/compute/exec/doc/key_map.md index 03f28c1f803..fdedc88c4d4 100644 --- a/cpp/src/arrow/compute/exec/doc/key_map.md +++ b/cpp/src/arrow/compute/exec/doc/key_map.md @@ -1,3 +1,22 @@ + + # Swiss Table A specialized hash table implementation used to dynamically map combinations of key field values to a dense set of integer ids. Ids can later be used in place of keys to identify groups of rows with equal keys. diff --git a/cpp/src/arrow/compute/exec/key_compare_avx2.cc b/cpp/src/arrow/compute/exec/key_compare_avx2.cc index 28e198a79be..5aea1613968 100644 --- a/cpp/src/arrow/compute/exec/key_compare_avx2.cc +++ b/cpp/src/arrow/compute/exec/key_compare_avx2.cc @@ -35,15 +35,15 @@ uint32_t KeyCompare::CompareFixedLength_UpTo8B_avx2( constexpr uint32_t unroll = 4; for (uint32_t i = 0; i < num_rows / unroll; ++i) { - auto key_left = _mm256_i64gather_epi64(reinterpret_cast(rows_left), - offset_left, 1); + auto key_left = _mm256_i64gather_epi64( + reinterpret_cast(rows_left), offset_left, 1); offset_left = _mm256_add_epi64(offset_left, offset_left_incr); __m128i offset_right = _mm_loadu_si128(reinterpret_cast(left_to_right_map) + i); offset_right = _mm_mullo_epi32(offset_right, _mm_set1_epi32(length)); - auto key_right = _mm256_i32gather_epi64(reinterpret_cast(rows_right), - offset_right, 1); + auto key_right = _mm256_i32gather_epi64( + reinterpret_cast(rows_right), offset_right, 1); uint32_t cmp = _mm256_movemask_epi8(_mm256_cmpeq_epi64( _mm256_and_si256(key_left, mask), _mm256_and_si256(key_right, mask))); reinterpret_cast(match_bytevector)[i] &= cmp; diff --git a/cpp/src/arrow/compute/exec/key_hash_avx2.cc b/cpp/src/arrow/compute/exec/key_hash_avx2.cc index 38ab48f7245..0244ef81968 100644 --- a/cpp/src/arrow/compute/exec/key_hash_avx2.cc +++ b/cpp/src/arrow/compute/exec/key_hash_avx2.cc @@ -104,7 +104,7 @@ void Hashing::helper_tails_avx2(uint32_t num_keys, uint32_t key_length, const uint8_t* keys, uint32_t* hash) { constexpr int unroll = 8; ARROW_DCHECK(num_keys % unroll == 0); - auto keys_i64 = reinterpret_cast(keys); + auto keys_i64 = reinterpret_cast(keys); // Process between 1 and 8 last bytes of each key, starting from 16B boundary. // The caller needs to make sure that there are no more than 8 bytes to process after diff --git a/cpp/src/arrow/compute/exec/key_map_avx2.cc b/cpp/src/arrow/compute/exec/key_map_avx2.cc index f78c5e179a0..a7cabc7491f 100644 --- a/cpp/src/arrow/compute/exec/key_map_avx2.cc +++ b/cpp/src/arrow/compute/exec/key_map_avx2.cc @@ -70,7 +70,7 @@ void SwissTable::lookup_1_avx2_x8(const int num_hashes, const uint32_t* hashes, __m256i voffset_B = _mm256_srli_epi64(vblock_offset, 32); __m256i vstamp_B = _mm256_srli_epi64(vstamp, 32); - auto blocks_i64 = reinterpret_cast(blocks_); + auto blocks_i64 = reinterpret_cast(blocks_); auto vblock_A = _mm256_i64gather_epi64(blocks_i64, voffset_A, 1); auto vblock_B = _mm256_i64gather_epi64(blocks_i64, voffset_B, 1); __m256i vblock_highbits_A = From 113706a0096590e3c244f33cbbe78c94a15b9f1c Mon Sep 17 00:00:00 2001 From: michalursa Date: Tue, 18 May 2021 22:05:03 -0700 Subject: [PATCH 10/11] GrouperFastImpl: more build fixes --- cpp/src/arrow/compute/exec/key_compare_avx2.cc | 8 ++++---- cpp/src/arrow/compute/exec/key_hash_avx2.cc | 2 +- cpp/src/arrow/compute/exec/key_map_avx2.cc | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cpp/src/arrow/compute/exec/key_compare_avx2.cc b/cpp/src/arrow/compute/exec/key_compare_avx2.cc index 5aea1613968..28e198a79be 100644 --- a/cpp/src/arrow/compute/exec/key_compare_avx2.cc +++ b/cpp/src/arrow/compute/exec/key_compare_avx2.cc @@ -35,15 +35,15 @@ uint32_t KeyCompare::CompareFixedLength_UpTo8B_avx2( constexpr uint32_t unroll = 4; for (uint32_t i = 0; i < num_rows / unroll; ++i) { - auto key_left = _mm256_i64gather_epi64( - reinterpret_cast(rows_left), offset_left, 1); + auto key_left = _mm256_i64gather_epi64(reinterpret_cast(rows_left), + offset_left, 1); offset_left = _mm256_add_epi64(offset_left, offset_left_incr); __m128i offset_right = _mm_loadu_si128(reinterpret_cast(left_to_right_map) + i); offset_right = _mm_mullo_epi32(offset_right, _mm_set1_epi32(length)); - auto key_right = _mm256_i32gather_epi64( - reinterpret_cast(rows_right), offset_right, 1); + auto key_right = _mm256_i32gather_epi64(reinterpret_cast(rows_right), + offset_right, 1); uint32_t cmp = _mm256_movemask_epi8(_mm256_cmpeq_epi64( _mm256_and_si256(key_left, mask), _mm256_and_si256(key_right, mask))); reinterpret_cast(match_bytevector)[i] &= cmp; diff --git a/cpp/src/arrow/compute/exec/key_hash_avx2.cc b/cpp/src/arrow/compute/exec/key_hash_avx2.cc index 0244ef81968..38ab48f7245 100644 --- a/cpp/src/arrow/compute/exec/key_hash_avx2.cc +++ b/cpp/src/arrow/compute/exec/key_hash_avx2.cc @@ -104,7 +104,7 @@ void Hashing::helper_tails_avx2(uint32_t num_keys, uint32_t key_length, const uint8_t* keys, uint32_t* hash) { constexpr int unroll = 8; ARROW_DCHECK(num_keys % unroll == 0); - auto keys_i64 = reinterpret_cast(keys); + auto keys_i64 = reinterpret_cast(keys); // Process between 1 and 8 last bytes of each key, starting from 16B boundary. // The caller needs to make sure that there are no more than 8 bytes to process after diff --git a/cpp/src/arrow/compute/exec/key_map_avx2.cc b/cpp/src/arrow/compute/exec/key_map_avx2.cc index a7cabc7491f..f78c5e179a0 100644 --- a/cpp/src/arrow/compute/exec/key_map_avx2.cc +++ b/cpp/src/arrow/compute/exec/key_map_avx2.cc @@ -70,7 +70,7 @@ void SwissTable::lookup_1_avx2_x8(const int num_hashes, const uint32_t* hashes, __m256i voffset_B = _mm256_srli_epi64(vblock_offset, 32); __m256i vstamp_B = _mm256_srli_epi64(vstamp, 32); - auto blocks_i64 = reinterpret_cast(blocks_); + auto blocks_i64 = reinterpret_cast(blocks_); auto vblock_A = _mm256_i64gather_epi64(blocks_i64, voffset_A, 1); auto vblock_B = _mm256_i64gather_epi64(blocks_i64, voffset_B, 1); __m256i vblock_highbits_A = From 4ca96afbae015ea7c5c5b41485d27756598bc6c4 Mon Sep 17 00:00:00 2001 From: michalursa Date: Wed, 19 May 2021 12:11:02 -0700 Subject: [PATCH 11/11] GrouperFastImpl: fixing more build errors plus a workaround for bigendian problem --- cpp/src/arrow/compute/exec/key_compare_avx2.cc | 8 ++++---- cpp/src/arrow/compute/exec/key_hash_avx2.cc | 2 +- cpp/src/arrow/compute/exec/key_map_avx2.cc | 2 +- cpp/src/arrow/compute/exec/util.h | 8 ++++++++ cpp/src/arrow/compute/kernels/hash_aggregate.cc | 4 ++++ 5 files changed, 18 insertions(+), 6 deletions(-) diff --git a/cpp/src/arrow/compute/exec/key_compare_avx2.cc b/cpp/src/arrow/compute/exec/key_compare_avx2.cc index 28e198a79be..6abdf6c3c3a 100644 --- a/cpp/src/arrow/compute/exec/key_compare_avx2.cc +++ b/cpp/src/arrow/compute/exec/key_compare_avx2.cc @@ -35,15 +35,15 @@ uint32_t KeyCompare::CompareFixedLength_UpTo8B_avx2( constexpr uint32_t unroll = 4; for (uint32_t i = 0; i < num_rows / unroll; ++i) { - auto key_left = _mm256_i64gather_epi64(reinterpret_cast(rows_left), - offset_left, 1); + auto key_left = _mm256_i64gather_epi64( + reinterpret_cast(rows_left), offset_left, 1); offset_left = _mm256_add_epi64(offset_left, offset_left_incr); __m128i offset_right = _mm_loadu_si128(reinterpret_cast(left_to_right_map) + i); offset_right = _mm_mullo_epi32(offset_right, _mm_set1_epi32(length)); - auto key_right = _mm256_i32gather_epi64(reinterpret_cast(rows_right), - offset_right, 1); + auto key_right = _mm256_i32gather_epi64( + reinterpret_cast(rows_right), offset_right, 1); uint32_t cmp = _mm256_movemask_epi8(_mm256_cmpeq_epi64( _mm256_and_si256(key_left, mask), _mm256_and_si256(key_right, mask))); reinterpret_cast(match_bytevector)[i] &= cmp; diff --git a/cpp/src/arrow/compute/exec/key_hash_avx2.cc b/cpp/src/arrow/compute/exec/key_hash_avx2.cc index 38ab48f7245..b58db015088 100644 --- a/cpp/src/arrow/compute/exec/key_hash_avx2.cc +++ b/cpp/src/arrow/compute/exec/key_hash_avx2.cc @@ -104,7 +104,7 @@ void Hashing::helper_tails_avx2(uint32_t num_keys, uint32_t key_length, const uint8_t* keys, uint32_t* hash) { constexpr int unroll = 8; ARROW_DCHECK(num_keys % unroll == 0); - auto keys_i64 = reinterpret_cast(keys); + auto keys_i64 = reinterpret_cast(keys); // Process between 1 and 8 last bytes of each key, starting from 16B boundary. // The caller needs to make sure that there are no more than 8 bytes to process after diff --git a/cpp/src/arrow/compute/exec/key_map_avx2.cc b/cpp/src/arrow/compute/exec/key_map_avx2.cc index f78c5e179a0..a2efb4d1bb9 100644 --- a/cpp/src/arrow/compute/exec/key_map_avx2.cc +++ b/cpp/src/arrow/compute/exec/key_map_avx2.cc @@ -70,7 +70,7 @@ void SwissTable::lookup_1_avx2_x8(const int num_hashes, const uint32_t* hashes, __m256i voffset_B = _mm256_srli_epi64(vblock_offset, 32); __m256i vstamp_B = _mm256_srli_epi64(vstamp, 32); - auto blocks_i64 = reinterpret_cast(blocks_); + auto blocks_i64 = reinterpret_cast(blocks_); auto vblock_A = _mm256_i64gather_epi64(blocks_i64, voffset_A, 1); auto vblock_B = _mm256_i64gather_epi64(blocks_i64, voffset_B, 1); __m256i vblock_highbits_A = diff --git a/cpp/src/arrow/compute/exec/util.h b/cpp/src/arrow/compute/exec/util.h index c0e94bcc6ac..d345bd3af0b 100644 --- a/cpp/src/arrow/compute/exec/util.h +++ b/cpp/src/arrow/compute/exec/util.h @@ -39,6 +39,14 @@ namespace arrow { namespace util { +// Some platforms typedef int64_t as long int instead of long long int, +// which breaks the _mm256_i64gather_epi64 and _mm256_i32gather_epi64 intrinsics +// which need long long. +// We use the cast to the type below in these intrinsics to make the code +// compile in all cases. +// +using int64_for_gather_t = const long long int; // NOLINT runtime-int + /// Storage used to allocate temporary vectors of a batch size. /// Temporary vectors should resemble allocating temporary variables on the stack /// but in the context of vectorized processing where we need to store a vector of diff --git a/cpp/src/arrow/compute/kernels/hash_aggregate.cc b/cpp/src/arrow/compute/kernels/hash_aggregate.cc index 7d3a883662a..0e5c8ace53f 100644 --- a/cpp/src/arrow/compute/kernels/hash_aggregate.cc +++ b/cpp/src/arrow/compute/kernels/hash_aggregate.cc @@ -443,6 +443,7 @@ struct GrouperImpl : Grouper { struct GrouperFastImpl : Grouper { static bool CanUse(const std::vector& keys) { +#if ARROW_LITTLE_ENDIAN for (size_t i = 0; i < keys.size(); ++i) { const auto& key = keys[i].type; if (is_large_binary_like(key->id())) { @@ -450,6 +451,9 @@ struct GrouperFastImpl : Grouper { } } return true; +#else + return false; +#endif } static Result> Make(