From 7120737f09ae9b4612f2165368b8a9ad6b45055c Mon Sep 17 00:00:00 2001 From: Daniel Lemire Date: Thu, 15 Oct 2026 01:10:23 -0400 Subject: [PATCH 1/2] Add roaring_bitmap_range_to_words Writes the values of a range of the bitmap, given in 64-bit words, as a bitset. Bitset containers are copied, array and run containers set only the bits in the range, so the cost depends on the range and not on the size of the bitmap. --- include/roaring/roaring.h | 18 ++++++++ src/roaring.c | 68 +++++++++++++++++++++++++++++ tests/toplevel_unit.c | 92 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 178 insertions(+) diff --git a/include/roaring/roaring.h b/include/roaring/roaring.h index 814e261bd..d3b6dde3d 100644 --- a/include/roaring/roaring.h +++ b/include/roaring/roaring.h @@ -588,6 +588,24 @@ void roaring_bitmap_to_uint32_array(const roaring_bitmap_t *r, uint32_t *ans); */ bool roaring_bitmap_to_bitset(const roaring_bitmap_t *r, bitset_t *bitset); +/** + * Writes the values of the bitmap from 64 * first_word (included) to + * 64 * (first_word + n_words) (excluded) as a bitset in `words`: value + * 64 * (first_word + k) + i, if present, sets bit i of words[k]. All n_words + * words are written (the caller does not need to zero them). The cost is + * proportional to n_words plus the number of values and runs in the range, + * not to the size of the bitmap. + * + * first_word + n_words must be at most 2^26 (that is, 2^32 / 64). + * + * uint64_t words[16]; + * // values 1024 to 2047, value 1024 + 64 * k + i in bit i of words[k] + * roaring_bitmap_range_to_words(mybitmap, 16, 16, words); + */ +void roaring_bitmap_range_to_words(const roaring_bitmap_t *r, + uint32_t first_word, uint32_t n_words, + uint64_t *words); + /** * Convert the bitmap to a sorted array from `offset` by `limit`, output in * `ans`. diff --git a/src/roaring.c b/src/roaring.c index 1c6ee0429..f9114a8c1 100644 --- a/src/roaring.c +++ b/src/roaring.c @@ -3705,6 +3705,74 @@ roaring_bitmap_t *roaring_bitmap_portable_deserialize_frozen(const char *buf) { #endif } +void roaring_bitmap_range_to_words(const roaring_bitmap_t *r, + uint32_t first_word, uint32_t n_words, + uint64_t *words) { + memset(words, 0, (size_t)n_words * sizeof(uint64_t)); + if (n_words == 0) return; + assert((uint64_t)first_word + n_words <= (UINT64_C(1) << 26)); + const uint64_t begin = (uint64_t)first_word * 64; + const uint64_t end = begin + (uint64_t)n_words * 64; // excluded + const roaring_array_t *ra = &r->high_low_container; + int32_t i = ra_get_index(ra, (uint16_t)(begin >> 16)); + if (i < 0) i = -i - 1; + for (; i < ra->size; ++i) { + const uint64_t chunk = (uint64_t)ra->keys[i] << 16; + if (chunk >= end) break; + // The window covers the words [lo, hi) of the container, which go + // to out[0, hi - lo). + const uint32_t lo = + chunk < begin ? (uint32_t)((begin - chunk) / 64) : 0; + const uint32_t hi = + chunk + 65536 > end ? (uint32_t)((end - chunk) / 64) : 1024; + uint64_t *out = words + (chunk / 64 + lo - first_word); + const uint32_t vlo = lo * 64, vhi = hi * 64; // values [vlo, vhi) + uint8_t type = ra->typecodes[i]; + const container_t *c = + container_unwrap_shared(ra->containers[i], &type); + switch (type) { + case BITSET_CONTAINER_TYPE: + memcpy(out, const_CAST_bitset(c)->words + lo, + (size_t)(hi - lo) * sizeof(uint64_t)); + break; + case ARRAY_CONTAINER_TYPE: { + const array_container_t *src = const_CAST_array(c); + int32_t k = 0; + if (vlo > 0) { + k = binarySearch(src->array, src->cardinality, + (uint16_t)vlo); + if (k < 0) k = -k - 1; + } + for (; k < src->cardinality && src->array[k] < vhi; ++k) { + const uint32_t v = src->array[k] - vlo; + out[v / 64] |= UINT64_C(1) << (v % 64); + } + } break; + case RUN_CONTAINER_TYPE: { + const run_container_t *src = const_CAST_run(c); + int32_t k = 0; + if (vlo > 0) { + // The last run starting at or before vlo may reach it. + k = interleavedBinarySearch(src->runs, src->n_runs, + (uint16_t)vlo); + if (k < 0) k = -k - 2; + if (k < 0) k = 0; + } + for (; k < src->n_runs && src->runs[k].value < vhi; ++k) { + uint32_t s = src->runs[k].value; + uint32_t e = s + src->runs[k].length + 1; // excluded + if (e <= vlo) continue; + if (s < vlo) s = vlo; + if (e > vhi) e = vhi; + bitset_set_range(out, s - vlo, e - vlo); + } + } break; + default: + roaring_unreachable; + } + } +} + bool roaring_bitmap_to_bitset(const roaring_bitmap_t *r, bitset_t *bitset) { uint32_t max_value = roaring_bitmap_maximum(r); size_t new_array_size = (size_t)(max_value / 64 + 1); diff --git a/tests/toplevel_unit.c b/tests/toplevel_unit.c index 8cc63633c..57a948305 100644 --- a/tests/toplevel_unit.c +++ b/tests/toplevel_unit.c @@ -3621,6 +3621,97 @@ DEFINE_TEST(test_flip_sparse_array_container) { } } +// Checks roaring_bitmap_range_to_words against roaring_bitmap_contains. +static void check_range_to_words(const roaring_bitmap_t *bm, + uint32_t first_word, uint32_t n_words) { + uint64_t *words = (uint64_t *)malloc((n_words + 2) * sizeof(uint64_t)); + assert_non_null(words); + // Guard words around the output must not be touched. + words[0] = words[n_words + 1] = UINT64_C(0x0123456789abcdef); + for (uint32_t k = 1; k <= n_words; k++) words[k] = ~UINT64_C(0); + roaring_bitmap_range_to_words(bm, first_word, n_words, words + 1); + assert_true(words[0] == UINT64_C(0x0123456789abcdef)); + assert_true(words[n_words + 1] == UINT64_C(0x0123456789abcdef)); + for (uint32_t k = 0; k < n_words; k++) { + uint64_t expected = 0; + for (uint32_t i = 0; i < 64; i++) { + uint32_t v = (uint32_t)(((uint64_t)first_word + k) * 64 + i); + if (roaring_bitmap_contains(bm, v)) expected |= UINT64_C(1) << i; + } + assert_true(words[k + 1] == expected); + } + free(words); +} + +DEFINE_TEST(test_range_to_words) { + roaring_bitmap_t *bm = roaring_bitmap_create(); + uint32_t seed = 1234; + // Chunk 0: array, chunk 1: bitset, chunk 2: runs, chunk 4: array, and + // the last chunk of the 32-bit range: runs and values at the very end. + for (uint32_t k = 0; k < 1000; k++) { + seed = seed * 1664525 + 1013904223; + roaring_bitmap_add(bm, (seed >> 16) % 65536); + } + for (uint32_t k = 0; k < 30000; k++) { + seed = seed * 1664525 + 1013904223; + roaring_bitmap_add(bm, 65536 + (seed >> 16) % 65536); + } + for (uint32_t k = 0; k < 300; k++) { + seed = seed * 1664525 + 1013904223; + uint32_t s = 2 * 65536 + (seed >> 16) % 65000; + roaring_bitmap_add_range(bm, s, s + 1 + (seed >> 8) % 300); + } + roaring_bitmap_add(bm, 4 * 65536 + 63); + roaring_bitmap_add(bm, 4 * 65536 + 64); + roaring_bitmap_add_range(bm, UINT64_C(0xFFFF0000) + 100, + UINT64_C(0xFFFF0000) + 40000); + roaring_bitmap_add_range(bm, UINT64_C(0xFFFFFFF0), UINT64_C(0x100000000)); + roaring_bitmap_run_optimize(bm); + roaring_statistics_t stats; + roaring_bitmap_statistics(bm, &stats); + assert_true(stats.n_array_containers >= 2); + assert_true(stats.n_bitset_containers >= 1); + assert_true(stats.n_run_containers >= 2); + + const uint32_t last = (UINT32_C(1) << 26); + const uint32_t windows[][2] = {{0, 0}, + {0, 1}, + {0, 1024}, + {0, 5000}, + {1, 1}, + {3, 2}, + {1023, 2}, + {1000, 100}, + {1024, 1024}, + {1500, 1}, + {2047, 3}, + {2048, 1024}, + {2100, 7}, + {2900, 300}, + {4096, 1}, + {4096, 2}, + {3000, 2000}, + {10000, 10}, + {last - 1, 1}, + {last - 1024, 1024}, + {last - 700, 700}, + {last - 1100, 200}, + {last - 3000, 3000}}; + for (size_t w = 0; w < sizeof(windows) / sizeof(windows[0]); w++) { + check_range_to_words(bm, windows[w][0], windows[w][1]); + } + for (int trial = 0; trial < 200; trial++) { + seed = seed * 1664525 + 1013904223; + uint32_t first = (seed >> 8) % 5200; + seed = seed * 1664525 + 1013904223; + check_range_to_words(bm, first, (seed >> 8) % 1500); + } + roaring_bitmap_t *empty = roaring_bitmap_create(); + check_range_to_words(empty, 100, 50); + roaring_bitmap_free(empty); + roaring_bitmap_free(bm); +} + // randomized test for rank query DEFINE_TEST(select_test) { const int min_runs = 1; @@ -6009,6 +6100,7 @@ int main() { cmocka_unit_test(test_flip_run_container_removal), cmocka_unit_test(test_flip_run_container_removal2), cmocka_unit_test(test_flip_sparse_array_container), + cmocka_unit_test(test_range_to_words), cmocka_unit_test(select_test), cmocka_unit_test(test_subset), cmocka_unit_test(test_or_many_memory_leak), From df3470a5c82726389353e05a62388e091a2bf3e9 Mon Sep 17 00:00:00 2001 From: Daniel Lemire Date: Thu, 15 Oct 2026 20:00:16 -0400 Subject: [PATCH 2/2] Rename roaring_bitmap_range_to_words to roaring_bitmap_word_range_to_bitset The new name says that the range is in units of 64-bit words. --- include/roaring/roaring.h | 20 ++++++++++---------- src/roaring.c | 6 +++--- tests/toplevel_unit.c | 18 +++++++++--------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/include/roaring/roaring.h b/include/roaring/roaring.h index d3b6dde3d..2aaf7a4b8 100644 --- a/include/roaring/roaring.h +++ b/include/roaring/roaring.h @@ -589,22 +589,22 @@ void roaring_bitmap_to_uint32_array(const roaring_bitmap_t *r, uint32_t *ans); bool roaring_bitmap_to_bitset(const roaring_bitmap_t *r, bitset_t *bitset); /** - * Writes the values of the bitmap from 64 * first_word (included) to - * 64 * (first_word + n_words) (excluded) as a bitset in `words`: value - * 64 * (first_word + k) + i, if present, sets bit i of words[k]. All n_words - * words are written (the caller does not need to zero them). The cost is - * proportional to n_words plus the number of values and runs in the range, - * not to the size of the bitmap. + * Writes the values of the bitmap in a range of 64-bit words, from + * 64 * first_word (included) to 64 * (first_word + n_words) (excluded), as a + * bitset in `words`: value 64 * (first_word + k) + i, if present, sets bit i + * of words[k]. All n_words words are written (the caller does not need to zero + * them). The cost is proportional to n_words plus the number of values and + * runs in the range, not to the size of the bitmap. * * first_word + n_words must be at most 2^26 (that is, 2^32 / 64). * * uint64_t words[16]; * // values 1024 to 2047, value 1024 + 64 * k + i in bit i of words[k] - * roaring_bitmap_range_to_words(mybitmap, 16, 16, words); + * roaring_bitmap_word_range_to_bitset(mybitmap, 16, 16, words); */ -void roaring_bitmap_range_to_words(const roaring_bitmap_t *r, - uint32_t first_word, uint32_t n_words, - uint64_t *words); +void roaring_bitmap_word_range_to_bitset(const roaring_bitmap_t *r, + uint32_t first_word, uint32_t n_words, + uint64_t *words); /** * Convert the bitmap to a sorted array from `offset` by `limit`, output in diff --git a/src/roaring.c b/src/roaring.c index f9114a8c1..3772e2110 100644 --- a/src/roaring.c +++ b/src/roaring.c @@ -3705,9 +3705,9 @@ roaring_bitmap_t *roaring_bitmap_portable_deserialize_frozen(const char *buf) { #endif } -void roaring_bitmap_range_to_words(const roaring_bitmap_t *r, - uint32_t first_word, uint32_t n_words, - uint64_t *words) { +void roaring_bitmap_word_range_to_bitset(const roaring_bitmap_t *r, + uint32_t first_word, uint32_t n_words, + uint64_t *words) { memset(words, 0, (size_t)n_words * sizeof(uint64_t)); if (n_words == 0) return; assert((uint64_t)first_word + n_words <= (UINT64_C(1) << 26)); diff --git a/tests/toplevel_unit.c b/tests/toplevel_unit.c index 57a948305..eac851d17 100644 --- a/tests/toplevel_unit.c +++ b/tests/toplevel_unit.c @@ -3621,15 +3621,15 @@ DEFINE_TEST(test_flip_sparse_array_container) { } } -// Checks roaring_bitmap_range_to_words against roaring_bitmap_contains. -static void check_range_to_words(const roaring_bitmap_t *bm, - uint32_t first_word, uint32_t n_words) { +// Checks roaring_bitmap_word_range_to_bitset against roaring_bitmap_contains. +static void check_word_range_to_bitset(const roaring_bitmap_t *bm, + uint32_t first_word, uint32_t n_words) { uint64_t *words = (uint64_t *)malloc((n_words + 2) * sizeof(uint64_t)); assert_non_null(words); // Guard words around the output must not be touched. words[0] = words[n_words + 1] = UINT64_C(0x0123456789abcdef); for (uint32_t k = 1; k <= n_words; k++) words[k] = ~UINT64_C(0); - roaring_bitmap_range_to_words(bm, first_word, n_words, words + 1); + roaring_bitmap_word_range_to_bitset(bm, first_word, n_words, words + 1); assert_true(words[0] == UINT64_C(0x0123456789abcdef)); assert_true(words[n_words + 1] == UINT64_C(0x0123456789abcdef)); for (uint32_t k = 0; k < n_words; k++) { @@ -3643,7 +3643,7 @@ static void check_range_to_words(const roaring_bitmap_t *bm, free(words); } -DEFINE_TEST(test_range_to_words) { +DEFINE_TEST(test_word_range_to_bitset) { roaring_bitmap_t *bm = roaring_bitmap_create(); uint32_t seed = 1234; // Chunk 0: array, chunk 1: bitset, chunk 2: runs, chunk 4: array, and @@ -3698,16 +3698,16 @@ DEFINE_TEST(test_range_to_words) { {last - 1100, 200}, {last - 3000, 3000}}; for (size_t w = 0; w < sizeof(windows) / sizeof(windows[0]); w++) { - check_range_to_words(bm, windows[w][0], windows[w][1]); + check_word_range_to_bitset(bm, windows[w][0], windows[w][1]); } for (int trial = 0; trial < 200; trial++) { seed = seed * 1664525 + 1013904223; uint32_t first = (seed >> 8) % 5200; seed = seed * 1664525 + 1013904223; - check_range_to_words(bm, first, (seed >> 8) % 1500); + check_word_range_to_bitset(bm, first, (seed >> 8) % 1500); } roaring_bitmap_t *empty = roaring_bitmap_create(); - check_range_to_words(empty, 100, 50); + check_word_range_to_bitset(empty, 100, 50); roaring_bitmap_free(empty); roaring_bitmap_free(bm); } @@ -6100,7 +6100,7 @@ int main() { cmocka_unit_test(test_flip_run_container_removal), cmocka_unit_test(test_flip_run_container_removal2), cmocka_unit_test(test_flip_sparse_array_container), - cmocka_unit_test(test_range_to_words), + cmocka_unit_test(test_word_range_to_bitset), cmocka_unit_test(select_test), cmocka_unit_test(test_subset), cmocka_unit_test(test_or_many_memory_leak),