Надежно измерять распределение СПМ


У меня есть две реализации одного и того же алгоритма. Я хотел бы убедиться, что ни один из них не использует больше памяти, чем необходимо, или, другими словами, что они выделяют точно такое же количество объектов.

Мое текущее решение состоит в том, чтобы измерить количество выделенных байтов до и после процедур с помощью threadMXBean.getThreadAllocatedBytes(threadId) и использовать это в качестве аппроксимации объема памяти.

Проблема в том, что этот метод нестабилен, т. е. иногда он возвращает гораздо большее число, чем это должен. Это особенно заметно на алгоритмах, которые не выделяют объекты. Одним из проблемных примеров является метод, который суммирует int[].

Фактический код (Котлин):

class MemAllocationTest {

    private val threadMXBean = (ManagementFactory.getThreadMXBean() as? com.sun.management.ThreadMXBean)
            ?: throw RuntimeException("Runtime does not support com.sun.management.ThreadMXBean")

    /**
     * May run [block] several times
     * */
    private inline fun measureAllocatedBytes(block: () -> Unit): Long {
        val threadId = Thread.currentThread().id

        val before = threadMXBean.getThreadAllocatedBytes(threadId)
        block()
        val after = threadMXBean.getThreadAllocatedBytes(threadId)

        return after - before
    }

....

Есть ли лучшее решение?

(я не знаю, как это сделать с JMH, но ИМХО это очень близкая тема)

2 3

2 ответа:

JMH имеет -prof gc профилировщик, который должен быть точным при профилировании распределения. Хотя он использует один и тот же ThreadMXBean под прикрытием, он может отфильтровывать эффекты разминки и усреднять икоту по нескольким вызовам @Benchmark. Типичные ошибки находятся в пределах 0.001 байт / ОП там.

Мое текущее решение заключается в сборе статистики с несколькими запусками:

private inline fun stabiliseMeasureAllocatedBytes(block: () -> Unit): Long {
    val runs = List(7) { measureAllocatedBytes(block) }
    val results = runs.drop(2) // skip warm-up

    val counts = results.groupingBy { it }.eachCount()
    val (commonResult, commonCount) = counts.entries.maxBy { (result, count) -> count }!!

    if (commonCount >= results.size / 2)
        return commonResult
    else
        throw RuntimeException("Allocation measurements vary too much: $runs")
}