Skip to content

Update to CRoaring 5.1.0 and complete the 32-bit and 64-bit API - #30

Merged
lemire merged 1 commit into
masterfrom
croaring-5.1.0-full-api
Aug 22, 2026
Merged

Update to CRoaring 5.1.0 and complete the 32-bit and 64-bit API#30
lemire merged 1 commit into
masterfrom
croaring-5.1.0-full-api

Conversation

@lemire

@lemire lemire commented Aug 22, 2026

Copy link
Copy Markdown
Member

Updates the bundled amalgamation to the v5.1.0 release (we were on 4.5.0) and completes the Go wrapper so that it covers both C APIs rather than a subset of the 32-bit one.

64-bit bitmaps

Bitmap64 wraps roaring64_bitmap_t and offers the same operations as Bitmap, with the free functions carrying a 64 suffix (Or64, And64, Read64...).

rb := gocroaring.New64()
rb.AddRange(1<<40, 1<<40+1000)
rb.RunOptimize()
fmt.Println(rb.Cardinality(), rb.Contains(1<<40+5))

32-bit gaps filled

Checked add/remove, closed ranges, bulk contexts, RankMany, GetIndex, subset tests, the lazy operations and RepairAfterLazy, FastOrHeap and FastXor, AddOffset, copy-on-write control, the native and portable-frozen serialization formats, PortableDeserializeSize, RangeToArray, ToDenseBitset, InternalValidate, and seekable iterators.

The exported surface goes from about 50 functions to 202. Nothing that was exported before was removed or had its signature changed; Statistics gains MinValue and MaxValue.

Memory management

Memory is now released through runtime.AddCleanup instead of runtime.SetFinalizer, following the approach taken by ada-url/goada. A bitmap is reclaimed in a single garbage collection cycle rather than two, the cleanup closure never captures the object, and Free stops the cleanup so explicit and automatic release cannot double free.

BenchmarkFinalizerCreate-14    11654116    348.1 ns/op
BenchmarkCleanupCreate-14      14391621    271.9 ns/op

This requires Go 1.24, so go.mod and the CI matrix move up (1.24.x, 1.25.x).

Fewer, cheaper boundary crossings

Iterators read values in blocks of 512 rather than one per cgo call:

BenchmarkIterateBuffered-14        250     1440917 ns/op
BenchmarkIterateOneByOne-14         10    32061525 ns/op
BenchmarkIterateBuffered64-14      264     1353573 ns/op
BenchmarkIterateOneByOne64-14        4    92739781 ns/op

ReadValidated deserializes and validates in a single crossing, through a small static inline trampoline, the way goada combines parse and validity check.

The CRoaring entry points are declared #cgo nocallback, and #cgo noescape wherever the function does not keep the buffer it is handed. The four frozen-view functions do retain their buffer and are deliberately left out of the noescape list.

Behaviour worth reviewing

  • The frozen views now report a misaligned buffer as ErrMisaligned, where CRoaring merely returned NULL and we surfaced a generic read failure. AlignedBuffer and AlignedBuffer64 hand back a buffer with the alignment each format needs (32 and 64 bytes).
  • Bitmap64.WriteFrozen returns ErrNotShrunken unless ShrinkToFit has been called, which roaring64_bitmap_frozen_size_in_bytes requires but signals only by returning zero.

Testing

New tests cover the added 32-bit surface and the whole 64-bit one. The full suite passes, including under -race (which enables checkptr), and make qa is clean.

https://claude.ai/code/session_01NzKGBVi8pbqkwocqktcjLU

Replace the bundled amalgamation with the v5.1.0 release, then fill in the
Go wrapper so that it covers both C APIs rather than a subset of the 32-bit
one.

Bitmap64 is new: it wraps roaring64_bitmap_t and offers the same operations
as Bitmap, with the free functions carrying a 64 suffix (Or64, Read64...).
The 32-bit side gains the entry points it was missing: checked add/remove,
closed ranges, bulk contexts, RankMany, GetIndex, subset tests, the lazy
operations and RepairAfterLazy, FastOrHeap and FastXor, AddOffset,
copy-on-write control, the native and portable-frozen serialization formats,
PortableDeserializeSize, RangeToArray, ToDenseBitset, InternalValidate, and
seekable iterators.

Memory is now released through runtime.AddCleanup instead of
runtime.SetFinalizer, following the approach taken by ada-url/goada. A
bitmap is reclaimed in a single garbage collection cycle rather than two,
and Free stops the cleanup so that explicit and automatic release cannot
double free. Creating and discarding a bitmap goes from 348ns to 272ns.
This requires Go 1.24, so go.mod and the CI matrix move up.

Two more changes cut the cost of crossing the Go/C boundary:

  - Iterators read values in blocks of 512 rather than one per cgo call.
    Walking a bitmap of a million values takes 1.4ms instead of 32ms
    (1.4ms instead of 93ms for the 64-bit iterator).

  - ReadValidated deserializes and validates in a single crossing, through
    a small static inline trampoline.

The CRoaring entry points are declared #cgo nocallback, and #cgo noescape
wherever the function does not keep the buffer it is handed. The four
frozen-view functions do retain their buffer and are deliberately left out
of the noescape list.

Two behaviours worth noting. The frozen views now report a misaligned
buffer as such, where CRoaring merely returned NULL, and AlignedBuffer and
AlignedBuffer64 hand back a buffer with the alignment each format needs.
Bitmap64.WriteFrozen returns ErrNotShrunken unless ShrinkToFit has been
called, which the C API requires but does not signal.

Claude-Session: https://claude.ai/code/session_01NzKGBVi8pbqkwocqktcjLU
@lemire
lemire merged commit 628535b into master Aug 22, 2026
10 checks passed
@lemire

lemire commented Aug 22, 2026

Copy link
Copy Markdown
Member Author

Pushed a follow-up commit after an audit of the wrapper against the C sources. Two real bugs, both pre-existing in the branch, plus two doc gaps.

In-place operations aborted the process on self-application. roaring_bitmap_xor_inplace, roaring_bitmap_andnot_inplace, roaring_bitmap_lazy_xor_inplace and roaring64_bitmap_xor_inplace assert x1 != x2, so rb.Xor(rb) did this:

Assertion failed: (x1 != x2), function roaring_bitmap_xor_inplace, file roaring.c, line 16340.
SIGABRT: abort

With -DNDEBUG the assertion vanishes and the same call corrupts memory instead. The C library is inconsistent here — and_inplace guards on both widths, or_inplace and overwrite guard only on the 64-bit side, and the 64-bit andnot_inplace guards nowhere — so rather than document a precondition per method, the wrapper now answers the aliased case itself: And/Or/Assign are no-ops, Xor/AndNot empty the bitmap. One pointer comparison, and the result is what the set algebra calls for.

FromRange panicked on an empty range. roaring_bitmap_from_range returns NULL both for step == 0 and for max <= min; I had only guarded the first, so FromRange(5, 5, 1) hit the null-pointer panic instead of returning an empty bitmap as documented. Same on the 64-bit side. (I had misread this function earlier in the branch's history — the C code does not return an empty bitmap for an empty range, it returns NULL.)

Docs: ReadFrozenView64 needs the buffer length to be exactly what was written, and MoveFrom32 takes containers without regard for copy-on-write sharing, so it shouldn't be used on a bitmap that has been cloned with COW enabled.

Regression tests cover every aliased combination on both widths and the empty-range cases. Full suite passes, including under -race.

For the record, the audit found nothing in these areas: all 189 C.roaring* calls match their declarations (argument order verified for the asymmetric ones — andnot, is_subset, overwrite(dest,src), rank_many(begin,end,ans) — and for the two open/closed pairs whose C names differ in word order between the widths); KeepAlive coverage is complete and every &b[0] is length-guarded; no AddCleanup argument is reachable from the object it cleans up, so nothing leaks; and the noescape list excludes exactly the four functions that retain their buffer, with every other listed function confirmed to copy or write only.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant