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-45201: [C++][Parquet] Improve performance of generating size statistics#45202
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
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -18,13 +18,27 @@ | ||
| #include "parquet/size_statistics.h" | ||
| #include <algorithm> | ||
| #include <numeric> | ||
| #include <ostream> | ||
| #include <string_view> | ||
| #include "arrow/util/logging.h" | ||
| #include "parquet/exception.h" | ||
| #include "parquet/schema.h" | ||
| namespace parquet { | ||
| namespace { | ||
| void MergeLevelHistogram(::arrow::util::span<int64_t> histogram, | ||
| ::arrow::util::span<const int64_t> other) { | ||
| ARROW_DCHECK_EQ(histogram.size(), other.size()); | ||
| std::transform(histogram.begin(), histogram.end(), other.begin(), histogram.begin(), | ||
| std::plus<>()); | ||
| } | ||
| } // namespace | ||
| void SizeStatistics::Merge(const SizeStatistics& other) { | ||
| if (repetition_level_histogram.size() != other.repetition_level_histogram.size()) { | ||
| throw ParquetException("Repetition level histogram size mismatch"); | ||
| @@ -36,12 +50,8 @@ void SizeStatistics::Merge(const SizeStatistics& other) { | ||
| other.unencoded_byte_array_data_bytes.has_value()) { | ||
| throw ParquetException("Unencoded byte array data bytes are not consistent"); | ||
| } | ||
| std::transform(repetition_level_histogram.begin(), repetition_level_histogram.end(), | ||
| other.repetition_level_histogram.begin(), | ||
| repetition_level_histogram.begin(), std::plus<>()); | ||
| std::transform(definition_level_histogram.begin(), definition_level_histogram.end(), | ||
| other.definition_level_histogram.begin(), | ||
| definition_level_histogram.begin(), std::plus<>()); | ||
| MergeLevelHistogram(repetition_level_histogram, other.repetition_level_histogram); | ||
| MergeLevelHistogram(definition_level_histogram, other.definition_level_histogram); | ||
| if (unencoded_byte_array_data_bytes.has_value()) { | ||
| unencoded_byte_array_data_bytes = unencoded_byte_array_data_bytes.value() + | ||
| other.unencoded_byte_array_data_bytes.value(); | ||
| @@ -91,4 +101,88 @@ std::unique_ptr<SizeStatistics> SizeStatistics::Make(const ColumnDescriptor* des | ||
| return size_stats; | ||
| } | ||
| std::ostream& operator<<(std::ostream& os, const SizeStatistics& size_stats) { | ||
| constexpr std::string_view kComma = ", "; | ||
| os << "SizeStatistics{"; | ||
| std::string_view sep = ""; | ||
| if (size_stats.unencoded_byte_array_data_bytes.has_value()) { | ||
| os << "unencoded_byte_array_data_bytes=" | ||
| << *size_stats.unencoded_byte_array_data_bytes; | ||
| sep = kComma; | ||
| } | ||
| auto print_histogram = [&](std::string_view name, | ||
| const std::vector<int64_t>& histogram) { | ||
| if (!histogram.empty()) { | ||
| os << sep << name << "={"; | ||
| sep = kComma; | ||
| std::string_view value_sep = ""; | ||
| for (int64_t v : histogram) { | ||
| os << value_sep << v; | ||
| value_sep = kComma; | ||
| } | ||
| os << "}"; | ||
| } | ||
| }; | ||
| print_histogram("repetition_level_histogram", size_stats.repetition_level_histogram); | ||
| print_histogram("definition_level_histogram", size_stats.definition_level_histogram); | ||
| os << "}"; | ||
| return os; | ||
| } | ||
| void UpdateLevelHistogram(::arrow::util::span<const int16_t> levels, | ||
| ::arrow::util::span<int64_t> histogram) { | ||
| const int64_t num_levels = static_cast<int64_t>(levels.size()); | ||
| DCHECK_GE(histogram.size(), 1); | ||
| const int16_t max_level = static_cast<int16_t>(histogram.size() - 1); | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (Should we MemberAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could! | ||
| if (max_level == 0) { | ||
| histogram[0] += num_levels; | ||
| return; | ||
| } | ||
| #ifndef NDEBUG | ||
| for (auto level : levels) { | ||
| ARROW_DCHECK_LE(level, max_level); | ||
| } | ||
| #endif | ||
| if (max_level == 1) { | ||
| // Specialize the common case for non-repeated non-nested columns. | ||
| // Summing the levels gives us the number of 1s, and the number of 0s follows. | ||
| // We do repeated sums in the int16_t space, which the compiler is likely | ||
| // to vectorize efficiently. | ||
| constexpr int64_t kChunkSize = 1 << 14; // to avoid int16_t overflows | ||
| int64_t hist1 = 0; | ||
| auto it = levels.begin(); | ||
| while (it != levels.end()) { | ||
| const auto chunk_size = std::min<int64_t>(levels.end() - it, kChunkSize); | ||
| hist1 += std::accumulate(levels.begin(), levels.begin() + chunk_size, int16_t{0}); | ||
| it += chunk_size; | ||
| } | ||
| histogram[0] += num_levels - hist1; | ||
| histogram[1] += hist1; | ||
| return; | ||
| } | ||
| // The generic implementation issues a series of histogram load-stores. | ||
| // However, it limits store-to-load dependencies by interleaving partial histogram | ||
| // updates. | ||
| constexpr int kUnroll = 4; | ||
| std::array<std::vector<int64_t>, kUnroll> partial_hist; | ||
| for (auto& hist : partial_hist) { | ||
| hist.assign(histogram.size(), 0); | ||
| } | ||
| int64_t i = 0; | ||
| for (; i <= num_levels - kUnroll; i += kUnroll) { | ||
| for (int j = 0; j < kUnroll; ++j) { | ||
| ++partial_hist[j][levels[i + j]]; | ||
| } | ||
| } | ||
| for (; i < num_levels; ++i) { | ||
| ++partial_hist[0][levels[i]]; | ||
| } | ||
| for (const auto& hist : partial_hist) { | ||
| MergeLevelHistogram(histogram, hist); | ||
| } | ||
| } | ||
| } // namespace parquet | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't fully understand why scaning a pass of
def_levelsandrep_levelsfirst would maximize cache efficiency, wouldWriteDefinitionLevelsfirst also update the L1D cache?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure and I don't see any difference on our mini-benchmark. But
WriteDefinitionLevelsalso writes to a destination buffer, so perhaps some of the levels will be evicted from the CPU cache.