|
Everett
|
I compare the same original bit vector at the same exclusive boundaries, rank(15*g), using the full bitmap directory, packed 15-entry populations, and the typed rank_groups_view<15> path. There is no RRR representation here.
The standalone benchmark and runner pin the baseline headers to 7732b1ed551dccc05256964106b7091e56e65bc0 and the SIMD rank15 header to 91bd022eaaf6334cdf6b1391389c7ea2eb706d48. The runner extracts those committed headers into an ignored build directory. Updating the installed or source-tree headers therefore does not silently change the baseline.
The arguments are dataset (all, check, or a case name below), trials, queries per timing batch, and optional core. Omitting core also exercises full sequential sweeps and controlled g mod 128 / (15*g) mod 512 offsets. Those additional timing modes are available but are not part of the recorded measurement below. check validates the two hot datasets without timing them.
bitmap_rank: the original Everett full-bit-vector rank_view::rank, with its 2048/512 directory and scalar loop over the preceding 64-bit words.poppy512_neon_cpu: a CPU translation using the 2048/512 layout from my Poppy shader. It loads all four 128-bit vectors, masks the prefix and reduces byte populations with NEON. The three packed ten-bit counts are summed with explicit parentheses. This is not literal GLSL or GPU timing; the reduction differs from the shader's lane prefix scan. The comparison stays below 2^32 source bits, and the raw allocation has zero padding sufficient for all four vector loads at its end.rank15_baseline: the original packed-word summation implementation.rank_groups15_baseline: the original typed path, which reads and sums individual classes after its checkpoint.rank15_simd: the four-vector, whole-prefix packed implementation. It shares the exact encoded classes and checkpoint allocation with both packed baselines.The original CSV also retains rank15_candidate, an intermediate scalar prototype measured in the same process. That optional prototype is not required by the runner or by the comparisons here; the runner reproduces the five variants listed above. The SIMD snapshot used in the measured process has SHA-256 f11fbcfa831d1b617ede57d09bd009ba35a24fe09eee3d84a839a393553a1de4, identical to the pinned 91bd022 header.
These measurements used an Apple M2 Max with 96 GiB RAM and AppleClang 21 (clang-2100.1.1.101), -std=c++20 -O3 -DNDEBUG. The exposed performance-cluster L2 size was 16 MiB; the system did not expose an hw.l3cachesize value. The thread requested user-initiated QoS. A shared exclusive CPU resource gate kept the cooperating workers' builds and timings separate.
Each row records five trials of 1,048,576 queries; variant order rotates between trials. The source bits come from deterministic SplitMix64 output. Queries are deterministic and shared across all variants within a case. Construction, allocation, oracle checking and file I/O are outside the timed region.
Independent random rows measure amortized nanoseconds per call, including query-array indexing and function dispatch. The dependent row feeds each answer into selection of the next query, so it includes the common multiply/XOR/query selection overhead and limits overlap between successive rank operations. It is not an isolated hardware load-latency measurement.
pair_two_ranks returns rank(g) + rank(g+1). pair_rank_plus_class computes the same result from 2*rank(g) + class_at(g); the bitmap version counts the next fifteen source bits, crossing a word boundary when necessary.
An independent oracle uses raw-word population prefixes, not packed classes. Every generated query, including the complete dependent chain, is checked against it; timed checksums also agree across all variants. Hot-case checks of all variants and pair modes passed ASan/UBSan. The standalone pinned-header runner was separately compiled with strict warnings and its oracle check passed.
Byte counts are exact encoded array sizes. They include the raw bitmap's final vector-load padding, rank directory and epoch base, or packed classes plus checkpoints. They exclude C++ view/vector objects, allocator bookkeeping, query arrays and the untimed oracle. Allocated vector capacities equal the reported section sizes in this run. Both full and packed data pointers were 64-byte aligned in every recorded case; checkpoint alignment is in the CSV.
| Case | Logical bits | Full data + metadata | Packed data + metadata | Shared query array |
|---|---|---|---|---|
hot_full32KiB | 253,440 | 31,680 + 1,000 = 32,680 B | 8,448 + 1,056 = 9,504 B | 128 KiB |
hot_packed32KiB | 873,600 | 109,248 + 3,424 = 112,672 B | 29,120 + 3,640 = 32,760 B | 256 KiB |
large_full96MiB | 780,902,400 | 97,612,800 + 3,050,408 = 100,663,208 B | 26,030,080 + 3,253,760 = 29,283,840 B | 8 MiB |
large_packed96MiB | 2,684,353,920 | 335,544,256 + 10,485,768 = 346,030,024 B | 89,478,464 + 11,184,808 = 100,663,272 B | 8 MiB |
Within each case, logical size and query positions are identical. Comparing full storage in a full case with packed storage in the corresponding packed case instead holds storage near 32 KiB or 96 MiB while increasing the packed logical universe. We should keep those two comparisons separate. The largest case places both representations well beyond the exposed 16 MiB CPU cache.
The hot hot_full32KiB case below uses the same 253,440 bits for every row. All entries are median nanoseconds per operation; the CSV preserves minima and maxima, including outliers in the pair column.
| Variant | Random rank | Two-rank pair | Rank + class pair | Dependent random rank |
|---|---|---|---|---|
| Full bitmap | 14.861 | 23.501 | 18.577 | 23.255 |
| Poppy-layout NEON CPU | 4.888 | 9.521 | 7.336 | 18.765 |
| Original rank15 | 15.782 | 28.241 | 18.152 | 19.963 |
| Original typed K=15 | 43.783 | 78.672 | 45.561 | 45.528 |
| Whole-prefix packed SIMD | 5.375 | 11.276 | 6.558 | 17.119 |
The full-vector SIMD comparison matters: it is faster than packed SIMD for independent single-rank calls in this hot case. Packed SIMD retains the smaller representation and has the smaller pair and dependent-query medians here. The old typed two-rank pair and the new packed rank-plus-class pair measure different algorithms for the same two-boundary result; their 78.672 versus 6.558 ns is not a claim about an entire blob lookup.
Large resident-memory results were noisy even with explicit QoS. Below I retain median [minimum, maximum] rather than turning these data into a general performance ranking. These spreads also prevent treating the independent and dependent columns as a clean hardware latency decomposition.
| Case / variant | Independent rank, ns | Dependent rank, ns |
|---|---|---|
| 96 MiB full / bitmap | 68.042 [66.812, 69.525] | 286.890 [150.485, 314.094] |
| 96 MiB full / Poppy CPU | 50.452 [43.622, 50.550] | 214.396 [170.665, 302.246] |
| 96 MiB full / packed SIMD | 39.748 [31.445, 41.378] | 137.298 [102.283, 171.042] |
| 96 MiB packed / bitmap | 39.272 [33.530, 77.190] | 223.048 [146.319, 313.036] |
| 96 MiB packed / Poppy CPU | 27.145 [24.768, 57.111] | 167.843 [156.076, 251.630] |
| 96 MiB packed / packed SIMD | 27.174 [26.726, 38.231] | 201.521 [134.779, 322.577] |
The original data retain all baselines, paired operations, both approximately equal-footprint cases and every trial range. These are resident-memory microbenchmarks, without mmap page faults, storage I/O, complete catalog searches or end-to-end update scheduling.