Skip to content

Commit f2728bf

Browse files
shanielhclaude
andcommitted
Add weighted update to kll_sketch
update(item, weight) is equivalent to calling update(item) weight times. A weight that fits into the free space of level zero is applied as plain updates; a larger one builds an exact sketch holding one copy of the item at each level whose bit is set in the weight and merges it, so the cost is logarithmic in the weight. This mirrors the weighted update in datasketches-java. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent c761c5d commit f2728bf

3 files changed

Lines changed: 180 additions & 0 deletions

File tree

‎kll/include/kll_sketch.hpp‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,18 @@ class kll_sketch {
237237
template<typename FwdT>
238238
void update(FwdT&& item);
239239

240+
/**
241+
* Updates this sketch with the given data item repeated the given number of times.
242+
* The result is equivalent to calling update(item) weight times, at a cost that grows
243+
* with the logarithm of the weight rather than with the weight itself.
244+
* If cross-language portability is required, callers should ensure that
245+
* the input string uses a compatible encoding (valid UTF-8).
246+
* @param item from a stream of items
247+
* @param weight number of times the item is repeated, must be positive
248+
*/
249+
template<typename FwdT>
250+
void update(FwdT&& item, uint64_t weight);
251+
240252
/**
241253
* Merges another sketch into this one.
242254
* If sketches contain strings, callers are responsible for ensuring that
@@ -570,6 +582,9 @@ class kll_sketch {
570582
std::unique_ptr<T, items_deleter> items, uint32_t items_size, optional<T>&& min_item,
571583
optional<T>&& max_item, bool is_level_zero_sorted, const C& comparator);
572584

585+
// for weighted update
586+
kll_sketch(uint16_t k, const T& item, uint64_t weight, const C& comparator, const A& allocator);
587+
573588
// common update code
574589
inline void update_min_max(const T& item);
575590
inline uint32_t internal_update();

‎kll/include/kll_sketch_impl.hpp‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,51 @@ void kll_sketch<T, C, A>::update(FwdT&& item) {
186186
reset_sorted_view();
187187
}
188188

189+
template<typename T, typename C, typename A>
190+
template<typename FwdT>
191+
void kll_sketch<T, C, A>::update(FwdT&& item, uint64_t weight) {
192+
if (weight == 0) throw std::invalid_argument("weight must be positive");
193+
if (!check_update_item(item)) { return; }
194+
if (weight < levels_[0]) { // fits into level zero without compaction
195+
update_min_max(static_cast<const T&>(item));
196+
for (uint64_t i = 1; i < weight; ++i) new (&items_[internal_update()]) T(static_cast<const T&>(item));
197+
new (&items_[internal_update()]) T(std::forward<FwdT>(item));
198+
reset_sorted_view();
199+
} else {
200+
merge(kll_sketch(k_, static_cast<const T&>(item), weight, comparator_, allocator_));
201+
}
202+
}
203+
204+
// An item at level h carries weight 2^h, so one copy of the item at each level
205+
// whose bit is set in the weight is an exact sketch of that many copies.
206+
// Level capacities are defined up to 61 levels, so bits above the top level
207+
// fold into weight >> 60 copies at level 60.
208+
template<typename T, typename C, typename A>
209+
kll_sketch<T, C, A>::kll_sketch(uint16_t k, const T& item, uint64_t weight, const C& comparator, const A& allocator):
210+
comparator_(comparator),
211+
allocator_(allocator),
212+
k_(k),
213+
m_(kll_constants::DEFAULT_M),
214+
min_k_(k),
215+
num_levels_(std::min<uint8_t>(64 - count_leading_zeros_in_u64(weight), 61)),
216+
is_level_zero_sorted_(true),
217+
n_(weight),
218+
levels_(num_levels_ + 1, 0, allocator),
219+
items_(nullptr),
220+
items_size_(0),
221+
min_item_(item),
222+
max_item_(item),
223+
sorted_view_(nullptr)
224+
{
225+
for (uint8_t level = 0; level < num_levels_; ++level) {
226+
const uint64_t count = level + 1 < num_levels_ ? (weight >> level) & 1 : weight >> level;
227+
levels_[level + 1] = levels_[level] + static_cast<uint32_t>(count);
228+
}
229+
items_size_ = levels_[num_levels_];
230+
items_ = allocator_.allocate(items_size_);
231+
for (uint32_t i = 0; i < items_size_; ++i) new (&items_[i]) T(item);
232+
}
233+
189234
template<typename T, typename C, typename A>
190235
void kll_sketch<T, C, A>::update_min_max(const T& item) {
191236
if (is_empty()) {

‎kll/test/kll_sketch_test.cpp‎

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,9 @@ TEST_CASE("kll sketch", "[kll_sketch]") {
443443
REQUIRE(kll_helper::floor_of_log2_of_fraction(6, 2) == 1);
444444
REQUIRE(kll_helper::floor_of_log2_of_fraction(7, 2) == 1);
445445
REQUIRE(kll_helper::floor_of_log2_of_fraction(8, 2) == 2);
446+
REQUIRE(kll_helper::floor_of_log2_of_fraction(1ULL << 63, 1) == 63);
447+
REQUIRE(kll_helper::floor_of_log2_of_fraction(std::numeric_limits<uint64_t>::max(), 1) == 63);
448+
REQUIRE(kll_helper::floor_of_log2_of_fraction(std::numeric_limits<uint64_t>::max(), 3) == 62);
446449
}
447450

448451
SECTION("out of order split points, float") {
@@ -831,6 +834,123 @@ TEST_CASE("kll sketch", "[kll_sketch]") {
831834
REQUIRE(sb.get_n() == 3);
832835
}
833836

837+
SECTION("weighted update: zero weight") {
838+
kll_float_sketch sketch(200, std::less<float>(), 0);
839+
REQUIRE_THROWS_AS(sketch.update(1.0f, 0), std::invalid_argument);
840+
REQUIRE(sketch.is_empty());
841+
}
842+
843+
SECTION("weighted update: NaN") {
844+
kll_float_sketch sketch(200, std::less<float>(), 0);
845+
sketch.update(std::numeric_limits<float>::quiet_NaN(), 1000);
846+
REQUIRE(sketch.is_empty());
847+
}
848+
849+
SECTION("weighted update: same as repeated updates in exact mode") {
850+
kll_float_sketch weighted(200, std::less<float>(), 0);
851+
kll_float_sketch repeated(200, std::less<float>(), 0);
852+
for (int i = 1; i <= 15; ++i) {
853+
weighted.update(static_cast<float>(i), i);
854+
for (int j = 0; j < i; ++j) repeated.update(static_cast<float>(i));
855+
}
856+
REQUIRE(weighted.get_n() == repeated.get_n());
857+
REQUIRE(weighted.get_num_retained() == repeated.get_num_retained());
858+
REQUIRE_FALSE(weighted.is_estimation_mode());
859+
for (int i = 0; i <= 16; ++i) {
860+
REQUIRE(weighted.get_rank(static_cast<float>(i)) == repeated.get_rank(static_cast<float>(i)));
861+
REQUIRE(weighted.get_rank(static_cast<float>(i), false) == repeated.get_rank(static_cast<float>(i), false));
862+
}
863+
}
864+
865+
SECTION("weighted update: one item with large weight") {
866+
kll_float_sketch sketch(200, std::less<float>(), 0);
867+
const uint64_t weight = (1ULL << 40) + 12345;
868+
sketch.update(7.0f, weight);
869+
REQUIRE(sketch.get_n() == weight);
870+
REQUIRE(sketch.get_num_retained() == 7); // number of bits set in the weight
871+
REQUIRE(sketch.get_min_item() == 7.0f);
872+
REQUIRE(sketch.get_max_item() == 7.0f);
873+
REQUIRE(sketch.get_rank(7.0f) == 1);
874+
REQUIRE(sketch.get_rank(7.0f, false) == 0);
875+
REQUIRE(sketch.get_quantile(0.5) == 7.0f);
876+
877+
uint64_t total_weight = 0;
878+
for (auto pair: sketch) total_weight += pair.second;
879+
REQUIRE(total_weight == weight);
880+
}
881+
882+
SECTION("weighted update: maximum weight") {
883+
kll_float_sketch sketch(200, std::less<float>(), 0);
884+
sketch.update(1.0f);
885+
sketch.update(2.0f, std::numeric_limits<uint64_t>::max() - 1);
886+
REQUIRE(sketch.get_n() == std::numeric_limits<uint64_t>::max());
887+
REQUIRE(sketch.get_min_item() == 1.0f);
888+
REQUIRE(sketch.get_max_item() == 2.0f);
889+
REQUIRE(sketch.get_quantile(0.5) == 2.0f);
890+
891+
uint64_t total_weight = 0;
892+
for (auto pair: sketch) total_weight += pair.second;
893+
REQUIRE(total_weight == sketch.get_n());
894+
895+
auto bytes = sketch.serialize();
896+
auto sketch2 = kll_float_sketch::deserialize(bytes.data(), bytes.size(), serde<float>(), std::less<float>(), 0);
897+
REQUIRE(sketch2.get_n() == sketch.get_n());
898+
REQUIRE(sketch2.get_num_retained() == sketch.get_num_retained());
899+
}
900+
901+
SECTION("weighted update: large weights into a full sketch") {
902+
kll_float_sketch sketch(200, std::less<float>(), 0);
903+
for (int i = 0; i < 1000; ++i) sketch.update(static_cast<float>(i));
904+
sketch.update(-1.0f, 1000);
905+
sketch.update(2000.0f, 3000);
906+
REQUIRE(sketch.get_n() == 5000);
907+
REQUIRE(sketch.get_min_item() == -1.0f);
908+
REQUIRE(sketch.get_max_item() == 2000.0f);
909+
REQUIRE(sketch.get_rank(-1.0f) == Approx(0.2).margin(RANK_EPS_FOR_K_200));
910+
REQUIRE(sketch.get_rank(2000.0f, false) == Approx(0.4).margin(RANK_EPS_FOR_K_200));
911+
}
912+
913+
SECTION("weighted update: estimation mode") {
914+
kll_float_sketch sketch(200, std::less<float>(), 0);
915+
const int n = 10000;
916+
std::vector<uint64_t> weights(n);
917+
uint64_t total = 0;
918+
for (int i = 0; i < n; ++i) {
919+
weights[i] = static_cast<uint64_t>(i % 13) * 1000 + 1;
920+
total += weights[i];
921+
sketch.update(static_cast<float>(i), weights[i]);
922+
}
923+
REQUIRE(sketch.get_n() == total);
924+
REQUIRE(sketch.is_estimation_mode());
925+
REQUIRE(sketch.get_min_item() == 0);
926+
REQUIRE(sketch.get_max_item() == n - 1);
927+
928+
uint64_t weight_below = 0;
929+
for (int i = 0; i < n; ++i) {
930+
const double true_rank = static_cast<double>(weight_below) / total;
931+
REQUIRE(sketch.get_rank(static_cast<float>(i), false) == Approx(true_rank).margin(RANK_EPS_FOR_K_200));
932+
weight_below += weights[i];
933+
}
934+
935+
auto bytes = sketch.serialize();
936+
auto sketch2 = kll_float_sketch::deserialize(bytes.data(), bytes.size(), serde<float>(), std::less<float>(), 0);
937+
REQUIRE(sketch2.get_n() == sketch.get_n());
938+
REQUIRE(sketch2.get_num_retained() == sketch.get_num_retained());
939+
REQUIRE(sketch2.get_quantile(0.5) == sketch.get_quantile(0.5));
940+
}
941+
942+
SECTION("weighted update: strings") {
943+
kll_string_sketch sketch(200, std::less<std::string>(), 0);
944+
sketch.update(std::string("b"), 3);
945+
const std::string a("a");
946+
sketch.update(a, 1000);
947+
sketch.update(std::string("c"), 1ULL << 20);
948+
REQUIRE(sketch.get_n() == 3 + 1000 + (1ULL << 20));
949+
REQUIRE(sketch.get_min_item() == "a");
950+
REQUIRE(sketch.get_max_item() == "c");
951+
REQUIRE(sketch.get_rank("a") == Approx(1000.0 / sketch.get_n()).margin(RANK_EPS_FOR_K_200));
952+
}
953+
834954
// cleanup
835955
REQUIRE(test_allocator_total_bytes == 0);
836956
}

0 commit comments

Comments
 (0)