diff --git a/Framework/Core/include/Framework/ASoA.h b/Framework/Core/include/Framework/ASoA.h index 784a0796f86fe..6ff77f8facaec 100644 --- a/Framework/Core/include/Framework/ASoA.h +++ b/Framework/Core/include/Framework/ASoA.h @@ -1517,6 +1517,11 @@ struct PreslicePolicySorted : public PreslicePolicyBase { SliceInfoPtr sliceInfo; std::shared_ptr getSliceFor(int value, std::shared_ptr const& input, uint64_t& offset) const; + // One-slot cache for the empty (0-row) slice, so that empty groups do not + // slice every column only to produce 0 rows (the common case for sparse + // grouping, e.g. candidates per collision). Keyed by the input table, which + // changes with every dataframe. + mutable std::pair> emptySlice{nullptr, nullptr}; }; struct PreslicePolicyGeneral : public PreslicePolicyBase { @@ -1731,7 +1736,10 @@ auto doSliceByCached(T const* table, framework::expressions::BindingNode const& auto localCache = cache.ptr->getCacheFor({"", originReplacement(cache.ptr->newOrigin)(o2::soa::getMatcherFromTypeForKey(node.name)), node.name}); auto [offset, count] = localCache.getSliceFor(value); - auto t = typename T::self_t({table->asArrowTable()->Slice(static_cast(offset), count)}, static_cast(offset)); + // Empty group: reuse a cached empty (0-row) table instead of slicing every column. + auto slice = count == 0 ? cache.ptr->getEmptySliceFor(table->asArrowTable()) + : table->asArrowTable()->Slice(static_cast(offset), count); + auto t = typename T::self_t({slice}, static_cast(offset)); if (t.tableSize() != 0) { table->copyIndexBindings(t); } @@ -1744,7 +1752,9 @@ auto doFilteredSliceByCached(T const* table, framework::expressions::BindingNode auto localCache = cache.ptr->getCacheFor({"", originReplacement(cache.ptr->newOrigin)(o2::soa::getMatcherFromTypeForKey(node.name)), node.name}); auto [offset, count] = localCache.getSliceFor(value); - auto slice = table->asArrowTable()->Slice(static_cast(offset), count); + // Empty group: reuse a cached empty (0-row) table instead of slicing every column. + auto slice = count == 0 ? cache.ptr->getEmptySliceFor(table->asArrowTable()) + : table->asArrowTable()->Slice(static_cast(offset), count); return prepareFilteredSlice(table, slice, offset); } diff --git a/Framework/Core/include/Framework/ArrowTableSlicingCache.h b/Framework/Core/include/Framework/ArrowTableSlicingCache.h index b7cd1df2a74c6..9b27480024674 100644 --- a/Framework/Core/include/Framework/ArrowTableSlicingCache.h +++ b/Framework/Core/include/Framework/ArrowTableSlicingCache.h @@ -107,6 +107,12 @@ struct ArrowTableSlicingCache { SliceInfoPtr getCacheForPos(int pos) const; SliceInfoUnsortedPtr getCacheUnsortedForPos(int pos) const; + // get a cached empty (0-row) slice of the given table, so that empty groups + // do not slice every column only to produce 0 rows (the common case for + // sparse grouping). One-slot cache keyed by the table pointer. + std::shared_ptr getEmptySliceFor(std::shared_ptr const& table); + std::pair> emptySlice{nullptr, nullptr}; + static void validateOrder(Entry const& bindingKey, std::shared_ptr const& input); }; } // namespace o2::framework diff --git a/Framework/Core/include/Framework/GroupSlicer.h b/Framework/Core/include/Framework/GroupSlicer.h index e3e602787ec15..74e5f16c1703f 100644 --- a/Framework/Core/include/Framework/GroupSlicer.h +++ b/Framework/Core/include/Framework/GroupSlicer.h @@ -218,10 +218,16 @@ struct GroupSlicer { auto oc = sliceInfos[index].getSliceFor(pos); uint64_t offset = oc.first; auto count = oc.second; - auto groupedElementsTable = originalTable.asArrowTable()->Slice(offset, count); if (count == 0) { - return std::decay_t{{groupedElementsTable}, soa::SelectionVector{}}; + // Empty group: avoid slicing every column only to discard it. Cache one + // empty (0-row) table per associated table and reuse it. This is the + // common case for sparse grouping (e.g. collisions with no candidates). + if (!emptyTables[index]) { + emptyTables[index] = originalTable.asArrowTable()->Slice(0, 0); + } + return std::decay_t{{emptyTables[index]}, soa::SelectionVector{}}; } + auto groupedElementsTable = originalTable.asArrowTable()->Slice(offset, count); // for each grouping element we need to slice the selection vector auto start_iterator = std::lower_bound(starts[index], selections[index]->end(), offset); @@ -275,6 +281,9 @@ struct GroupSlicer { std::span groupSelection; std::array const*, sizeof...(A)> selections; std::array::iterator, sizeof...(A)> starts; + // Cached empty (0-row) table per associated table, lazily built and reused + // for empty groups so we do not slice every column on each empty group. + std::array, sizeof...(A)> emptyTables{}; std::array sliceInfos; std::array sliceInfosUnsorted; diff --git a/Framework/Core/src/ASoA.cxx b/Framework/Core/src/ASoA.cxx index 29acfc4b221e0..f565fa6e9ce47 100644 --- a/Framework/Core/src/ASoA.cxx +++ b/Framework/Core/src/ASoA.cxx @@ -317,9 +317,16 @@ void PreslicePolicyGeneral::updateSliceInfo(SliceInfoUnsortedPtr&& si) std::shared_ptr PreslicePolicySorted::getSliceFor(int value, std::shared_ptr const& input, uint64_t& offset) const { auto [offset_, count] = this->sliceInfo.getSliceFor(value); - auto output = input->Slice(offset_, count); offset = static_cast(offset_); - return output; + if (count == 0) { + // Empty group: avoid slicing every column only to discard it. Cache one + // empty (0-row) table per input table and reuse it (see GroupSlicer). + if (emptySlice.first != input.get()) { + emptySlice = {input.get(), input->Slice(0, 0)}; + } + return emptySlice.second; + } + return input->Slice(offset_, count); } std::span PreslicePolicyGeneral::getSliceFor(int value) const diff --git a/Framework/Core/src/ArrowTableSlicingCache.cxx b/Framework/Core/src/ArrowTableSlicingCache.cxx index a3cb755f158ef..e7bfdabe4344a 100644 --- a/Framework/Core/src/ArrowTableSlicingCache.cxx +++ b/Framework/Core/src/ArrowTableSlicingCache.cxx @@ -269,6 +269,14 @@ SliceInfoUnsortedPtr ArrowTableSlicingCache::getCacheUnsortedForPos(int pos) con }; } +std::shared_ptr ArrowTableSlicingCache::getEmptySliceFor(std::shared_ptr const& table) +{ + if (emptySlice.first != table.get()) { + emptySlice = {table.get(), table->Slice(0, 0)}; + } + return emptySlice.second; +} + void ArrowTableSlicingCache::validateOrder(Entry const& bindingKey, const std::shared_ptr& input) { auto const& [target, matcher, key, enabled] = bindingKey;