Почему повторное распределение памяти наблюдается медленнее при использовании Epsilon против G1?

Мне было любопытно измерить время, потраченное на выделение памяти в JDK 13 с использованием G1 и Epsilon. Результаты, которые я наблюдал, являются неожиданными, и мне интересно понять, что происходит. В конечном счете, я пытаюсь понять, как сделать использование Epsilon более производительным, чем G1 (или, если это невозможно, почему).

Я написал небольшой тест, который многократно выделяет память. В зависимости от ввода из командной строки: либо

  • создаст 1024 новых массива 1 МБ, либо
  • создаст 1024 новых массива 1 МБ, измерит время вокруг выделения и распечатает прошедшее время Это не измеряет только само распределение, и включает время, прошедшее для чего-либо еще, что происходит между двумя вызовами к System.nanoTime() - тем не менее, это, кажется, полезный сигнал для прослушивания.

Вот код:

public static void main(String[] args) {
    if (args[0].equals("repeatedAllocations")) {
        repeatedAllocations();
    } else if (args[0].equals("repeatedAllocationsWithTimingAndOutput")) {
        repeatedAllocationsWithTimingAndOutput();
    }
}

private static void repeatedAllocations() {
    for (int i = 0; i < 1024; i++) {
        byte[] array = new byte[1048576]; // allocate new 1MB array
    }
}

private static void repeatedAllocationsWithTimingAndOutput() {
    for (int i = 0; i < 1024; i++) {
        long start = System.nanoTime();
        byte[] array = new byte[1048576]; // allocate new 1MB array
        long end = System.nanoTime();
        System.out.println((end - start));
    }
}

Вот информация о версии для JDK, который я использую:

$ java -version
openjdk version "13-ea" 2019-09-17
OpenJDK Runtime Environment (build 13-ea+22)
OpenJDK 64-Bit Server VM (build 13-ea+22, mixed mode, sharing)

Вот различные способы запуска программа:

  • только выделение с использованием G1: $ time java -XX:+UseG1GC Scratch repeatedAllocations
  • только выделение, Epsilon: $ time java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC Scratch repeatedAllocations
  • выделение + синхронизация + выход с использованием G1: $ time java -XX:+UseG1GC Scratch repeatedAllocationsWithTimingAndOutput
  • выделение + синхронизация + выход, эпсилон: time java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC Scratch repeatedAllocationsWithTimingAndOutput

Вот некоторые временные параметры запуска G1 только с выделениями:

$ time java -XX:+UseG1GC Scratch repeatedAllocations
real    0m0.280s
user    0m0.404s
sys     0m0.081s

$ time java -XX:+UseG1GC Scratch repeatedAllocations
real    0m0.293s
user    0m0.415s
sys     0m0.080s

$ time java -XX:+UseG1GC Scratch repeatedAllocations
real    0m0.295s
user    0m0.422s
sys     0m0.080s

$ time java -XX:+UseG1GC Scratch repeatedAllocations
real    0m0.296s
user    0m0.422s
sys     0m0.079s

Вот некоторые временные параметры от запуска Epsilon только с распределениями:

$ time java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC Scratch repeatedAllocations
real    0m0.665s
user    0m0.314s
sys     0m0.373s

$ time java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC Scratch repeatedAllocations
real    0m0.652s
user    0m0.313s
sys     0m0.354s

$ time java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC Scratch repeatedAllocations
real    0m0.659s
user    0m0.314s
sys     0m0.362s

$ time java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC Scratch repeatedAllocations
real    0m0.665s
user    0m0.320s
sys     0m0.367s

С или без синхронизации + выход, G1 быстрее, чем Epsilon. В качестве дополнительного измерения, используя временные числа из repeatedAllocationsWithTimingAndOutput, среднее время распределения больше при использовании Epsilon. В частности, один из локальных прогонов показал, что G1GC в среднем составлял 227,218 нанос на распределение, тогда как Epsilon составлял в среднем 521,217 нанос (я записал выходные числа, вставил их в электронную таблицу и использовал функцию average для каждого набора чисел).

Я ожидал, что тесты Эпсилона будут заметно быстрее, однако на практике я вижу примерно в 2 раза медленнее. Максимальное время выделения с G1 определенно выше, но только с перерывами - большинство распределений G1 значительно медленнее, чем у Epsilon, почти на один порядок медленнее.

Вот график 1024 раз из repeatedAllocationsWithTimingAndOutput() с G1 и Эпсилоном. Темно-зеленый - для G1; светло-зеленый для Эпсилон; Ось Y - «нанос на распределение»; Меньшие линии сетки по оси Y каждые 250000 нанос. Это показывает, что время выделения Epsilon очень стабильно, каждый раз около 300-400 тыс. Нано. Это также показывает, что время G1 значительно быстрее в большинстве случаев, но также периодически - в 10 раз медленнее, чем у Epsilon. Я предполагаю, что это может быть связано с работой сборщика мусора, что было бы нормальным и нормальным, но также, кажется, сводит на нет идею, что G1 достаточно умен, чтобы знать, что ему не нужно выделять какую-либо новую память.

[+1113] [+1113]

16
задан kaan 25 September 2019 в 02:14
поделиться

2 ответа

комментарий @Holger выше объясняет часть, которую я пропускал в исходном тесте †“получение новой памяти от ОС, является более дорогим, чем переработка памяти в JVM. Комментарий @the8472 указал, что код приложения не сохранял ссылки ни на один из выделенных массивов, таким образом, тест не тестировал то, что я хотел. Путем изменения теста для хранения ссылки на каждый новый массив результаты теперь показывают Эпсилон, превосходящий G1 по характеристикам.

Вот то, что я сделал в коде для сохранения ссылок. Определите это как членскую переменную:

static ArrayList<byte[]> savedArrays = new ArrayList<>(1024);

затем добавляют это после каждого выделения:

savedArrays.add(array);

выделения Эпсилона подобны прежде, который ожидается:

$ time java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC Scratch repeatedAllocations
real    0m0.587s
user    0m0.312s
sys     0m0.296s

$ time java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC Scratch repeatedAllocations
real    0m0.589s
user    0m0.313s
sys     0m0.297s

$ time java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC Scratch repeatedAllocations
real    0m0.605s
user    0m0.316s
sys     0m0.313s

времена G1 теперь намного медленнее, чем прежде и также медленнее, чем Эпсилон:

$ time java -XX:+UseG1GC Scratch repeatedAllocations
real    0m0.884s
user    0m1.265s
sys     0m0.538s

$ time java -XX:+UseG1GC Scratch repeatedAllocations
real    0m0.884s
user    0m1.251s
sys     0m0.533s

$ time java -XX:+UseG1GC Scratch repeatedAllocations
real    0m0.864s
user    0m1.214s
sys     0m0.528s

Повторное выполнение времен на выделение с помощью repeatedAllocationsWithTimingAndOutput(), средние числа теперь соответствуют Эпсилону, являющемуся быстрее.

average time (in nanos) for 1,024 consecutive 1MB array allocations
Epsilon 491,665
G1      883,981
3
ответ дан 30 November 2019 в 17:17
поделиться

Я полагаю, что Вы видите затраты на обеспечение электричеством памяти на первом доступе.

В случае Эпсилона, выделения всегда достигают новой памяти, что означает, что сама ОС должна обеспечить электричеством физические страницы к процессу JVM. В случае G1 происходит то же самое, но после первого цикла GC, это выделило бы объекты в уже обеспеченной электричеством памяти. G1 испытал бы случайные переходы задержки, коррелируемые с паузами GC.

, Но существуют особенности ОС. По крайней мере, на Linux, когда JVM (или действительно, любой другой процесс) "резервы" и память "фиксаций" память не на самом деле обеспечена электричеством: это , физические страницы еще не присвоены ему. Как оптимизация, Linux приводит в порядок этот провод на первом доступе для записи к странице. То действие ОС проявило бы как sys%, между прочим, который является, почему Вы видите его в синхронизациях.

И это - возможно правильная вещь для ОС, чтобы сделать при оптимизации места, например, много процессов, работающих на машине, (пред-) выделяющий большую память, но едва использующий ее. Это произошло бы с, скажем, -Xms4g -Xmx4g: ОС счастливо сообщила бы, что вся 4G "фиксируется", но ничего еще не произошло бы, пока JVM не начнет писать там.

Все это - подход к этому странному приему: предварительное касание всей памяти "кучи" в JVM запускается с -XX:+AlwaysPreTouch (уведомление head, это самые первые образцы):

$ java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC -Xms4g -Xmx4g \
       Scratch repeatedAllocationsWithTimingAndOutput | head
491988
507983
495899
492679
485147

$ java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC -XX:+AlwaysPreTouch -Xms4g -Xmx4g \
       Scratch repeatedAllocationsWithTimingAndOutput | head
45186
42242
42966
49323
42093

И здесь, выполнение из поля действительно заставляет Эпсилон выглядеть хуже, чем G1 (уведомление tail, это самые последние образцы):

$ java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC -Xms4g -Xmx4g \
       Scratch repeatedAllocationsWithTimingAndOutput | tail
389255
386474
392593
387604
391383

$ java -XX:+UseG1GC -Xms4g -Xmx4g \
  Scratch repeatedAllocationsWithTimingAndOutput | tail
72150
74065
73582
73371
71889

..., но это изменяется, как только обеспечение электричеством памяти вне изображения (уведомление tail, это самые последние образцы):

$ java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC -XX:+AlwaysPreTouch -Xms4g -Xmx4g \
       Scratch repeatedAllocationsWithTimingAndOutput | tail
42636
44798
42065
44948
42297

$ java -XX:+UseG1GC -XX:+AlwaysPreTouch -Xms4g -Xmx4g \
        Scratch repeatedAllocationsWithTimingAndOutput | tail
52158
51490
45602
46724
43752

G1 улучшается также, потому что он касается небольшого количества новой памяти после каждого цикла. Эпсилон немного быстрее, потому что он имеет меньше материала, чтобы сделать.

В целом, это - то, почему -XX:+AlwaysPreTouch рекомендуемая опция для low-latency/high-throughput рабочих нагрузок, которые могут принять оплачиваемую авансом стоимость запуска и оплачиваемую авансом оплату места RSS.

UPD: Задумайтесь об этом, это - Эпсилон ошибка UX, и простые особенности должны произвести предупреждение пользователям .

29
ответ дан 30 November 2019 в 17:17
поделиться
Другие вопросы по тегам:

Похожие вопросы: