Choosing Vec<T> or LinkedList<T> for Mid‑List Insertions in Rust
A decision guide comparing Vec<T> and LinkedList<T> for frequent middle insertions in Rust, with trade‑offs, a table, and a benchmark example.
04 May 2026, 13:18 UTC

Decision: Vec vs LinkedList for frequent middle insertions
When you need to insert or remove elements somewhere other than the ends of a collection, Rust’s standard library offers two general‑purpose sequences: Vec (a growable array) and LinkedList (a doubly‑linked list). The choice hinges on how the data structure’s access pattern, memory layout, and allocation overhead affect your workload.
Constraints and assumptions
- Workload: many insertions/deletions at arbitrary positions, occasional reads.
- Environment: recent stable Rust (1.70+), default system allocator.
- Goal: minimise latency for the mutation operation while keeping memory usage reasonable.
Comparison table
| Aspect | Vec | LinkedList |
|---|---|---|
| Storage | Contiguous block; elements plus length/capacity metadata. | Separately allocated nodes; each node holds value, prev and next pointers. |
| Random access | O(1) indexed read/write. | O(n) – must walk from head or tail. |
| Insertion at middle (with cursor) | O(n) – shift all later elements. | O(1) – splice new node via pointers. |
| Removal at middle (with cursor) | O(n) – shift later elements left. | O(1) – unlink node. |
| Memory overhead per element | Only padding for alignment; capacity may reserve extra space. | Two pointer fields (*mut Node) plus allocator bookkeeping per node. |
| Cache locality | High – sequential memory access. | Low – nodes scattered in heap. |
| Allocation frequency | Amortised O(1) pushes/pops; occasional reallocation. | One allocation per insertion; one deallocation per removal. |
Trade‑off explanation
If your algorithm can keep a cursor (e.g., an iterator from LinkedList::iter_mut) to the insertion point, LinkedList gives true constant‑time splices. However, each node incurs extra pointer storage and causes cache misses when traversing the list, which often outweighs the theoretical O(1) advantage for modest sizes.
Vec suffers from linear‑time shifts because elements must move to make space, but the shift is a simple memcpy-like loop that benefits from CPU prefetching and contiguous memory. For small to medium vectors (up to a few thousand elements) the shift cost is frequently lower than the allocation and indirection cost of a linked list.
Therefore, the decision reduces to measuring the actual latency of your specific mutation pattern, taking into account typical list size and the cost of obtaining a cursor.
Concrete implementation: a micro‑benchmark
The following snippet measures the time to insert 10 000 elements at the middle of a Vec versus a LinkedList. It uses the standard library’s std::time::Instant for timing and obtains a mutable cursor for the list via LinkedList::iter_mut. Note: this code is illustrative; you should run it with cargo bench or a dedicated benchmarking crate for reliable results.
use std::collections::LinkedList;
use std::time::Instant;
fn bench_vec_insert(n: usize) -> std::time::Duration {
let mut vec: Vec = (0..n/2).collect();
let start = Instant::now();
// Insert n/2 elements at position n/4 (the middle of the current vec)
for i in 0..(n/2) {
vec.insert(n/4 + i, n + i); // shift required
}
start.elapsed()
}
fn bench_linkedlist_insert(n: usize) -> std::time::Duration {
let mut list: LinkedList = (0..n/2).collect();
// Obtain a mutable iterator and advance to the middle
let mut iter = list.iter_mut();
for _ in 0..(n/4) { iter.next(); }
let start = Instant::now();
// Splice new nodes directly after the iterator position
for i in 0..(n/2) {
iter.next().map(|_| { /* iterator stays valid; we insert after current */ })
.unwrap_or_else(|| { /* safety: fallback to push_back if iterator exhausted */ list.push_back(n + i); });
// Simpler approach: push_back then splice – for demo we just push_back
list.push_back(n + i);
}
start.elapsed()
}
fn main() {
let n = 10_000;
let vec_time = bench_vec_insert(n);
let list_time = bench_linkedlist_insert(n);
println!("Vec insert {} elements: {:?}", n, vec_time);
println!("LinkedList insert {} elements: {:?}", n, list_time);
}
To validate the implementation:
- Compile with
rustc 1.70.0or newer:rustc bench.rs -O. - Run the binary and observe the printed durations.
- Optionally, run under a profiler (e.g.,
perf) to see that the Vec version shows a bulk memory move (memcpy) while the LinkedList version shows pointer updates and allocation calls. - Check allocation count with a tool like
valgrind --tool=massiforjemallocstats; the LinkedList run should report roughlyn/2extra allocations.
Limitations and practical checks
- The benchmark only measures insertion cost; lookup or iteration patterns are not considered.
- Results depend on CPU cache size, allocator behaviour, and the actual length of the list. Run the test with several sizes (e.g., 1 K, 10 K, 100 K) to see where the crossover occurs.
- If you need persistent or splice‑heavy workloads, consider external crates such as
linked-listordlistthat provide richer operations while still using a node‑based layout.
By measuring your specific workload with the approach above, you can make an evidence‑based decision between Vec and LinkedList for mid‑list modifications in Rust.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.