Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 4.3k
GH-31769: [C++][Acero] Add spilling for hash join#13669
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Uh oh!
There was an error while loading. Please reload this page.
Closed
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
9e683a4
Implement spilling for Hash Join
save-buffer 52df6bf
Make my poor code completely unreadable
save-buffer d8291d3
Some win32 fixes
save-buffer 12f3b5b
Fix more windows errors
save-buffer 5cb8c50
Respond to Weston comments
save-buffer 98a912a
ARROW_EXPORT some stuff to hopefully fix windows
save-buffer d47fe5b
More windows nonsense
save-buffer 81708bd
Change number of tests to see if it passes CI
save-buffer File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Jump to file
Failed to load files.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -21,16 +21,19 @@ | ||
| #include <vector> | ||
| #include "arrow/compute/exec.h" | ||
| #include "arrow/compute/exec/partition_util.h" | ||
| #include "arrow/compute/exec/spilling_util.h" | ||
| #include "arrow/compute/exec/task_util.h" | ||
| #include "arrow/compute/light_array.h" | ||
| namespace arrow { | ||
| namespace util { | ||
| using arrow::compute::ExecBatch; | ||
| namespace compute { | ||
| /// \brief A container that accumulates batches until they are ready to | ||
| /// be processed. | ||
| class AccumulationQueue { | ||
| class ARROW_EXPORT AccumulationQueue { | ||
| public: | ||
| AccumulationQueue() : row_count_(0) {} | ||
| AccumulationQueue() = default; | ||
| ~AccumulationQueue() = default; | ||
| // We should never be copying ExecBatch around | ||
| @@ -42,16 +45,128 @@ class AccumulationQueue { | ||
| void Concatenate(AccumulationQueue&& that); | ||
| void InsertBatch(ExecBatch batch); | ||
| int64_t row_count() { return row_count_; } | ||
| size_t batch_count() { return batches_.size(); } | ||
| void SetBatch(size_t idx, ExecBatch batch); | ||
| size_t batch_count() const { return batches_.size(); } | ||
| bool empty() const { return batches_.empty(); } | ||
| size_t CalculateRowCount() const; | ||
| // Resizes the accumulation queue to contain size batches. The | ||
| // new batches will be empty and have length 0, but they will be | ||
| // usable (useful for concurrent modification of the AccumulationQueue | ||
| // of separate elements). | ||
| void Resize(size_t size) { batches_.resize(size); } | ||
| void Clear(); | ||
| ExecBatch& operator[](size_t i); | ||
| ExecBatch& operator[](size_t i) { return batches_[i]; } | ||
| const ExecBatch& operator[](size_t i) const { return batches_[i]; } | ||
| private: | ||
| int64_t row_count_; | ||
| std::vector<ExecBatch> batches_; | ||
| }; | ||
| } // namespace util | ||
| /// Accumulates batches in a queue that can be spilled to disk if needed | ||
| /// | ||
| /// Each batch is partitioned by the lower bits of the hash column (which must be present) | ||
| /// and rows are initially accumulated in batch builders (one per partition). As a batch | ||
| /// builder fills up the completed batch is put into an in-memory accumulation queue (per | ||
| /// partition). | ||
| /// | ||
| /// When memory pressure is encountered the spilling queue's "spill cursor" can be | ||
| /// advanced. This will cause a partition to be spilled to disk. Any future data | ||
| /// arriving for that partition will go immediately to disk (after accumulating a full | ||
| /// batch in the batch builder). Note that hashes are spilled separately from batches and | ||
| /// have their own cursor. We assume that the Batch cursor is advanced faster than the | ||
| /// spill cursor. Hashes are spilled separately to enable building a Bloom filter for | ||
| /// spilled partitions. | ||
| /// | ||
| /// Later, data is retrieved one partition at a time. Partitions that are in-memory will | ||
| /// be delivered immediately in new thread tasks. Partitions that are on disk will be | ||
| /// read from disk and delivered as they arrive. | ||
| /// | ||
| /// This class assumes that data is fully accumulated before it is read-back. As such, do | ||
| /// not call InsertBatch after calling GetPartition. | ||
| class ARROW_EXPORT SpillingAccumulationQueue { | ||
| public: | ||
| // Number of partitions must be a power of two, since we assign partitions by | ||
| // looking at bottom few bits. | ||
| static constexpr int kLogNumPartitions = 6; | ||
| static constexpr int kNumPartitions = 1 << kLogNumPartitions; | ||
| Status Init(QueryContext* ctx); | ||
| // Assumes that the final column in batch contains 64-bit hashes of the columns. | ||
| Status InsertBatch(size_t thread_index, ExecBatch batch); | ||
| // Runs `on_batch` on each batch in the SpillingAccumulationQueue for the given | ||
| // partition. Each batch will have its own task. Once all batches have had their | ||
| // on_batch function run, `on_finished` will be called. | ||
| Status GetPartition(size_t thread_index, size_t partition_idx, | ||
| std::function<Status(size_t, size_t, ExecBatch)> | ||
| on_batch, // thread_index, batch_index, batch | ||
| std::function<Status(size_t)> on_finished); | ||
| // Returns hashes of the given partition and batch index. | ||
| // partition MUST be at least hash_cursor, as if partition < hash_cursor, | ||
| // these hashes will have been deleted. | ||
| const uint64_t* GetHashes(size_t partition_idx, size_t batch_idx); | ||
| inline size_t batch_count(size_t partition_idx) const { | ||
| const Partition& partition = partitions_[partition_idx]; | ||
| size_t num_full_batches = partition_idx >= spilling_cursor_ | ||
| ? partition.queue.batch_count() | ||
| : partition.file.num_batches(); | ||
| return num_full_batches + (partition.builder.num_rows() > 0); | ||
| } | ||
| inline size_t row_count(size_t partition_idx, size_t batch_idx) const { | ||
| const Partition& partition = partitions_[partition_idx]; | ||
| if (batch_idx < partition.hash_queue.batch_count()) | ||
| return partition.hash_queue[batch_idx].length; | ||
| else | ||
| return partition.builder.num_rows(); | ||
| } | ||
| static inline constexpr size_t partition_id(uint64_t hash) { | ||
| // Hash Table uses the top bits of the hash, so it is important | ||
| // to use the bottom bits of the hash for spilling to avoid | ||
| // a huge number of hash collisions per partition. | ||
| return static_cast<size_t>(hash & (kNumPartitions - 1)); | ||
| } | ||
| // Returns the row count for the partition if it is still in-memory. | ||
| // Returns 0 if the partition has already been spilled. | ||
| size_t CalculatePartitionRowCount(size_t partition) const; | ||
save-buffer marked this conversation as resolved.
Outdated
Uh oh!There was an error while loading. Please reload this page. | ||
| // Spills the next partition of batches to disk and returns true, | ||
| // or returns false if too many partitions have been spilled. | ||
| // The QueryContext's bytes_in_flight will be increased by the | ||
| // number of bytes spilled (unless the disk IO was very fast and | ||
| // the bytes_in_flight got reduced again). | ||
| // | ||
| // We expect that we always advance the SpillCursor faster than the | ||
| // HashCursor, and only advance the HashCursor when we've exhausted | ||
| // partitions for the SpillCursor. | ||
| Result<bool> AdvanceSpillCursor(); | ||
| // Same as AdvanceSpillCursor but spills the hashes for the partition. | ||
| Result<bool> AdvanceHashCursor(); | ||
| inline size_t spill_cursor() const { return spilling_cursor_.load(); } | ||
| inline size_t hash_cursor() const { return hash_cursor_.load(); } | ||
| private: | ||
| std::atomic<size_t> spilling_cursor_{0}; // denotes the first in-memory partition | ||
| std::atomic<size_t> hash_cursor_{0}; | ||
| QueryContext* ctx_; | ||
| PartitionLocks partition_locks_; | ||
| struct Partition { | ||
| AccumulationQueue queue; | ||
| AccumulationQueue hash_queue; | ||
| ExecBatchBuilder builder; | ||
| SpillFile file; | ||
| int task_group_read; | ||
| std::function<Status(size_t, size_t, ExecBatch)> read_back_fn; | ||
| std::function<Status(size_t)> on_finished; | ||
| }; | ||
| Partition partitions_[kNumPartitions]; | ||
| }; | ||
| } // namespace compute | ||
| } // namespace arrow | ||
Oops, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.