| Crates.io | small_range |
| lib.rs | small_range |
| version | 1.0.0 |
| created_at | 2026-01-22 01:44:11.096742+00 |
| updated_at | 2026-01-22 01:44:11.096742+00 |
| description | A compact range type: 50% smaller than Range |
| homepage | |
| repository | https://github.com/shergin/small-range |
| max_upload_size | |
| id | 2060565 |
| size | 75,661 |
A compact range type: 50% smaller than Range
Imagine you need to store
Option<Range<usize>>millions of times. That's 192 bits per instance. With this library you can shrink that to just 64 bits (see tradeoffs).
Standard Range<T> stores start and end as separate fields, requiring 2 * size_of::<T>() bytes. For applications managing millions of ranges (spatial indexing, text processing, interval trees), this overhead adds up.
SmallRange<T> packs start and length into a single value of type T:
| Type | Size | vs Range<T> |
Max Start | Max Length |
|---|---|---|---|---|
SmallRange<u16> |
2 bytes | vs 4 bytes (50%) | 254 | 254 |
SmallRange<u32> |
4 bytes | vs 8 bytes (50%) | 65,534 | 65,534 |
SmallRange<u64> |
8 bytes | vs 16 bytes (50%) | ~4.29B | ~4.29B |
SmallRange<usize> |
8 bytes | vs 16 bytes (50%) | ~4.29B | ~4.29B |
Plus: Option<SmallRange<T>> is the same size as SmallRange<T> due to niche optimization.
use small_range::SmallRange;
use core::mem::size_of;
// 50% space savings
assert_eq!(size_of::<SmallRange<u64>>(), 8); // vs Range<u64> = 16 bytes
assert_eq!(size_of::<SmallRange<usize>>(), 8); // vs Range<usize> = 16 bytes
// Option adds no overhead (niche optimization)
assert_eq!(size_of::<SmallRange<u64>>(), size_of::<Option<SmallRange<u64>>>());
Half the memory footprint means better cache locality. Ideal for:
use small_range::SmallRange;
// Create a range (defaults to u64 storage)
let range = SmallRange::new(10u64, 20u64);
// Access bounds
assert_eq!(range.start(), 10u64);
assert_eq!(range.end(), 20u64); // exclusive, like std Range
assert_eq!(range.len(), 10);
assert!(!range.is_empty());
// Iterate
for i in &range {
println!("{}", i);
}
// Convert to standard Range
let std_range: core::ops::Range<u64> = range.to_range();
use small_range::SmallRange;
use core::mem::size_of;
// SmallRange<u16>: 2 bytes, values 0-254
let r16 = SmallRange::<u16>::new(0, 100);
assert_eq!(size_of::<SmallRange<u16>>(), 2);
// SmallRange<u32>: 4 bytes, values 0-65,534
let r32 = SmallRange::<u32>::new(0, 1000);
assert_eq!(size_of::<SmallRange<u32>>(), 4);
// SmallRange<u64>: 8 bytes, values 0-4,294,967,294 (default)
let r64 = SmallRange::<u64>::new(0, 1_000_000);
assert_eq!(size_of::<SmallRange<u64>>(), 8);
// SmallRange<usize>: convenient for slice indexing
let r_usize = SmallRange::<usize>::new(0, 100);
let data = vec![0; 200];
let slice = &data[r_usize.start()..r_usize.end()];
Start and length must each fit in half the storage width minus 1:
SmallRange<u16>: max start = 254, max length = 254SmallRange<u32>: max start = 65,534, max length = 65,534SmallRange<u64>: max start = 4,294,967,294, max length = 4,294,967,294use small_range::SmallRange;
// This will panic in debug mode (start exceeds capacity)
let invalid = SmallRange::<u16>::new(255, 256);
RangeBounds ImplementationSmallRange does not implement RangeBounds<T> because the trait requires returning references (Bound<&T>), but our values are computed from packed bits -- there's no stored T to reference.
Workaround: Use .to_range() which returns Range<T>, and Range<T> implements RangeBounds<T>:
use small_range::SmallRange;
use core::ops::RangeBounds;
let small = SmallRange::new(10u64, 20u64);
// to_range() gives full RangeBounds support
let range = small.to_range();
assert!(range.contains(&15));
assert!(!range.contains(&25));
The SmallRangeStorage trait is sealed -- only u16, u32, u64, and usize are supported.
Values are packed as (start+1, length+1) where start is in the high bits and length is in the low bits:
SmallRange<u32> in 4 bytes:
+----------------+----------------+
| start + 1 | length + 1 | -> NonZero<u32>
| (16 bits) | (16 bits) |
+----------------+----------------+
SmallRange<u64> in 8 bytes:
+--------------------------------+--------------------------------+
| start + 1 | length + 1 | -> NonZero<u64>
| (32 bits) | (32 bits) |
+--------------------------------+--------------------------------+
By adding 1 to both start and length:
Option::None (niche optimization)len() is a simple mask + subtract operationAll operations are #[inline] and compile to minimal assembly:
new(): Subtract, two adds, shift, ORstart(): Shift, mask, subtractend(): Shift, mask, subtract, addlen(): Mask, subtractNo heap allocation, no branches, no function calls.
See full benchmark results comparing Option<SmallRange> vs Option<Range> with 100 million entries:
use small_range::SmallRange;
use core::mem::{size_of, align_of};
// Transparent wrapper around NonZero<T>
assert_eq!(size_of::<SmallRange<u64>>(), 8);
assert_eq!(align_of::<SmallRange<u64>>(), 8);
// Niche optimization works
assert_eq!(size_of::<Option<SmallRange<u64>>>(), 8);
| Method | Description |
|---|---|
SmallRange::new(start, end) |
Create from start and end values |
SmallRange::default() |
Empty range (0, 0) |
| Method | Returns | Description |
|---|---|---|
start() |
T |
Start bound (inclusive) |
end() |
T |
End bound (exclusive) |
len() |
usize |
Number of elements |
is_empty() |
bool |
True if start == end |
to_range() |
Range<T> |
Convert to std Range |
| Method | Yields | Description |
|---|---|---|
for x in range |
T |
Consuming iteration |
for x in &range |
T |
Borrowing iteration |
| Trait | Notes |
|---|---|
Clone, Copy |
Zero-cost copy |
PartialEq, Eq |
Bitwise comparison |
Hash |
Based on packed bits |
Default |
Empty range (0, 0) |
Debug |
Shows start and end |
IntoIterator |
For both owned and borrowed |
Good fit:
Option<Range> with zero discriminant overheadno_std environments (only depends on num-traits)Poor fit:
RangeBounds trait directly (use .to_range() as workaround)Range<T>)MIT