Andrey Minogin

Reliability and Performance of LLM, JVM and Postgres systems

Reducing memory usage 10 times with High-Performance Primitive Collections

Kotlin basic types such as Int or Double correspond to high-performance Java primitive types such as int or double. But nullable (Int?) and generic (<Int>) versions of those types are mapped to boxed Java types such as Integer or Double.

Boxed types are memory heavy. Let’s make a simple comparison.

Kotlin
@Test
fun `memory occupied by primitive int`() {
    data class A(
        val x: Int
    )

    val N = 100_000_000

    val mem1 = calculateOccupiedMemoryMB()

    val list = List(N) { A(it) }

    val mem2 = calculateOccupiedMemoryMB()

    println("Occupied memory: ${mem2 - mem1} MB")

    list
}

> Occupied memory: 1910 MB
Kotlin
@Test
fun `memory occupied by boxed Int`() {
    data class A(
        val x: Int?
    )

    val N = 100_000_000

    val mem1 = calculateOccupiedMemoryMB()

    val list = List(N) { A(it) }

    val mem2 = calculateOccupiedMemoryMB()

    println("Occupied memory: ${mem2 - mem1} MB")

    list
}

> Occupied memory: 3436 MB

We already see almost 2x difference, but actually it’s more serious as our test is not accurate enough.

Code explained

calculateOccupiedMemoryMB measures the diff between total and occupied memory running garbage collection for at least 3 seconds in advance to reduce the garbage footprint.

Kotlin
fun calculateOccupiedMemoryMB(): Int {
    getRuntime().gc()
    Thread.sleep(3000)
    return ((getRuntime().totalMemory() - getRuntime().freeMemory()) / (1024 * 1024)).toInt()
}

list reference at the end of the block is a trick to avoid JVM optimization. If JVM sees an object is not used it might wipe it off the RAM.

What if we need a huge Set of Int‘s or a huge Map of Int to Object? Unfortunately standard Java Collections are based on generics which means all of the objects will be autoboxed.

Here HPPC: High Performance Primitive Collections comes to the rescue. This library has predefined collection for all the primitive types.

Let’s compare memory footprints of a normal Java HashSet<Int> and a corresponding HPPC IntHashSet.

Kotlin
@Test
fun `memory occupied by HashSet`() {
    val N = 100_000_000

    val mem1 = calculateOccupiedMemoryMB()

    val set = hashSetOf<Int>()
    repeat(N) { set.add(it) }

    val mem2 = calculateOccupiedMemoryMB()

    println("Occupied memory: ${mem2 - mem1} MB")

    1 in set
}

> Occupied memory: 5098 MB
Kotlin
@Test
fun `memory occupied by HashSet`() {
    val N = 100_000_000

    val mem1 = calculateOccupiedMemoryMB()

    val set = IntHashSet()
    repeat(N) { set.add(it) }

    val mem2 = calculateOccupiedMemoryMB()

    println("Occupied memory: ${mem2 - mem1} MB")

    1 in set
}

> Occupied memory: 518 MB

10 times less memory used!


Have a problem like this in your own system? I start with two sessions on it — €2,000 fixed. How it works →

Andrey Minogin — software architect, 15+ years on data-intensive systems. I find out why a system in production is slow, expensive or wrong — in Java and Kotlin services, PostgreSQL and LLM workflows.

When the head of one of the world’s top PostgreSQL consultancies reviewed database work I had done, his conclusion after a day on site: there was nothing he could add.

Work with me · LinkedIn · andrey@minogin.com