What is the difference between Vec and VecDeque

Vec is a single-ended growable array, while VecDeque is a double-ended queue optimized for operations at both ends.

The queue trap in a game loop

You're writing a high-performance game loop. Every frame, the operating system delivers a batch of input events. Your game logic processes those events and generates new ones to queue up for the next frame. You need to add events to the back of the line and pull them off the front. You reach for Vec, write events.insert(0, new_event), and suddenly your 60 FPS game stutters to a crawl. The profiler shows the CPU spending cycles in memmove, shuffling memory around instead of rendering pixels.

The problem isn't your logic. It's the data structure. You treated a queue like a list. Vec is optimized for random access and appending at the end. Inserting at the front forces Vec to shift every existing element to make room. That shift is an O(n) operation. Doing it every frame turns your game into a memory-copying machine. The fix isn't to optimize the shift. The fix is to stop shifting entirely.

Contiguous array vs ring buffer

Vec is a contiguous array. It allocates a single block of memory and stores elements side-by-side. This layout gives you instant access to any element by index and excellent performance for iteration. The CPU cache prefetches data perfectly because it knows the next element is right after the current one. The trade-off is that inserting or removing elements from the middle or front requires moving data.

VecDeque is a double-ended queue backed by a ring buffer. A ring buffer treats a fixed-size array as a circle. When you reach the end, you wrap around to the start. VecDeque manages this wrapping for you. It maintains two indices: head and tail. push_back writes at tail and advances tail. push_front decrements head and writes. If head hits zero, it wraps to the end of the buffer. This allows O(1) insertion and removal at both ends. No data moves. Only the indices change.

The ring buffer trades a tiny bit of index calculation for the ability to grow in both directions without moving data. You pay a small cost for the modulo arithmetic in indexing, but you save the massive cost of shifting elements.

Minimal comparison

use std::collections::VecDeque;

fn main() {
    // Vec: contiguous memory, optimal for random access and stack-like usage.
    let mut vec_data = Vec::new();
    vec_data.push(10);
    vec_data.push(20);
    // Access is direct pointer arithmetic. Extremely fast due to cache locality.
    let val = vec_data[1];

    // VecDeque: ring buffer, optimal for queue-like usage.
    let mut deque_data = VecDeque::new();
    deque_data.push_back(10);
    deque_data.push_front(5); // O(1) at front. No shifting occurs.
    // Access involves index calculation with modulo. Slightly slower than Vec.
    let first = deque_data[0];
}

Don't pay the shifting tax for a queue. If you need front operations, Vec is the wrong tool.

How the ring buffer works

When you create a VecDeque, Rust allocates a buffer and initializes head and tail to zero. The capacity tracks the size of the buffer. As you push elements, tail advances. When you push to the front, head decrements. If head becomes negative, it wraps to capacity - 1. The length is calculated as (tail - head + capacity) % capacity.

This wrapping logic means the logical start of the deque isn't always at index zero of the underlying array. The head index points to the first element, which might be anywhere in the buffer. When you access dq[i], VecDeque computes the actual index as (head + i) % capacity. The modulo operation is the overhead. Modern CPUs handle modulo efficiently, but it's still more work than the simple addition used by Vec.

When the buffer fills up, VecDeque needs to grow. It allocates a new buffer, usually doubling the capacity. It then copies the elements from the old ring to the new ring. Because the old ring might be wrapped, the copy can involve two segments: one from head to the end, and one from the start to tail. This reallocation is O(n), but it happens infrequently. The amortized cost of push operations remains O(1).

The ring buffer is the geometry of efficiency for double-ended operations. It eliminates data movement by embracing circularity.

Iteration and the cache penalty

The ring buffer introduces a discontinuity in memory. If the deque has wrapped, the elements are stored in two separate regions of the buffer. Iterating over a VecDeque might jump from the last element of the buffer back to the first. This jump breaks the CPU's hardware prefetcher. The prefetcher expects sequential access and loads the next cache line automatically. When the iterator jumps, the prefetcher fails, and you get a cache miss.

For large deques, this cache penalty can make iteration slower than Vec, even if the algorithm is identical. Vec iteration is a straight line through memory. The prefetcher keeps the pipeline full. VecDeque iteration has a potential gap. If your workload involves heavy iteration over the entire structure, Vec might still win. If you only access the ends, VecDeque wins.

Index math is cheap, but memory movement is expensive. Choose the structure that moves less data.

Realistic example: Sliding window

Sliding windows are a classic use case for VecDeque. You maintain a fixed-size window of recent values. New values enter at the back, and old values leave at the front. Using Vec would require removing the first element, which shifts the rest of the window. VecDeque pops the front in O(1).

use std::collections::VecDeque;

/// Maintains a sliding window of recent sensor readings.
/// Efficiently discards old data from the front.
fn update_window(window: &mut VecDeque<f64>, max_len: usize, reading: f64) {
    window.push_back(reading);

    // O(1) removal from front.
    // Using Vec here would require window.remove(0), which is O(n).
    if window.len() > max_len {
        window.pop_front();
    }
}

fn main() {
    let mut readings = VecDeque::new();
    update_window(&mut readings, 3, 1.0);
    update_window(&mut readings, 3, 2.0);
    update_window(&mut readings, 3, 3.0);
    update_window(&mut readings, 3, 4.0); // 1.0 is dropped.

    // Window now contains [2.0, 3.0, 4.0].
    assert_eq!(readings.len(), 3);
    assert_eq!(readings[0], 2.0);
}

If your window size is fixed and known at compile time, consider a fixed-size array or ArrayVec to avoid allocation entirely.

Pitfalls and conventions

VecDeque is not a magic bullet for all insertions. It only guarantees O(1) operations at the ends. Inserting in the middle is O(n). VecDeque::insert shifts elements in the ring to make room. If you need sorted insertion or frequent middle modifications, VecDeque will perform poorly. Reach for BinaryHeap for priority queues or BTreeMap for sorted collections.

Slicing VecDeque requires care. You cannot get a single &[T] slice from a VecDeque if the ring is wrapped. The data might be split across two regions. The community convention is to use as_slices(). This method returns a tuple of two slices: (&[T], &[T]). The first slice contains the main contiguous part. The second slice contains the wrapped part, if any. Often the second slice is empty.

// as_slices returns two slices to handle the ring wrap.
// The first slice is the primary segment. The second is the wrapped segment.
let (front_slice, back_slice) = deque_data.as_slices();

// Process the front slice.
for val in front_slice {
    println!("{}", val);
}

// Process the back slice if it exists.
for val in back_slice {
    println!("{}", val);
}

Range indexing is not supported. If you try to use dq[0..2], the compiler rejects it with a trait bound error. VecDeque implements Index for usize, but not for ranges. Use as_slices() or iterate with bounds.

Trust the benchmark. If indexing dominates your profile, Vec might still win. If you need a slice, call as_slices. Don't fight the wrap.

Decision matrix

Use Vec when random access by index dominates your workload; the contiguous layout provides the lowest overhead per access and the best cache performance for iteration.

Use Vec when you only push and pop from the back; it acts as a stack with zero extra overhead compared to a raw array.

Use VecDeque when you need a queue or deque where you frequently push and pop from both the front and the back; it guarantees O(1) operations at both ends without shifting elements.

Use VecDeque when implementing a sliding window or a buffer that grows at one end and shrinks at the other; popping from the front is O(1), whereas removing from the front of a Vec is O(n).

Reach for BinaryHeap when you need to extract the minimum or maximum element efficiently; VecDeque does not maintain order, so finding the min/max requires scanning the whole structure.

Pick the shape that matches your access pattern, not the one that feels familiar.

Where to go next