Чем можно объяснить огромное снижение производительности при написании ссылки на кучу?

Изучая более тонкие последствия сборщиков мусора для поколений для производительности приложений, я обнаружил ошеломляющее несоответствие в производительности очень простой операции - простая запись в кучу мест - относительно того, является ли написанное значение примитивным или ссылочным.

Микробенчмарк
@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

Ответы на вопрос(1)

Ваш ответ на вопрос