Skip to content

Commit f770f7d

Browse files
committed
Fix MicroStats bin/range overflow at the top of the 64-bit domain
calcbin() maps any value with bit 63 set into octave (64-NDB), whose highest index is ((64-NDB)<<NDB)+MASK. NUMBINS was sized (64-NDB)<<NDB, one octave short, so those samples wrote past bins[] in add(). Size it (65-NDB)<<NDB. calcrange() held the sub-bin partition in a uint32_t and shifted it left by up to (numbits-NDB-1), which exceeds 31 for high octaves and overflows, corrupting the reconstructed range (and thus percentiles) for very large samples. Hold partition in 64 bits. Add testMicroStatsRange: a dependency-free regression test covering NDB 0..6 across the full 64-bit domain (top octave and 2^64-1 included), asserting no out-of-bounds index, calcrange(calcbin(v)) contains v, and monotonic bins. Returns nonzero on failure so ctest catches it; wired in via add_test + enable_testing().
1 parent fd8b8e5 commit f770f7d

4 files changed

Lines changed: 79 additions & 4 deletions

File tree

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ include_directories( libs )
6060
set( TARGETS tinyperfstats )
6161

6262
if ( TINYPERF_BUILD_TESTS AND Armadillo_FOUND )
63+
enable_testing()
6364
add_subdirectory( datasets )
6465
add_subdirectory( tests )
6566
endif()

libs/MicroStats.h

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,12 @@ inline uint32_t msb(uint64_t value) {
3232
template <uint32_t NDB>
3333
class MicroStats {
3434
public:
35-
//! The number of available bins possible in a 64-bit range
36-
static constexpr uint32_t NUMBINS = (64 - NDB) << NDB;
35+
//! The number of available bins possible in a 64-bit range.
36+
//! A value with bit 63 set has msb()==64 and lands in octave (64-NDB),
37+
//! so the highest bin index is ((64-NDB)<<NDB)+MASK. Sizing the array at
38+
//! (64-NDB)<<NDB is one octave short and overflows on those samples;
39+
//! (65-NDB)<<NDB gives the top octave its own slots.
40+
static constexpr uint32_t NUMBINS = (65 - NDB) << NDB;
3741

3842
//! Mask used extensively in the calculations
3943
static constexpr uint64_t MASK = (1ULL << NDB) - 1;
@@ -117,7 +121,10 @@ class MicroStats {
117121
//! Computes the range of a bin given its ID
118122
static Range calcrange(uint32_t bin) {
119123
if (bin < (1 << NDB)) return Range{bin, bin};
120-
uint32_t partition = bin & MASK;
124+
// partition is shifted left by up to (numbits - NDB - 1), which exceeds
125+
// 31 for high octaves; a uint32_t shift overflows there and corrupts the
126+
// range. Hold it in 64 bits so the shift stays valid across the range.
127+
uint64_t partition = bin & MASK;
121128
uint32_t numbits = (bin >> NDB) + NDB;
122129
uint64_t base = 1ULL << (numbits - 1);
123130
uint64_t offset = partition << (numbits - (NDB + 1));

tests/microstats/CMakeLists.txt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,9 @@
22
add_executable( testMicroStats testMicroStats.cpp )
33
target_link_libraries( testMicroStats ${REQUIRED_LIBS} )
44

5-
list( APPEND TARGETS testMicroStats )
5+
# Assertion-based regression test for the bin/range arithmetic. Header-only,
6+
# no external dependencies, returns nonzero on failure so ctest catches it.
7+
add_executable( testMicroStatsRange testMicroStatsRange.cpp )
8+
add_test( NAME testMicroStatsRange COMMAND testMicroStatsRange )
9+
10+
list( APPEND TARGETS testMicroStats testMicroStatsRange )
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Regression test for MicroStats bin/range arithmetic across the full 64-bit
2+
// domain and for NDB = 0..6. It guards two corner cases that the demo in
3+
// testMicroStats.cpp never exercises:
4+
//
5+
// 1) calcbin() must never return an index >= NUMBINS. A value with bit 63
6+
// set lands in the top octave; if NUMBINS is one octave short the index
7+
// runs past the bins[] array (out-of-bounds write in add()).
8+
//
9+
// 2) calcrange(calcbin(v)) must contain v. For high octaves the sub-bin
10+
// partition is shifted left by more than 31 bits, so holding it in a
11+
// 32-bit value overflows and corrupts the reconstructed range.
12+
//
13+
// Uses explicit checks and a nonzero exit code rather than assert(), which is
14+
// compiled out in Release builds.
15+
16+
#include "MicroStats.h"
17+
#include <cstdint>
18+
#include <cstdio>
19+
20+
template <uint32_t NDB>
21+
static long check() {
22+
using M = MicroStats<NDB>;
23+
long oob = 0, inversion = 0, monotonic = 0;
24+
25+
auto probe = [&](uint64_t v) {
26+
uint32_t b = M::calcbin(v);
27+
if (b >= M::NUMBINS) { ++oob; return; }
28+
auto r = M::calcrange(b);
29+
if (!(v >= r.from && v <= r.to)) ++inversion;
30+
};
31+
32+
// Dense low range, also checking that the bin index is non-decreasing in v.
33+
uint32_t prev = 0;
34+
for (uint64_t v = 1; v < (1ull << 24); ++v) {
35+
uint32_t b = M::calcbin(v);
36+
if (b < prev) ++monotonic;
37+
prev = b;
38+
probe(v);
39+
}
40+
// Sparse sweep across every high octave, including the top one (bit 63),
41+
// landing on each sub-bin boundary.
42+
for (int e = 20; e < 64; ++e)
43+
for (uint64_t d = 0; d < (1u << NDB); ++d)
44+
probe((1ull << e) + (d << (e > (int)NDB ? e - (int)NDB : 0)));
45+
probe(~0ull); // 2^64 - 1, the very top
46+
47+
long total = oob + inversion + monotonic;
48+
std::printf(" NDB=%u NUMBINS=%-5u out-of-bounds=%ld inversion=%ld monotonicity=%ld %s\n",
49+
NDB, M::NUMBINS, oob, inversion, monotonic, total ? "FAIL" : "ok");
50+
return total;
51+
}
52+
53+
int main() {
54+
long failures = check<0>() + check<1>() + check<2>() + check<3>() +
55+
check<4>() + check<5>() + check<6>();
56+
if (failures) {
57+
std::printf("FAILED: %ld total violations\n", failures);
58+
return 1;
59+
}
60+
std::printf("PASSED: bin/range arithmetic valid across NDB 0..6 and the full 64-bit domain\n");
61+
return 0;
62+
}

0 commit comments

Comments
 (0)