From 7662b7faa9c5d27201ecaca2196269f3cb0ce89e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20Stojko?= Date: Thu, 20 Aug 2026 13:25:41 +0000 Subject: [PATCH 1/3] Add update_theta_sketch::get_result() to trim to k in one pass Returns a compact_theta_sketch bounded to the nominal size k (2^lg_k), matching the at-most-k guarantee of theta_union::get_result(). It does the nth_element/erase cutback directly on the output vector compact() already allocates, avoiding the throwaway 2k rehash of trim()+compact(). Co-authored-by: Isaac --- theta/include/theta_sketch.hpp | 8 ++++++ theta/include/theta_sketch_impl.hpp | 20 +++++++++++++ theta/test/theta_sketch_test.cpp | 44 +++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+) diff --git a/theta/include/theta_sketch.hpp b/theta/include/theta_sketch.hpp index 4aab4b92..218d0939 100644 --- a/theta/include/theta_sketch.hpp +++ b/theta/include/theta_sketch.hpp @@ -336,6 +336,13 @@ class update_theta_sketch_alloc: public theta_sketch_alloc { */ compact_theta_sketch_alloc compact(bool ordered = true) const; + /** + * Produces a compact sketch trimmed to the nominal size k in a single pass. + * Like trim() followed by compact(), but without rebuilding the hash table. Result is unordered. + * @return compact sketch with at most k retained entries + */ + compact_theta_sketch_alloc get_result() const; + virtual iterator begin(); virtual iterator end(); virtual const_iterator begin() const; @@ -518,6 +525,7 @@ class compact_theta_sketch_alloc: public theta_sketch_alloc { template friend class theta_union_base; template friend class theta_intersection_base; template friend class theta_set_difference_base; + template friend class update_theta_sketch_alloc; compact_theta_sketch_alloc(bool is_empty, bool is_ordered, uint16_t seed_hash, uint64_t theta, std::vector&& entries); }; diff --git a/theta/include/theta_sketch_impl.hpp b/theta/include/theta_sketch_impl.hpp index 304ae64c..8082354f 100644 --- a/theta/include/theta_sketch_impl.hpp +++ b/theta/include/theta_sketch_impl.hpp @@ -23,6 +23,7 @@ #include #include #include +#include #include "binomial_bounds.hpp" #include "theta_helpers.hpp" @@ -243,6 +244,25 @@ compact_theta_sketch_alloc update_theta_sketch_alloc::compact(bool ordered return compact_theta_sketch_alloc(*this, ordered); } +template +compact_theta_sketch_alloc update_theta_sketch_alloc::get_result() const { + std::vector entries(table_.allocator_); + if (is_empty()) { + return compact_theta_sketch_alloc(true, true, get_seed_hash(), get_theta64(), std::move(entries)); + } + entries.reserve(get_num_retained()); + std::copy(begin(), end(), std::back_inserter(entries)); + uint64_t theta = table_.theta_; + const uint32_t nominal_num = 1 << table_.lg_nom_size_; + if (entries.size() > nominal_num) { + std::nth_element(entries.begin(), entries.begin() + nominal_num, entries.end()); + theta = entries[nominal_num]; + entries.erase(entries.begin() + nominal_num, entries.end()); + } + const bool ordered = entries.size() <= 1; + return compact_theta_sketch_alloc(false, ordered, get_seed_hash(), theta, std::move(entries)); +} + template void update_theta_sketch_alloc::print_specifics(std::ostringstream& os) const { os << " lg nominal size : " << static_cast(table_.lg_nom_size_) << std::endl; diff --git a/theta/test/theta_sketch_test.cpp b/theta/test/theta_sketch_test.cpp index 97c4f14e..25faf964 100644 --- a/theta/test/theta_sketch_test.cpp +++ b/theta/test/theta_sketch_test.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -167,6 +168,49 @@ TEST_CASE("theta sketch: estimation", "[theta_sketch]") { REQUIRE(compact_sketch.get_upper_bound(1) > n); } +TEST_CASE("theta sketch: get_result trims to k in one pass", "[theta_sketch]") { + update_theta_sketch update_sketch = update_theta_sketch::builder().build(); + const int n = 8000; + for (int i = 0; i < n; i++) update_sketch.update(i); + const uint32_t k = 1 << theta_constants::DEFAULT_LG_K; + REQUIRE(update_sketch.get_num_retained() > k); // over-provisioned before trimming + + compact_theta_sketch result = update_sketch.get_result(); + REQUIRE_FALSE(result.is_empty()); + REQUIRE(result.is_estimation_mode()); + REQUIRE(result.get_num_retained() == k); // trimmed to nominal size + REQUIRE_FALSE(result.is_ordered()); // unordered: no sort performed + + // fused get_result() must match trim() + compact() + update_theta_sketch trimmed = update_sketch; + trimmed.trim(); + compact_theta_sketch expected = trimmed.compact(false); + REQUIRE(result.get_theta64() == expected.get_theta64()); + REQUIRE(result.get_num_retained() == expected.get_num_retained()); + REQUIRE(result.get_estimate() == expected.get_estimate()); + + // same set of retained hashes (order-independent) + std::vector a(result.begin(), result.end()); + std::vector b(expected.begin(), expected.end()); + std::sort(a.begin(), a.end()); + std::sort(b.begin(), b.end()); + REQUIRE(a == b); +} + +TEST_CASE("theta sketch: get_result on empty and below-k sketches", "[theta_sketch]") { + compact_theta_sketch empty_result = update_theta_sketch::builder().build().get_result(); + REQUIRE(empty_result.is_empty()); + REQUIRE(empty_result.get_num_retained() == 0); + + update_theta_sketch small = update_theta_sketch::builder().build(); + for (int i = 0; i < 100; i++) small.update(i); + REQUIRE_FALSE(small.is_estimation_mode()); + compact_theta_sketch small_result = small.get_result(); + REQUIRE_FALSE(small_result.is_estimation_mode()); + REQUIRE(small_result.get_num_retained() == 100); // below k: nothing trimmed + REQUIRE(small_result.get_estimate() == Approx(100.0)); +} + TEST_CASE("theta sketch: deserialize compact v1 empty from java", "[theta_sketch]") { std::ifstream is; is.exceptions(std::ios::failbit | std::ios::badbit); From c174f89fe2309313473463f976a59c672a532c34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20Stojko?= Date: Thu, 20 Aug 2026 13:42:25 +0000 Subject: [PATCH 2/3] Add ordered parameter to update_theta_sketch::get_result() Matches the ordered = true parameter of theta_union::get_result() and theta_intersection::get_result(); sorts only when ordered is requested. Co-authored-by: Isaac --- theta/include/theta_sketch.hpp | 5 +-- theta/include/theta_sketch_impl.hpp | 4 +-- theta/test/theta_sketch_test.cpp | 50 ++++++++++++++++++----------- 3 files changed, 36 insertions(+), 23 deletions(-) diff --git a/theta/include/theta_sketch.hpp b/theta/include/theta_sketch.hpp index 218d0939..494df894 100644 --- a/theta/include/theta_sketch.hpp +++ b/theta/include/theta_sketch.hpp @@ -338,10 +338,11 @@ class update_theta_sketch_alloc: public theta_sketch_alloc { /** * Produces a compact sketch trimmed to the nominal size k in a single pass. - * Like trim() followed by compact(), but without rebuilding the hash table. Result is unordered. + * Like trim() followed by compact(), but without rebuilding the hash table. + * @param ordered optional flag to specify if an ordered sketch should be produced * @return compact sketch with at most k retained entries */ - compact_theta_sketch_alloc get_result() const; + compact_theta_sketch_alloc get_result(bool ordered = true) const; virtual iterator begin(); virtual iterator end(); diff --git a/theta/include/theta_sketch_impl.hpp b/theta/include/theta_sketch_impl.hpp index 8082354f..5ad971bb 100644 --- a/theta/include/theta_sketch_impl.hpp +++ b/theta/include/theta_sketch_impl.hpp @@ -245,7 +245,7 @@ compact_theta_sketch_alloc update_theta_sketch_alloc::compact(bool ordered } template -compact_theta_sketch_alloc update_theta_sketch_alloc::get_result() const { +compact_theta_sketch_alloc update_theta_sketch_alloc::get_result(bool ordered) const { std::vector entries(table_.allocator_); if (is_empty()) { return compact_theta_sketch_alloc(true, true, get_seed_hash(), get_theta64(), std::move(entries)); @@ -259,7 +259,7 @@ compact_theta_sketch_alloc update_theta_sketch_alloc::get_result() const { theta = entries[nominal_num]; entries.erase(entries.begin() + nominal_num, entries.end()); } - const bool ordered = entries.size() <= 1; + if (ordered) std::sort(entries.begin(), entries.end()); return compact_theta_sketch_alloc(false, ordered, get_seed_hash(), theta, std::move(entries)); } diff --git a/theta/test/theta_sketch_test.cpp b/theta/test/theta_sketch_test.cpp index 25faf964..3077c7b5 100644 --- a/theta/test/theta_sketch_test.cpp +++ b/theta/test/theta_sketch_test.cpp @@ -175,40 +175,52 @@ TEST_CASE("theta sketch: get_result trims to k in one pass", "[theta_sketch]") { const uint32_t k = 1 << theta_constants::DEFAULT_LG_K; REQUIRE(update_sketch.get_num_retained() > k); // over-provisioned before trimming - compact_theta_sketch result = update_sketch.get_result(); - REQUIRE_FALSE(result.is_empty()); - REQUIRE(result.is_estimation_mode()); - REQUIRE(result.get_num_retained() == k); // trimmed to nominal size - REQUIRE_FALSE(result.is_ordered()); // unordered: no sort performed - - // fused get_result() must match trim() + compact() + // default is ordered, matching union/intersection get_result + compact_theta_sketch ordered_result = update_sketch.get_result(); + REQUIRE_FALSE(ordered_result.is_empty()); + REQUIRE(ordered_result.is_estimation_mode()); + REQUIRE(ordered_result.get_num_retained() == k); // trimmed to nominal size + REQUIRE(ordered_result.is_ordered()); + REQUIRE(std::is_sorted(ordered_result.begin(), ordered_result.end())); + + // fused get_result(true) must match trim() + compact(true) update_theta_sketch trimmed = update_sketch; trimmed.trim(); - compact_theta_sketch expected = trimmed.compact(false); - REQUIRE(result.get_theta64() == expected.get_theta64()); - REQUIRE(result.get_num_retained() == expected.get_num_retained()); - REQUIRE(result.get_estimate() == expected.get_estimate()); - - // same set of retained hashes (order-independent) - std::vector a(result.begin(), result.end()); - std::vector b(expected.begin(), expected.end()); - std::sort(a.begin(), a.end()); - std::sort(b.begin(), b.end()); - REQUIRE(a == b); + compact_theta_sketch expected = trimmed.compact(true); + REQUIRE(ordered_result.get_theta64() == expected.get_theta64()); + REQUIRE(ordered_result.get_num_retained() == expected.get_num_retained()); + REQUIRE(ordered_result.get_estimate() == expected.get_estimate()); + REQUIRE(std::vector(ordered_result.begin(), ordered_result.end()) + == std::vector(expected.begin(), expected.end())); + + // unordered variant: same trimmed set and theta, no sort + compact_theta_sketch unordered_result = update_sketch.get_result(false); + REQUIRE_FALSE(unordered_result.is_ordered()); + REQUIRE(unordered_result.get_num_retained() == k); + REQUIRE(unordered_result.get_theta64() == expected.get_theta64()); + std::vector unordered_hashes(unordered_result.begin(), unordered_result.end()); + std::sort(unordered_hashes.begin(), unordered_hashes.end()); + REQUIRE(unordered_hashes == std::vector(expected.begin(), expected.end())); } TEST_CASE("theta sketch: get_result on empty and below-k sketches", "[theta_sketch]") { compact_theta_sketch empty_result = update_theta_sketch::builder().build().get_result(); REQUIRE(empty_result.is_empty()); REQUIRE(empty_result.get_num_retained() == 0); + REQUIRE(empty_result.is_ordered()); update_theta_sketch small = update_theta_sketch::builder().build(); for (int i = 0; i < 100; i++) small.update(i); REQUIRE_FALSE(small.is_estimation_mode()); - compact_theta_sketch small_result = small.get_result(); + + compact_theta_sketch small_result = small.get_result(); // default ordered REQUIRE_FALSE(small_result.is_estimation_mode()); REQUIRE(small_result.get_num_retained() == 100); // below k: nothing trimmed REQUIRE(small_result.get_estimate() == Approx(100.0)); + REQUIRE(small_result.is_ordered()); + REQUIRE(std::is_sorted(small_result.begin(), small_result.end())); + + REQUIRE_FALSE(small.get_result(false).is_ordered()); // unordered variant } TEST_CASE("theta sketch: deserialize compact v1 empty from java", "[theta_sketch]") { From 314f4acdc53b30e55d46f4e9589837f89a8949d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20Stojko?= Date: Sat, 22 Aug 2026 13:12:23 +0000 Subject: [PATCH 3/3] Document get_result() naming rationale in docstring Co-authored-by: Isaac --- theta/include/theta_sketch.hpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/theta/include/theta_sketch.hpp b/theta/include/theta_sketch.hpp index 494df894..297b675a 100644 --- a/theta/include/theta_sketch.hpp +++ b/theta/include/theta_sketch.hpp @@ -339,6 +339,8 @@ class update_theta_sketch_alloc: public theta_sketch_alloc { /** * Produces a compact sketch trimmed to the nominal size k in a single pass. * Like trim() followed by compact(), but without rebuilding the hash table. + * Named get_result() to parallel theta_union::get_result() and + * theta_intersection::get_result(), which likewise return an already-trimmed result. * @param ordered optional flag to specify if an ordered sketch should be produced * @return compact sketch with at most k retained entries */