Contiguous Representation and Geometric Growth
At the machine level, a dynamic array (such as C++ std::vector<T>, Rust Vec<T>, or Java ArrayList<E>) is a 24-byte header struct stored on the stack or inline within an object: a base pointer T* data, a current element count size_t size, and an allocated slot count size_t capacity. Whenever size < capacity, appending an element writes directly to data[size++] in a single store instruction with zero allocator interaction.
When size == capacity, the container must allocate a new contiguous region of capacity $\lceil k \cdot C \rceil$, move or copy the existing $C$ elements into the new region, destroy the old elements (if non-trivially destructible), and free the old block. If the array grew arithmetically by a constant $c$ (e.g., adding 100 slots each time), appending $N$ elements would copy $c + 2c + 3c + \dots + N = \Theta(N^2)$ elements, yielding $\Theta(N)$ amortized cost per append. Geometric growth by $k > 1$ ensures that reallocation distances grow exponentially, bounding total copies to a geometric series:
$$\sum_{i=0}^{\lfloor \log_k N \rfloor} k^i = \frac{k^{\lfloor \log_k N \rfloor + 1} - 1}{k - 1} \le \frac{k N - 1}{k - 1} = \Theta(N)$$
Dividing by $N$ insertions yields $1 + \frac{k}{k - 1} = O(1)$ amortized work per append. Notice the trade-off embedded in $\frac{k}{k - 1}$: as $k$ decreases toward $1$, wasted slack memory $(k - 1)N$ shrinks, but the copy multiplier $\frac{1}{k - 1}$ explodes (for $k = 2$, at most $2N$ copies occur; for $k = 1.5$, at most $3N$ copies occur; for $k = 1.1$, $11N$ copies occur).
Suppose a dynamic array grows consecutively within a contiguous arena without intervening external allocations, freeing each previous block of size $C, kC, k^2C, \dots, k^{m-1}C$ as it allocates $k^m C$. Can the $m$-th allocation ever fit inside the coalesced sum of all previously freed blocks?
For the $m$-th block of size $k^m$ to fit into the coalesced sum of the prior blocks $\sum_{i=0}^{m-1} k^i$, we require:
$$k^m \le \sum_{i=0}^{m-1} k^i = \frac{k^m - 1}{k - 1}$$
For $k = 2$, $2^m > 2^m - 1$ for every $m \ge 1$! A $2\times$ growth factor guarantees that the newly requested block is always strictly larger (by 1 initial unit) than the sum of every block the vector has ever freed in its lifetime. In the asymptotic limit as $m \to \infty$, the critical polynomial is $k^2 - k - 1 \le 0$, whose positive root is the Golden Ratio $\varphi = \frac{1 + \sqrt{5}}{2} \approx 1.618$. Therefore, any growth factor $1 < k < 1.618$ (such as $k = 1.5$) eventually allows a coalescing allocator to satisfy future reallocations purely from the vector's own previously freed address range.
Comparative Growth Strategies Across Runtimes
| Runtime / Implementation | Growth Factor ($k$) | Amortized Element Moves per Push | Max Slack Overhead | In-Arena Coalesced Reuse |
|---|---|---|---|---|
GCC libstdc++ std::vector / Rust Vec |
$2.0\times$ | $1 / (2 - 1) = 1.0$ | $50\%$ of capacity | Never ($2^m > \sum_{i<m} 2^i$) |
MSVC std::vector / Java ArrayList |
$1.5\times$ (c + (c >> 1)) |
$1 / (1.5 - 1) = 2.0$ | $33.3\%$ of capacity | Yes, after 5th resize ($1.5^4 < 1 + 1.5 + 2.25 + 3.375$) |
Meta folly::fbvector |
$1.5\times$ + jemalloc size classes |
$\approx 2.0$ (zero-copy via xallocx when possible) |
Aligned to jemalloc bin | Yes + in-place expansion via xallocx |
Go slice (runtime.growslice, Go 1.18+) |
$2.0\times$ ($<256$), smoothly decays to $1.25\times$ | $1.0 \to 4.0$ for large slices | $20\% - 50\%$ | Yes for large slices ($k \le 1.25 < \varphi$) |
Visualizing $k=2.0$ Heap Crawl vs. $k=1.5$ Coalesced Reuse
Reallocation Lifecycle & Exception Safety
size == capacity"] --> B["Compute new_cap =
cap + (cap >> 1)"] B --> C["Allocate raw buffer
aligned_alloc(new_cap)"] C --> D{"is_nothrow_move
_constructible?"} D -- "Yes (Fast Path)" --> E["Move-construct elements
or memcpy if trivial"] D -- "No (Strong Guarantee)" --> F["Copy-construct elements
rollback on throw"] E --> G["Destroy old elements
& free old buffer"] F --> G
realloc, and Page Table RemappingIn C++, std::vector cannot blindly call C's realloc() on arbitrary objects because C++ types may contain internal self-referential pointers (such as std::string's Small String Optimization buffer pointer in libstdc++). However, if a type is trivially copyable (or marked FollyIsRelocatable), expansion uses vectorized AVX-512 memcpy at L1/DRAM bandwidth speeds. Even better, when a dynamic array exceeds the mmap threshold (typically 128 KB in glibc ptmalloc), calling mremap() on Linux grows the allocation in $O(1)$ physical memory operations by simply rewiring virtual page table entries (PTEs) to a new virtual address range without copying a single byte of physical DRAM!
Step through push(x) operations as capacity doubles (1 → 2 → 4 → 8 → 16). Watch Tarjan’s potential function Φ = 2n − m bank 2 credits on cheap inserts to pay for O(n) reallocation copies.
Invariant & State
m = 1 | Size n = 1 | Load Factor α = 100%
Production Implementation: Aligned 1.5× Vector with Strong Exception Guarantee
#include <cstdlib>
#include <cstring>
#include <memory>
#include <new>
#include <type_traits>
#include <utility>
template <typename T>
class FbStyleVector {
T* data_ = nullptr;
std::size_t size_ = 0;
std::size_t cap_ = 0;
static std::size_t next_capacity(std::size_t current) noexcept {
if (current == 0) return 4;
if (current < 64) return current * 2;
// 1.5x growth (k = 1.5 < phi) enables coalesced heap reuse
return current + (current >> 1);
}
void grow() {
const std::size_t new_cap = next_capacity(cap_);
T* new_data = static_cast<T*>(::operator new(new_cap * sizeof(T), std::align_val_t{alignof(T)}));
if constexpr (std::is_trivially_copyable_v<T>) {
if (size_ > 0) {
std::memcpy(new_data, data_, size_ * sizeof(T));
}
} else if constexpr (std::is_nothrow_move_constructible_v<T>) {
std::uninitialized_move_n(data_, size_, new_data);
std::destroy_n(data_, size_);
} else {
// Strong exception guarantee: copy first; if an exception is thrown,
// destroy already-copied elements in new_data and leave data_ intact.
std::size_t constructed = 0;
try {
for (; constructed < size_; ++constructed) {
std::construct_at(new_data + constructed, data_[constructed]);
}
} catch (...) {
std::destroy_n(new_data, constructed);
::operator delete(new_data, std::align_val_t{alignof(T)});
throw;
}
std::destroy_n(data_, size_);
}
if (data_) {
::operator delete(data_, std::align_val_t{alignof(T)});
}
data_ = new_data;
cap_ = new_cap;
}
public:
void push_back(const T& value) {
if (size_ == cap_) grow();
std::construct_at(data_ + size_, value);
++size_;
}
std::size_t size() const noexcept { return size_; }
std::size_t capacity() const noexcept { return cap_; }
};
noexcept on Move Constructors & Reference InvalidationTwo bugs routinely cripple C++ std::vector performance and safety in production:
- Missing
noexcepton custom Move Constructors: Becausestd::vector::push_backguarantees the Strong Exception Guarantee (if reallocation fails partway, the vector remains unchanged),std::move_if_noexceptrefuses to call a throwing move constructor during resize—falling back to deep copy construction of every element! Always mark move constructors and move assignment operatorsnoexcept. - Self-Aliasing in
v.push_back(v[0]): Ifsize == capacity, reallocating must construct the new element before freeing the old buffer, or passing an element reference from inside the vector itself will read from freed memory (use-after-free).