← Math in Practice
Concept Capacity, powers, and logarithms

Powers of two and logarithms

One relationship answers both ‘how large?’ and ‘how many doublings?’

A telemetry collector is being sized for a short burst of records. The estimate says the buffer needs to hold 1,500 records, and an implementation proposal rounds that to 2,048. Why that number? What does the extra capacity cost, and what does the exponent 11 tell us?

The judgment to keep

Use powers of two when a design has a reason for them. Double to find the next capacity; use log₂ to ask how many doublings reach a capacity. Then inspect the boundary, memory budget, and whether the implementation actually needs this constraint.

TypeScriptGo Repeated doubling · exact powers · log₂ · capacity boundaries
01 / Read the burst target

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.

Case file / Telemetry burst Choose a slot capacity that covers the stated target.
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?
02 / Follow the doublings

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.

Below the target2¹⁰ = 1,024 < 1,500
Smallest capacity that fits2¹¹ = 2,048 ≥ 1,500
Spare slots at the target2,048 − 1,500 = 548
Payload storage estimate2,048 × 32 B = 65,536 B = 64 KiB
03 / Ask the inverse question

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.

Forward question2ᵏ = capacity
Inverse questionlog₂(capacity) = k
Whole levels for arbitrary request nk = ceil(log₂ n)
Integer equivalentsmallest k such that 2ᵏ ≥ n
04 / Change the request

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.

Rounded capacity2,048
Doublings / levels11 · 211
Slots above target548
Payload storage 65,536 B

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.

05 / Practice in code

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.

Compare the same capacity rule in TypeScript and Go.

Both examples expose the exponent and reject values outside their stated limit.

TypeScriptPower-of-two capacity · bounded integer doubling
capacity.ts
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`);
GoPower-of-two capacity · bounded integer doubling
capacity.go
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)
}
06 / Check the design limits

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.

07 / Carry the method over

A request just past the boundary changes the memory decision.

Practice / Transfer A replay worker must retain 2,049 compact records.
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.

Rule to carry: powers tell you capacity from a level; logarithms tell you the level from capacity. For a whole-number requirement, round the level up, then check the next boundary and the cost of the capacity it selects.