The capacity proposal hides a trade-off inside one rounded number.
Suppose a collector should retain up to 1,500 telemetry records during a brief downstream slowdown. The number is a planning target for this example, not a measured burst distribution. Each slot is estimated at 32 bytes for record payload. Metadata, object headers, alignment, allocator behavior, and other process memory are outside that estimate.
A power-of-two ring buffer can use a bit mask to wrap an index: for capacity C = 2k, the index i wraps as i & (C − 1). That is one reason an implementation may require such a
capacity. But a ring buffer does not have to be a power of two; another implementation can
use remainder arithmetic for any positive capacity. The sizing question is therefore
partly mathematical and partly a design choice.
- Target
- 1,500 records in this illustrative burst.
- Candidate rule
- Choose the smallest power-of-two capacity at or above the target.
- Slot estimate
- 32 bytes of payload per slot; overhead excluded.
- Decision
- Does rounding up fit the memory budget, and does the code need a power-of-two capacity?
The exponent counts how many times capacity doubled from one.
Start at one slot. After each doubling, the capacity is multiplied by 2: 1, 2, 4, 8, 16, …. After k doublings, the capacity is 2k. So 210 = 1,024 and 211 = 2,048. This is exact integer arithmetic, not an estimate.
The boundary matters. A request for exactly 2,048 slots stays at exponent 11. A request for 2,049 no longer fits and rounds to 4,096, exponent 12. A one-record change near a power can double the reserved capacity. That step change is easy to miss if you only look at the exponent.
Log₂ answers how many doublings produce a given size.
Powers answer: “If we double k times, how many slots do we have?” The inverse
question is: “How many doublings reach n?” For an exact power, log₂(2k) = k. For example, log₂(2,048) = 11.
When the requested size is not an exact power, log₂(1,500) ≈ 10.5507. A
buffer cannot have 10.5507 levels of doubling, so take the ceiling: ceil(log₂(1,500)) = 11. Then the whole-number capacity is 2¹¹ = 2,048. In integer code, the safest statement is often not “compute a
floating log and round,” but “keep doubling until capacity is at least the request,” while
checking the limit before each multiplication.
Move the input by one slot and watch the reservation cross a boundary.
This calculator rounds a positive integer request to the smallest power of two that covers it. Its upper limit is an authored policy input, not a recommendation for any particular process. The byte result counts only fixed-size payload slots.
Method: start at 1 and double while capacity is below the request. The maximum is checked before each doubling to avoid silently crossing the configured limit.
Use integer doubling to make the boundary visible.
The TypeScript and Go examples accept a positive requested count and an
application-supplied maximum, return the rounded capacity and exponent, and reject a
request that cannot fit. Rather than compute ceil(log₂ n) in floating point, they
repeatedly double an integer capacity and check the maximum before multiplication. Their arithmetic
does not allocate a buffer or prove that the chosen maximum is safe for a particular workload.
Both examples expose the exponent and reject values outside their stated limit.
export type CapacityPlan = {
requested: number;
capacity: number;
levels: number;
unusedSlots: number;
};
/** Round a positive integer request up to a power-of-two capacity. */
export function planPowerOfTwoCapacity(
requested: number,
maximumCapacity = 1_048_576
): CapacityPlan {
if (!Number.isSafeInteger(requested) || requested < 1) {
throw new RangeError('requested must be a positive safe integer');
}
if (!Number.isSafeInteger(maximumCapacity) || maximumCapacity < 1) {
throw new RangeError('maximumCapacity must be a positive safe integer');
}
let capacity = 1;
let levels = 0;
while (capacity < requested) {
if (capacity > Math.floor(maximumCapacity / 2)) {
throw new RangeError('no supported power-of-two capacity can hold the request');
}
capacity *= 2;
levels += 1;
}
if (capacity > maximumCapacity) {
throw new RangeError('no supported power-of-two capacity can hold the request');
}
return { requested, capacity, levels, unusedSlots: capacity - requested };
}
const plan = planPowerOfTwoCapacity(1_500);
console.log(`${plan.capacity} slots = 2^${plan.levels}; ${plan.unusedSlots} remain`);
package main
import (
"errors"
"fmt"
)
type CapacityPlan struct {
Requested uint64
Capacity uint64
Levels uint
UnusedSlots uint64
}
// PlanPowerOfTwoCapacity rounds a positive request up without overflowing.
func PlanPowerOfTwoCapacity(requested, maximumCapacity uint64) (CapacityPlan, error) {
if requested == 0 || maximumCapacity == 0 {
return CapacityPlan{}, errors.New("requested and maximum capacity must be positive")
}
capacity := uint64(1)
var levels uint
for capacity < requested {
if capacity > maximumCapacity/2 {
return CapacityPlan{}, errors.New("no supported power-of-two capacity can hold the request")
}
capacity *= 2
levels++
}
if capacity > maximumCapacity {
return CapacityPlan{}, errors.New("no supported power-of-two capacity can hold the request")
}
return CapacityPlan{
Requested: requested, Capacity: capacity, Levels: levels,
UnusedSlots: capacity - requested,
}, nil
}
func main() {
plan, err := PlanPowerOfTwoCapacity(1_500, 1_048_576)
if err != nil {
panic(err)
}
fmt.Printf("%d slots = 2^%d; %d remain\n", plan.Capacity, plan.Levels, plan.UnusedSlots)
}
The formula checks a rule; it does not justify the rule.
For a mask-based ring index, capacity must be a power of two because C − 1 then has all low-order bits set. For example, capacity 2,048 has mask
2,047. A general ring buffer can instead use index % capacity and support a non-power-of-two
size. The fastest option depends on the language, compiler, hardware, and workload; do not infer
a useful performance difference from the arithmetic alone.
Rounding can also consume substantial space. If a request barely exceeds 2,048, the next capacity is 4,096. For a very large fixed record, the unused slots may dominate memory. A maximum-capacity policy makes this cost explicit, but the real limit should include metadata, concurrent buffers, allocator overhead, and the cost of overflow or data loss.
A request just past the boundary changes the memory decision.
- Requested
- 2,049 records, not 2,048.
- Slot payload
- 32 bytes each, fixed for this estimate.
- Task
- Find the next power-of-two size and explain the memory step.
Show a worked answer
2¹¹ = 2,048 is one record too small, so the next whole exponent is 12: ceil(log₂(2,049)) = 12, and 2¹² = 4,096. That leaves 4,096 − 2,049 = 2,047 slots above this target. Payload storage is 4,096 × 32 B = 131,072 B = 128 KiB, twice the payload allocation for 2,048
slots. The step is exact for these integers; total process memory will be larger.
Before approving that allocation, ask whether 2,049 is a hard minimum or a noisy estimate, whether all records must be retained, and whether a non-power-of-two capacity is acceptable. Measure the occupancy trace and confirm the implementation's overflow semantics.