Why size a ring buffer to a power of two, and when is the index-masking trick not worth it?
answer
- what operation hides inside mod?
- compare division latency to a bitwise and
- the identity holds for one family of N
- count the bytes that rounding up costs
- mask means and with capacity minus one
basics
~10 sA power-of-two capacity makes index mod N exactly index & (N-1): a bitwise mask instead of a division, which matters in tight per-item loops. It stops paying when rounding capacity up wastes real memory.
solid answer
~50 sFor a non-negative index and a capacity N that is an exact power of two, `i mod N` and `i & (N - 1)` agree, because the mask keeps precisely the low bits the remainder would have left. That swaps a one-cycle bitwise operation for an integer division — one of the slowest arithmetic instructions, and one the compiler cannot strengthen away when capacity is a runtime value. In a per-sample capture buffer taking tens of millions of writes a second, that is a real win. It is cargo cult in three cases: when rounding a large capacity up costs close to double the memory; when the per-item work of copying a record dwarfs the index arithmetic; and when `if index == N then index = 0` would do, a branch that predicts almost perfectly. The identity also fails for negative indices, which is how it usually breaks.
go deeper
Know the identity itself: with a power-of-two capacity, reducing an index by the capacity is the same as masking with capacity minus one, and it does not work for other sizes.
Explain why a remainder can be expensive, why a compile-time constant capacity changes the answer, and what the compare-and-reset advance offers instead of a mask.
Show you weigh it: is the reduction on the hot path at all, what does rounding capacity up cost in bytes at your real size, and does a profile actually show the index math.
Own the constraint this puts on the API. A power-of-two-only capacity is a contract every caller inherits and every configuration must respect, so accept it only where the measurement justifies the memory it rounds up.
## The identity For N = 2^k and a non-negative integer i, the remainder `i mod N` is exactly the low k bits of i. Bitwise-and with `N - 1` — a value whose binary form is k ones — keeps precisely those bits and clears everything above. Hence `i & (N - 1) == i mod N`. This is a special case, not a general one: for any N that is not an exact power of two, `N - 1` is not a clean run of ones and the masked result is not the remainder. ## Why anyone bothers Integer division and remainder are among the most expensive arithmetic operations available, with latencies measured in tens of cycles on typical hardware, and they do not pipeline the way cheap arithmetic does. A bitwise-and is about as cheap as an instruction gets. Replacing one with the other in an inner loop that runs tens of millions of times a second is a measurable change. The crucial detail is *when* it is a change at all. If the capacity is a compile-time constant, a modern optimising compiler already replaces the remainder with a multiply-and-shift sequence, so the mask buys you very little. The masking trick pays most when capacity is a **runtime** value — configured, computed, or passed in — because then the compiler has no constant to strengthen and emits a real division. The second real payer is the non-wrapping-counter design, where head and tail only ever increase and the array index is derived on every access. There the reduction is not incidental, it is the indexing step itself, executed on every read and every write. ## The alternative nobody mentions You do not have to reduce at all. If the index is advanced by exactly one each time, `index = index + 1; if index == N then index = 0` is correct for any capacity, uses no division, and consists of one comparison and a branch that is taken once every N iterations — as predictable as branches get. In a loop that is not division-bound this typically measures the same as masking, and it lets you size the buffer to exactly the capacity you want. Reach for it whenever the advance is by one and the capacity is awkward. ## The negative-index trap The identity is stated for non-negative i, and that qualifier is load-bearing. Masking a negative value yields a non-negative result that is not the mathematical remainder you intended. This bites most often not on a stored index but on a computed one — a difference of two cursors, an offset backwards from the newest item, a "look at the sample K positions ago" helper that can go below zero. Keep the values you mask non-negative by construction, or add the capacity before reducing. ## The memory bill Rounding capacity up to a power of two is free at small sizes and expensive at large ones, and this is where the trick most often becomes cargo cult. Rounding a 1,000-slot buffer to 1,024 costs 2.4%. But consider an audio-sample capture buffer holding 90 seconds at 48,000 samples per second: 4,320,000 samples. The next power of two is 8,388,608 — you have paid roughly double the memory of the capture window to save a handful of cycles per sample. Multiply that across many concurrent capture channels and the tradeoff inverts entirely. Your options are to accept the memory, to shrink to 4,194,304 samples (about 87 seconds) if the window can give up three seconds, or to size exactly and use the compare-and-reset advance. ## When the index math is noise If each item stored is a record you copy or serialize into the slot, the per-item cost is dominated by that copy, and the reduction is invisible in a profile. The same goes for a buffer whose throughput is set by something downstream. Optimising the index arithmetic of a structure that spends its time moving bytes is the classic misallocation of effort — measure before you distort the capacity. ## A decision rule Ask three questions. Is the capacity a runtime value, so a real division would be emitted? Is the reduction actually on the hot path, executed once or more per item, with little other per-item work? And what does rounding up cost in bytes at the size you actually need? Two yes answers and a small rounding cost make power-of-two sizing an easy default — it costs nothing but a comment explaining the constraint. A large rounding cost with the reduction off the hot path makes it a habit imported from somewhere it belonged.
- Does the mask identity still hold if the index can be negative?No. `i & (N - 1) == i mod N` is stated for non-negative i; masking a negative value yields a non-negative result that is not the remainder you wanted. The usual source is a computed index rather than a stored one — a difference of two cursors, or an offset backwards from the newest item. Keep masked values non-negative by construction, or add the capacity before reducing.
- A capture buffer must hold 90 seconds at 48 kHz. How does power-of-two sizing interact with that?That is 4,320,000 samples, and the next power of two is 8,388,608 — nearly double the memory, spent to save a few cycles per sample. Three honest options: accept the extra memory if it fits the budget, shrink to 4,194,304 samples (about 87 seconds) if the window can give up three seconds, or size exactly and advance with a compare-and-reset instead of a mask.
- If the capacity is a compile-time constant, how much does masking actually save?Usually very little. Optimising compilers replace a remainder by a constant with a multiply-and-shift sequence, so the division you were afraid of is not emitted in the first place. The masking trick earns its keep mainly when the capacity is a runtime value the compiler cannot see, which is exactly the case in a buffer whose size is configured rather than hard-coded.
saying these in an interview costs you the question
- Claims masking works for any even capacity
- Rounds a large capacity up without counting the bytes
- Assumes a remainder is always a slow division
- Optimises index math while copying whole records per item
- Masks an index that can go negative