Статьи

Неожиданное распределение — JIT-компиляция Jitter

Работая над ByteWatcher (см. Мой последний пост ), я наткнулся на что-то довольно странное.

Это фактический фрагмент кода, который используется для определения объема выделенных ресурсов в определенном потоке:

1
2
3
4
5
6
return (long) mBeanServer.invoke(
  name,
  GET_THREAD_ALLOCATED_BYTES,
  PARAMS,
  SIGNATURE
);
  • Для полного контекста смотрите здесь .

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

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

Выделение, вызванное этим вызовом, должно быть вычтено из возвращенного числа, чтобы мы изолировали выделение, вызванное программой, то есть вызов meanBeanServer = выделение потока программы + накладные расходы вызова

То, что я заметил, было то, что этот объем выделения обычно составлял 336 байтов. Однако когда я вызвал этот метод в цикле, я обнаружил кое-что интересное. Время от времени это будет выделять различную сумму.

Для этого теста:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
@Test
  public void testQuietMeasuringThreadAllocatedBytes() {
    ByteWatcherSingleThread am = new ByteWatcherSingleThread();
    System.out.println("MeasuringCostInBytes = " + am.getMeasuringCostInBytes());
    long[] marks = new long[1_000_000];
    for (int i = 0; i < 1_000_000; i++) {
      marks[i] = am.threadAllocatedBytes();
    }
 
    long prevDiff = -1;
    for (int i = 1; i < 1_000_000; i++) {
      long diff = marks[i] - marks[i - 1];
      if (prevDiff != diff)
        System.out.println("Allocation changed at iteration " + i + "->" + diff);
      prevDiff = diff;
    }
  }

Это был типичный результат:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
MeasuringCostInBytes = 336
Allocation changed at iteration 1->336
Allocation changed at iteration 12->28184
Allocation changed at iteration 13->360
Allocation changed at iteration 14->336
Allocation changed at iteration 1686->600
Allocation changed at iteration 1687->336
Allocation changed at iteration 2765->672
Allocation changed at iteration 2766->336
Allocation changed at iteration 5458->496
Allocation changed at iteration 5459->336
Allocation changed at iteration 6213->656
Allocation changed at iteration 6214->336
Allocation changed at iteration 6535->432
Allocation changed at iteration 6536->336
Allocation changed at iteration 6557->8536
Allocation changed at iteration 6558->336
Allocation changed at iteration 7628->576
Allocation changed at iteration 7629->336
Allocation changed at iteration 8656->4432
Allocation changed at iteration 8657->336
Allocation changed at iteration 9698->968
Allocation changed at iteration 9699->336
Allocation changed at iteration 11881->1592
Allocation changed at iteration 11882->336
Allocation changed at iteration 12796->1552
Allocation changed at iteration 12797->336
Allocation changed at iteration 13382->456
Allocation changed at iteration 13383->336
Allocation changed at iteration 14844->608
Allocation changed at iteration 14845->336
Allocation changed at iteration 36685->304
Allocation changed at iteration 52522->336
Allocation changed at iteration 101440->400
Allocation changed at iteration 101441->336

Учитывая, что в программе определенно не было никакого распределения, для меня было загадкой, почему один и тот же вызов иногда выделял разные суммы.

Таким образом, более 1 000 000 прогонов программа выделяла разные суммы примерно в 25 раз. Примечательно, что после итераций по 100 тысяч пиков не было.

Я поделился этой проблемой с Хайнцем Кабуцем и Крисом Ньюлендом. Крис заметил, что это связано с джиттером компиляции JIT. Это очень хорошо видно, если повторно запустить тест с флагом -Xint (запускается только в интерпретированном режиме, т.е. без компиляции JIT). Теперь было только 2 шипа.

1
2
3
4
5
MeasuringCostInBytes = 336
Allocation changed at iteration 1->336
Allocation changed at iteration 12->28184
Allocation changed at iteration 13->360
Allocation changed at iteration 14->336

Аналогично работает с флагом -Xcomp (только компиляция):

1
2
3
4
5
MeasuringCostInBytes = 336
Allocation changed at iteration 1->336
Allocation changed at iteration 12->29696
Allocation changed at iteration 13->360
Allocation changed at iteration 14->336

Так что теперь мы можем быть достаточно уверены, что именно джиттер компиляции JIT вызывает мошенническое распределение.

Я не совсем понимаю, почему это так, но, думаю, это понятно. Чтобы компенсировать это, я ввел фазу калибровки в конструкторе ByteWatcher, которая была доработана Хайнцем.

Вы можете увидеть код калибровки здесь, но он состоит из нескольких этапов:

  1. Вызовите метод, чтобы выяснить, сколько потока выделено в узком цикле (мы называем его 100 000 раз) — позволяет JIT правильно подогревать код, чтобы он был скомпилирован
  2. Подождите 50 миллисекунд — это дает JVM шанс завершить джиттер компиляции

С этим кодом в конструкторе, даже если он работает без специальных флагов, пиковых значений выделения нет.

Вывод

  • JIT компиляция вызывает дрожание
  • Запуск программы без дрожания компиляции (т. Е. Только с интерпретацией или только компиляцией) значительно уменьшает это распределение, но не полностью устраняет его.
  • После запуска 100 тыс. Разрядов выделение останавливается, что указывает на то, что для остановки дрожания требуется 100 тыс. Пробежек. Это интересно, потому что мы знаем, что код должен компилироваться после 10 000 итераций.