← Back to Blog

Memory Haven: The Hidden Power of Segmented Pools

When we need a dynamic list, our muscle memory reaches for ArrayList or std::vector. It's the default for a reason: contiguous memory is cache-friendly, and random access is blazing fast. But for high-performance systems dealing with massive datasets or complex object lifecycles, this default choice can silently become your bottleneck.

The problem lies in resizing. When a dynamic array fills up, it must allocate a larger block, copy everything over, and discard the old one. This isn't just a CPU spike; it destroys pointer stability. Any reference or pointer to an element inside that array becomes invalid (or in Java's case, adds significant GC pressure) the moment the array grows.

Enter the Segmented Pool.

What is a Segmented Pool?

The philosophy of a Segmented Pool is simple: Grow, don't move.

Instead of one giant contiguous array that keeps getting replaced, a Segmented Pool is a collection of fixed-size arrays (segments). When the current segment is full, the container allocates a new one and links it. The old segments remain exactly where they are.

Think of it like a notebook. When you fill a page, you don't copy the entire page onto a larger sheet of paper. You just turn the page. The previous page's content stays exactly where you wrote it.

The Trade-off: Stability vs. Access Speed

1. The Stability Win

In languages like C++, this pattern is critical because pointers to elements remain valid even as the container grows. In Java, while object references are stable, the Segmented Pool offers a different advantage: Zero-Copy Growth.

Imagine a list with 10 million items. Growing an ArrayList involves allocating a new array for ~15 million items and copying the old 10 million over. That's a massive "stop-the-world" event for your CPU caches and a huge meal for the Garbage Collector. With a Segmented Pool, adding the 10,000,001st item just means allocating one small array. Zero copying.

2. The Random Access Cost

Nothing comes for free. The price of this stability is Random Access Performance.

In a standard array, accessing index i is one instruction: base + i * size. In a Segmented Pool, it's more complex:

  1. Find the segment: segmentIndex = i / SEGMENT_SIZE
  2. Find the offset: offset = i % SEGMENT_SIZE
  3. Fetch the segment array, then fetch the element.

This adds division, modulo, and an extra pointer dereference. While modern CPUs are good at predicting this, it is strictly slower than a raw array access.

3. Memory Fragmentation

Standard dynamic arrays can cause memory fragmentation by demanding huge contiguous blocks (e.g., finding a 2GB hole in the heap). Segmented Pools break this requirement down into manageable chunks (e.g., thousands of 4KB pages), which plays much nicer with memory allocators.

The Implementation (Java)

Here is a simplified implementation demonstrating the concept. Notice how the getSegmentIdentity proves that the underlying memory blocks (segments) never move, even as we add thousands of items.

import java.util.ArrayList;
import java.util.List;

/**
 * Segmented Pool Demo: Showing how to grow a container without moving old data.
 */
public class SegmentedPool<T> {
    // Power of 2 size helps with bitwise optimizations in real implementations
    private static final int DEFAULT_SEGMENT_SIZE = 1024;
    
    // The list of segments. Each segment is a fixed-size array.
    private final List<Object[]> segments = new ArrayList<>();
    
    private int currentSegmentIndex = -1;
    private int currentOffset = 0;

    public SegmentedPool() {
        addNewSegment();
    }

    private void addNewSegment() {
        segments.add(new Object[DEFAULT_SEGMENT_SIZE]);
        currentSegmentIndex++;
        currentOffset = 0;
    }

    /**
     * Appends an item. If the current segment is full, allocates a new one.
     * Returns the global index.
     */
    public int append(T item) {
        if (currentOffset >= DEFAULT_SEGMENT_SIZE) {
            addNewSegment();
        }

        Object[] currentSegment = segments.get(currentSegmentIndex);
        currentSegment[currentOffset] = item;
        
        int globalIndex = (currentSegmentIndex * DEFAULT_SEGMENT_SIZE) + currentOffset;
        currentOffset++;

        return globalIndex;
    }

    @SuppressWarnings("unchecked")
    public T getAt(int globalIndex) {
        // The Trade-off: Access requires calculation and double-indirection
        int segIdx = globalIndex / DEFAULT_SEGMENT_SIZE;
        int offset = globalIndex % DEFAULT_SEGMENT_SIZE;
        
        if (segIdx > currentSegmentIndex || 
           (segIdx == currentSegmentIndex && offset >= currentOffset)) {
            throw new IndexOutOfBoundsException();
        }

        return (T) segments.get(segIdx)[offset];
    }

    /**
     * Helper to verify memory stability.
     * In an ArrayList, the backing array object changes on resize.
     * Here, it stays the same.
     */
    public int getSegmentIdentity(int globalIndex) {
        int segIdx = globalIndex / DEFAULT_SEGMENT_SIZE;
        return System.identityHashCode(segments.get(segIdx));
    }

    public static void main(String[] args) {
        SegmentedPool<String> pool = new SegmentedPool<>();

        // 1. Add initial item
        int idx1 = pool.append("Base Item");
        int id1 = pool.getSegmentIdentity(idx1);
        
        System.out.println("Initial Segment ID: " + Integer.toHexString(id1));

        // 2. Trigger multiple segment allocations
        System.out.println("Appending 5000 items...");
        for (int i = 0; i < 5000; i++) {
            pool.append("Item " + i);
        }

        // 3. Verify the first segment hasn't moved
        int id2 = pool.getSegmentIdentity(0);
        System.out.println("Final Segment ID: " + Integer.toHexString(id2));
        
        if (id1 == id2) {
            System.out.println("Success: The container grew, but the old data stayed put.");
        }
    }
}

Conclusion

The Segmented Pool isn't a replacement for ArrayList; it's a specialized tool for a specific problem. If you need blazing fast random access and your dataset is small or static, stick with arrays.

But if you are building a system that appends data relentlessly, needs to hold references to that data for a long time, or wants to avoid the latency spikes of massive array copies, the Segmented Pool is your friend. It trades a little bit of access speed for a lot of stability and predictability.