Skip to main content

Command Palette

Search for a command to run...

Using the Agrona library (Part 3)

Primitive Collections: Maps Without Boxing

Published
7 min read

"Every Integer.valueOf() is a tiny betrayal of performance. Agrona's primitive collections commit zero betrayals."

Java generics require objects. Agrona provides primitive-specialized collections that store int, long, and double values directly in arrays — no boxing, no wrapper objects, no GC pressure.


The Boxing Problem Revisited

// JDK: 3 objects per map entry
HashMap<Integer, Integer> jdkMap = new HashMap<>();
jdkMap.put(42, 100);
// Creates: Integer(42) + Integer(100) + HashMap.Node

// Agrona: 0 objects per map entry
Int2IntHashMap agronaMap = new Int2IntHashMap(Integer.MIN_VALUE);
agronaMap.put(42, 100);
// Creates: nothing. Stores 42 and 100 directly in int[] arrays.

Int2IntHashMap — int→int Mapping

The most commonly used Agrona collection. Stores int keys mapped to int values with zero object allocation:

import org.agrona.collections.Int2IntHashMap;

// The "missing value" — returned when a key doesn't exist (instead of null)
int MISSING = Integer.MIN_VALUE;
Int2IntHashMap map = new Int2IntHashMap(MISSING);

// Basic operations
map.put(1001, 15025);     // instrumentId → price
map.put(1002, 22050);
map.put(1003, 8775);

int price = map.get(1001);         // 15025 (primitive int, no boxing!)
int missing = map.get(9999);       // Integer.MIN_VALUE (missing)
boolean has = map.containsKey(1001); // true
int old = map.remove(1003);        // 8775

// Size and emptiness
map.size();     // 2
map.isEmpty();  // false

The Missing Value Pattern

Since primitives can't be null, Agrona uses a missing value sentinel:

// ❌ JDK: null means "not found"
Integer val = jdkMap.get(unknownKey); // null
if (val == null) { /* not found */ }

// ✅ Agrona: a specific int value means "not found"
int val = agronaMap.get(unknownKey); // returns MISSING_VALUE
if (val == MISSING) { /* not found */ }

// IMPORTANT: You cannot store the missing value as a real value!
// Int2IntHashMap map = new Int2IntHashMap(0);
// map.put(42, 0);  // ⚠ Cannot distinguish "value is 0" from "key not found"!
// Choose a missing value that your data will never use.

How Open Addressing Works

Unlike JDK's HashMap (which uses chaining — linked lists for collisions), Agrona uses open addressing with linear probing:

Linear Probing in Detail

// Conceptual Int2IntHashMap.put():
void put(int key, int value) {
    int slot = hash(key) & mask;  // Start slot

    while (true) {
        if (keys[slot] == MISSING_VALUE) {
            // Empty slot — insert here
            keys[slot] = key;
            values[slot] = value;
            size++;
            return;
        }
        if (keys[slot] == key) {
            // Key exists — update value
            values[slot] = value;
            return;
        }
        // Collision — try next slot (linear probe)
        slot = (slot + 1) & mask;
    }
}

// Conceptual Int2IntHashMap.get():
int get(int key) {
    int slot = hash(key) & mask;

    while (true) {
        if (keys[slot] == MISSING_VALUE) {
            return missingValue;  // Key not found
        }
        if (keys[slot] == key) {
            return values[slot];  // Found!
        }
        slot = (slot + 1) & mask;  // Linear probe
    }
}

Why Linear Probing is Cache-Friendly

Adjacent array elements are in the same cache line (~64 bytes = 16 ints). Probing the next slot is often a cache hit. Following a Node.next pointer to a random heap location is almost always a cache miss.

Load Factor

Agrona maps resize when the load factor (default 0.55) is exceeded:

// Custom initial capacity and load factor
Int2IntHashMap map = new Int2IntHashMap(
    1024,     // initial capacity
    0.67,     // load factor (max 67% full before resize)
    Integer.MIN_VALUE // missing value
);

Lower load factor → fewer collisions → faster lookups → more memory. Higher load factor → more collisions → slower lookups → less memory.


Long2ObjectHashMap — long→Object Mapping

When keys are long primitives but values are objects:

import org.agrona.collections.Long2ObjectHashMap;

Long2ObjectHashMap<String> sessionMap = new Long2ObjectHashMap<>();

sessionMap.put(1234567890L, "session-abc");
sessionMap.put(9876543210L, "session-def");

String session = sessionMap.get(1234567890L); // "session-abc"
// Key is stored as primitive long — no Long boxing!
// Value is still an object reference (unavoidable for non-primitives)

Available Primitive-Key Map Variants


Int2ObjectHashMap — int→Object Mapping

import org.agrona.collections.Int2ObjectHashMap;

Int2ObjectHashMap<Order> orderBook = new Int2ObjectHashMap<>();

orderBook.put(1001, new Order("AAPL", 100, 150.25));
orderBook.put(1002, new Order("MSFT", 200, 380.50));

Order order = orderBook.get(1001);  // No Integer boxing for the key!

IntArrayList & LongArrayList — Primitive Lists

Avoid boxing for list operations:

import org.agrona.collections.IntArrayList;

// JDK: boxes every int
List<Integer> jdkList = new ArrayList<>();
jdkList.add(42);     // Integer.valueOf(42) — allocation!
int val = jdkList.get(0); // Integer.intValue() — unboxing

// Agrona: stores primitive ints directly
IntArrayList agronaList = new IntArrayList();
agronaList.addInt(42);   // No boxing — stored in int[]
int val2 = agronaList.getInt(0); // No unboxing — returns primitive

// Bulk operations
agronaList.addInt(10);
agronaList.addInt(30);
agronaList.addInt(20);
agronaList.sort();       // Sorts using Arrays.sort(int[])

// Also available:
// LongArrayList — for long primitives

IntHashSet & ObjectHashSet

import org.agrona.collections.IntHashSet;
import org.agrona.collections.ObjectHashSet;

// Primitive int set — no boxing
IntHashSet instrumentIds = new IntHashSet();
instrumentIds.add(1001);
instrumentIds.add(1002);
instrumentIds.add(1003);
instrumentIds.contains(1002); // true — no Integer boxing!

// Object set — open addressing (unlike JDK HashSet which wraps HashMap)
ObjectHashSet<String> symbols = new ObjectHashSet<>();
symbols.add("AAPL");
symbols.add("MSFT");
symbols.contains("AAPL"); // true

Key difference from JDK: ObjectHashSet uses open addressing directly, while JDK's HashSet wraps a HashMap with dummy values (extra overhead).


Int2ObjectCache — Set-Associative Cache

A specialized cache with LRU-like eviction using a set-associative design (similar to CPU caches):

import org.agrona.collections.Int2ObjectCache;

// Capacity MUST be a power of 2
Int2ObjectCache<String> cache = new Int2ObjectCache<>(
    1024,  // number of sets
    8,     // associativity (entries per set)
    (key, value) -> {
        // Eviction callback — called when an entry is evicted
        System.out.println("Evicted: " + key + " → " + value);
    }
);

cache.put(42, "Hello");
String val = cache.get(42); // "Hello"

// When a set is full, the oldest entry is evicted

Iteration Without Allocation

JDK iterators allocate Iterator objects. Agrona provides allocation-free iteration:

Int2IntHashMap map = new Int2IntHashMap(Integer.MIN_VALUE);
map.put(1, 100);
map.put(2, 200);
map.put(3, 300);

// ✅ Allocation-free iteration with forEach
map.forEach((key, value) -> {
    System.out.println(key + " → " + value);
});

// ✅ Key-only iteration
map.forEachKey((key) -> {
    process(key);
});

// ⚠️ JDK-compatible iteration (allocates Iterator, but works with for-each)
for (Int2IntHashMap.Entry entry : map.entrySet()) {
    // entry is a flyweight — reused across iterations!
    int key = entry.getIntKey();
    int value = entry.getIntValue();
}

Complete Comparison Table

Agrona Collection JDK Equivalent Boxing Memory/Entry Objects/Entry
Int2IntHashMap HashMap<Integer,Integer> None ~8 B 0
Long2LongHashMap HashMap<Long,Long> None ~16 B 0
Int2ObjectHashMap<V> HashMap<Integer,V> Key only avoided ~12 B 0 (key)
Long2ObjectHashMap<V> HashMap<Long,V> Key only avoided ~16 B 0 (key)
Object2IntHashMap<K> HashMap<K,Integer> Value only avoided ~12 B 0 (value)
IntArrayList ArrayList<Integer> None 4 B 0
LongArrayList ArrayList<Long> None 8 B 0
IntHashSet HashSet<Integer> None ~4 B 0
ObjectHashSet<E> HashSet<E> N/A ~8 B 0 (no Map wrapper)

Summary

The one-liner: Agrona's primitive collections store int/long values directly in arrays using open-addressing hash maps — eliminating boxing, reducing memory 8x, and achieving zero GC pressure in the hot path.

Using the Agrona library (Part 3)