A rare pause may be part of a cheap sequence.
Suppose an array starts with room for 8 elements. When it fills, the implementation allocates a buffer twice as large and copies the old elements into it. This repeats as needed. We will model only the element-copy work, assume allocation succeeds, and treat copying one element as one unit of work. Those are useful assumptions for counting, not a complete runtime benchmark.
To append 41 elements, the capacity sequence is 8 → 16 → 32 → 64. Growth happens before the 9th, 17th, and 33rd elements are written. The resize copies 8, then 16, then 32 existing elements. Appending the 41st element does not resize again because 64 slots are already available.
- Starting capacity
- 8 element slots.
- Growth policy
- Double capacity whenever the buffer is full.
- Work counted
- Existing elements copied during each resize.
- Question
- How many element copies happen while appending 41 elements?
Doubling makes the resize costs a geometric series.
A series is a sum of terms. In a geometric series, each term is the
previous term multiplied by the same ratio. For a starting capacity C₀ and a
doubling policy, if m resizes occur, the copied capacities are C₀, 2C₀, 4C₀, …, C₀·2m−1.
For first term a, common ratio r ≠ 1, and m terms,
the finite sum is Sₘ = a(1 − rm) / (1 − r); when r = 1, it is simply m × a. For doubling, r = 2, so
the capacity-copy sum simplifies to Sₘ = C₀(2m − 1). Here m is the number of resize events, not the number of appends. There are three resizes,
so the substituted calculation is shown below.
The factor 23 − 1 = 7 has no unit; multiplying it by a capacity
measured in elements leaves the answer in elements. You can sanity-check by listing the
terms: 8 + 16 + 32 = 56. A formula is useful partly because it agrees with the
trace.
The same finite addition can answer a timing question. If a client waits 1, 2, 4, and 8
seconds before four later attempts, those scheduled pauses total 1 + 2 + 4 + 8 = 15 seconds under this illustrative schedule. That is timer-only
wait; request durations are excluded. A maximum-delay cap or jitter changes the terms, so sum
the schedule the client actually uses. Retry safety and load are separate questions covered
in the retry lesson.
One total can describe the sequence without describing every step.
The 56 copies are cumulative extra work. The 41 new values are also written once, so this
simple model counts 41 + 56 = 97 element writes. Averaged over 41 appends,
that is 97 ÷ 41 ≈ 2.37 element writes per append. The value is an average for
this particular finite prefix, rounded to two decimal places; it is not a guarantee of
2.37 writes for each operation.
For any number N > 8 of appends under this exact doubling policy, the
copied capacities form a prefix of 8 + 16 + …. Their sum is less than the
final capacity, and that final capacity is less than 2N. So this model has
fewer than 2N resize copies and fewer than 3N total element writes,
including the N new values. This is why the sequence has constant amortized copy-plus-insert
work per append even though a resize append can copy many elements.
Amortized means spreading total cost across a sequence according to a stated accounting model. It is not a probability, and it does not say a slow event cannot happen. A service with a strict per-request latency target may still need preallocation, incremental migration, or another data structure.
Watch the next resize boundary add a whole term.
Change the batch size from 0 to 5,000. The model starts with eight slots and doubles whenever the next append finds the buffer full. Notice how the copy total stays fixed between boundary crossings, then jumps by the old capacity at the next resize.
Resize copy terms (elements): 8 + 16 + 32
Total resize copies: 56 elements
Closed-form cross-check: 8 × (23 − 1) = 56 elements
New-element writes: 41 elements
Total modeled writes: 97 element writes
Average over this batch: 2.37 element writes per append
Count both the resize terms and the inserts.
The examples accept at most 5,000 appends, record each capacity copied, and compare the iterated sum with the closed form. At this bound the counts are small exact integers in both implementations. Try 8 and 9 to see the first boundary, then 40 and 41 to see why the average can fall after a resize even while cumulative work increases.
Both programs count the same resize copies, new writes, and geometric formula.
export type GrowthWork = {
appends: number;
resizedCapacities: number[];
resizeCopies: number;
formulaCopies: number;
totalWrites: number;
averageWritesPerAppend: number | null;
};
/** Count element-copy work for a doubling array, with small explicit safe bounds. */
export function countGrowthWork(appends: number, initialCapacity = 8): GrowthWork {
if (!Number.isSafeInteger(appends) || appends < 0 || appends > 5_000) {
throw new RangeError('Appends must be a whole number from 0 through 5,000.');
}
if (!Number.isSafeInteger(initialCapacity) || initialCapacity < 1 || initialCapacity > 8_192) {
throw new RangeError('Initial capacity must be a whole number from 1 through 8,192.');
}
let capacity = initialCapacity;
const resizedCapacities: number[] = [];
for (let index = 0; index < appends; index += 1) {
if (index === capacity) {
resizedCapacities.push(capacity);
capacity *= 2;
}
}
const resizeCopies = resizedCapacities.reduce((sum, oldCapacity) => sum + oldCapacity, 0);
const formulaCopies = initialCapacity * (2 ** resizedCapacities.length - 1);
if (!Number.isSafeInteger(resizeCopies) || resizeCopies !== formulaCopies) {
throw new RangeError(
'The copy count exceeded the exact integer range or failed its formula check.'
);
}
const totalWrites = appends + resizeCopies;
return {
appends,
resizedCapacities,
resizeCopies,
formulaCopies,
totalWrites,
averageWritesPerAppend: appends === 0 ? null : totalWrites / appends
};
}
console.log(countGrowthWork(41));
package main
import (
"errors"
"fmt"
)
type GrowthWork struct {
Appends int
ResizedCapacities []int
ResizeCopies int
FormulaCopies int
TotalWrites int
AverageWritesPerAppend *float64
}
// Count element-copy work for a doubling array, with small explicit bounds.
func countGrowthWork(appends, initialCapacity int) (GrowthWork, error) {
if appends < 0 || appends > 5_000 {
return GrowthWork{}, errors.New("appends must be a whole number from 0 through 5,000")
}
if initialCapacity < 1 || initialCapacity > 8_192 {
return GrowthWork{}, errors.New("initial capacity must be from 1 through 8,192")
}
capacity := initialCapacity
resizedCapacities := make([]int, 0)
for index := 0; index < appends; index++ {
if index == capacity {
resizedCapacities = append(resizedCapacities, capacity)
capacity *= 2
}
}
resizeCopies := 0
for _, oldCapacity := range resizedCapacities {
resizeCopies += oldCapacity
}
formulaCopies := initialCapacity * ((1 << len(resizedCapacities)) - 1)
if resizeCopies != formulaCopies {
return GrowthWork{}, errors.New("iterated copy count did not match the geometric sum")
}
totalWrites := appends + resizeCopies
var average *float64
if appends > 0 {
value := float64(totalWrites) / float64(appends)
average = &value
}
return GrowthWork{
Appends: appends, ResizedCapacities: resizedCapacities,
ResizeCopies: resizeCopies, FormulaCopies: formulaCopies,
TotalWrites: totalWrites, AverageWritesPerAppend: average,
}, nil
}
func main() {
work, err := countGrowthWork(41, 8)
if err != nil {
panic(err)
}
fmt.Printf("copied capacities %v; resize copies %d; formula copies %d; total writes %d; average %.2f writes/append\n",
work.ResizedCapacities, work.ResizeCopies, work.FormulaCopies, work.TotalWrites, *work.AverageWritesPerAppend)
}
Ask what changes when the capacity policy changes.
Suppose the batch has 70 elements and starts with capacity 16. Under doubling, resize
copies are 16 + 32 + 64 = 112 elements. Including the 70 new values, the
model counts 182 element writes. The geometric formula gives 16 × (23 − 1) = 112 copies. This is a new input, so do not carry over the 8-slot result.
Now change the design: if the batch size is known and the system reserves 70 slots before appending, this model has 70 writes and no growth copies. The reservation trades spare memory and up-front allocation for fewer resize events. If inputs arrive incrementally or may be much larger than expected, the reservation itself needs a justified bound.
- Boundary check
- At 8 appends: no resize. At 9: copy 8 existing elements.
- Exact later edge
- At 40: capacities 8, 16, and 32 have been copied; the array has 56 copy work units.
- Next append
- At 41, capacity is still 64, so no additional resize copy occurs.
- Decision
- For throughput, estimate aggregate operations; for latency, inspect boundary operations and allocation behavior.