Чем можно объяснить огромное снижение производительности при написании ссылки на кучу?
Изучая более тонкие последствия сборщиков мусора для поколений для производительности приложений, я обнаружил ошеломляющее несоответствие в производительности очень простой операции - простая запись в кучу мест - относительно того, является ли написанное значение примитивным или ссылочным.
Микробенчмарк@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 1, time = 1)
@Measurement(iterations = 3, time = 1)
@State(Scope.Thread)
@Threads(1)
@Fork(2)
public class Writing
{
static final int TARGET_SIZE = 1024;
static final int[] primitiveArray = new int[TARGET_SIZE];
static final Object[] referenceArray = new Object[TARGET_SIZE];
int val = 1;
@GenerateMicroBenchmark
public void fillPrimitiveArray() {
final int primitiveValue = val++;
for (int i = 0; i < TARGET_SIZE; i++)
primitiveArray[i] = primitiveValue;
}
@GenerateMicroBenchmark
public void fillReferenceArray() {
final Object referenceValue = new Object();
for (int i = 0; i < TARGET_SIZE; i++)
referenceArray[i] = referenceValue;
}
}
Результаты, достиженияBenchmark Mode Thr Cnt Sec Mean Mean error Units
fillPrimitiveArray avgt 1 6 1 87.891 1.610 nsec/op
fillReferenceArray avgt 1 6 1 640.287 8.368 nsec/op
Посколькувесь цикл почти в 8 раз медленнее, сама запись, вероятно, более чем в 10 раз медленнее. Что может объяснить такое замедление?
Скорость записи примитивного массива составляет более 10 записей в наносекунду. Возможно, я должен задать обратную сторону моего вопроса:что делает примитивное письмо таким быстрым? (Кстати, япроверено, время масштабируется линейно с размером массива.)
Обратите внимание, что это все однопоточное; указав@Threads(2)
увеличит оба измерения, но соотношение будет одинаковым.
Объект в молодом поколении может оказаться достижимым только от объекта в старом поколении. Чтобы избежать сбора живых объектов, сборщик YG должен знать о любых ссылках, которые были записаны в область «Старое поколение» со времени последней коллекции YG. Это достигается с помощью своего рода "грязный флаг стол ", называетсякарточный стол, который имеет один флаг для каждого блока из 512 байтов кучи.
"некрасиво» часть схемы приходит, когда мы понимаем, что каждая запись ссылки должна сопровождатьсяинвариант карточного стола-сопровождающий фрагмент кода: место в карточном столе, которое защищает записываемый адрес, должно быть помечено какгрязный, Этот кусок кода называетсянаписать барьер.
В конкретном машинном коде это выглядит следующим образом:
lea edx, [edi+ebp*4+0x10] ; calculate the heap location to write
mov [edx], ebx ; write the value to the heap location
shr edx, 9 ; calculate the offset into the card table
mov [ecx+edx], ah ; mark the card table entry as dirty
И это все, что требуется для той же операции высокого уровня, когда записанное значение является примитивным:
mov [edx+ebx*4+0x10], ebp
Барьер записи, кажется, способствуетпросто" еще одну запись, но мои измерения показывают, что это вызываетзамедление на порядок, Я могу'объяснить это.
UseCondCardMark
только делает это хужеСуществует довольно неясный флаг JVM, который должен избегать записи в карточный стол, если запись уже помечена как грязная. Это важно, в первую очередь, в некоторых вырожденных случаях, когда много записей картложный обмен между потоками через кэш процессора. Во всяком случае, я попытался с этим флагом:
with -XX:+UseCondCardMark:
Benchmark Mode Thr Cnt Sec Mean Mean error Units
fillPrimitiveArray avgt 1 6 1 89.913 3.586 nsec/op
fillReferenceArray avgt 1 6 1 1504.123 12.130 nsec/op