Есть ли какое-либо переупорядочение команд, выполненное компилятором JIT Hotspot, которое можно воспроизвести?
Как известно, некоторые JIT позволяют переупорядочивать объекты для инициализации, например,
someRef = new SomeObject();
может быть разложен на следующие шаги:
objRef = allocate space for SomeObject; //step1
call constructor of SomeObject; //step2
someRef = objRef; //step3
JIT-компилятор может изменить его порядок следующим образом:
objRef = allocate space for SomeObject; //step1
someRef = objRef; //step3
call constructor of SomeObject; //step2
а именно, step2 и step3 могут быть переупорядочены компилятором JIT. Несмотря на то, что это теоретически правильное переупорядочение, я не смог воспроизвести его с помощью Hotspot(jdk1.7) на платформе x86.
Итак, существует ли какое-либо изменение порядка команд, выполненное компилятором Hotspot JIT, которое можно воспроизвести?
Обновление: я провел тест на моей машине (Linux x86_64,JDK 1.8.0_40, i5-3210M), используя следующую команду:
java -XX:-UseCompressedOops -XX:+UnlockDiagnosticVMOptions -XX:CompileCommand="print org.openjdk.jcstress.tests.unsafe.UnsafePublication::publish" -XX:CompileCommand="inline, org.openjdk.jcstress.tests.unsafe.UnsafePublication::publish" -XX:PrintAssemblyOptions=intel -jar tests-custom/target/jcstress.jar -f -1 -t .*UnsafePublication.* -v > log.txt
и я вижу, что инструмент сообщил что-то вроде:
[1] 5 ПРИНЯТО. Объект опубликован, по крайней мере, 1 поле видно.
Это означало, что поток наблюдателя увидел неинициализированный экземпляр MyObject.
Однако я НЕ видел код сборки, сгенерированный как @Ivan's:
0x00007f71d4a15e34: mov r11d,DWORD PTR [rbp+0x10] ;getfield x
0x00007f71d4a15e38: mov DWORD PTR [rax+0x10],r11d ;putfield x00
0x00007f71d4a15e3c: mov DWORD PTR [rax+0x14],r11d ;putfield x01
0x00007f71d4a15e40: mov DWORD PTR [rax+0x18],r11d ;putfield x02
0x00007f71d4a15e44: mov DWORD PTR [rax+0x1c],r11d ;putfield x03
0x00007f71d4a15e48: mov QWORD PTR [rbp+0x18],rax ;putfield o
Кажется, здесь нет переупорядочения компилятора.
Обновление 2: @ Иван поправил меня. Я использовал неправильную команду JIT для захвата кода сборки. После исправления этой ошибки я могу получить код сборки ниже:
0x00007f76012b18d5: mov DWORD PTR [rax+0x10],ebp ;*putfield x00
0x00007f76012b18d8: mov QWORD PTR [r8+0x18],rax ;*putfield o
; - org.openjdk.jcstress.tests.unsafe.generated.UnsafePublication_jcstress$Runner_publish::call@94 (line 156)
0x00007f76012b18dc: mov DWORD PTR [rax+0x1c],ebp ;*putfield x03
По-видимому, компилятор сделал переупорядочение, которое вызвало небезопасную публикацию.
1 ответ
Вы можете воспроизвести любой порядок компиляции. Правильный вопрос - какой инструмент использовать для этого. Чтобы увидеть переупорядочение компилятора - вы должны следовать до уровня сборки с помощью JITWatch (так как он использует вывод журнала сборки HotSpot) или JMH с LinuxPerfAsmProfiler.
Давайте рассмотрим следующий эталонный тест на основе JMH:
public class ReorderingBench {
public int[] array = new int[] {1 , -1, 1, -1};
public int sum = 0;
@Benchmark
public void reorderGlobal() {
int[] a = array;
sum += a[1];
sum += a[0];
sum += a[3];
sum += a[2];
}
@Benchmark
public int reorderLocal() {
int[] a = array;
int sum = 0;
sum += a[1];
sum += a[0];
sum += a[3];
sum += a[2];
return sum;
}
}
Обратите внимание, что доступ к массиву неупорядочен. На моей машине для метода с глобальной переменной sum
Выход ассемблера:
mov 0xc(%rcx),%r8d ;*getfield sum
...
add 0x14(%r12,%r10,8),%r8d ;add a[1]
add 0x10(%r12,%r10,8),%r8d ;add a[0]
add 0x1c(%r12,%r10,8),%r8d ;add a[3]
add 0x18(%r12,%r10,8),%r8d ;add a[2]
но для метода с локальной переменной sum
схема доступа была изменена:
mov 0x10(%r12,%r10,8),%edx ;add a[0] <-- 0(0x10) first
add 0x14(%r12,%r10,8),%edx ;add a[1] <-- 1(0x14) second
add 0x1c(%r12,%r10,8),%edx ;add a[3]
add 0x18(%r12,%r10,8),%edx ;add a[2]
Вы можете играть с оптимизацией компилятора c1 c1_RangeCheckElimination
Обновить:
С точки зрения пользователя чрезвычайно трудно видеть только переупорядочения компилятора, потому что вам нужно запустить несколько примеров, чтобы уловить неординарное поведение. Также важно разделить проблемы с компилятором и оборудованием, например, слабо упорядоченное оборудование, такое как POWER, может изменить поведение. Давайте начнем с правильного инструмента: jcstress - экспериментального жгута и набора тестов, чтобы помочь в исследовании правильности поддержки параллелизма в JVM, библиотеках классов и оборудовании. Вот средство воспроизведения, в котором планировщик команд может принять решение об отправке нескольких хранилищ полей, затем опубликовать ссылку, а затем выбросить остальные хранилища полей (также вы можете прочитать о безопасных публикациях и планировании команд здесь). В некоторых случаях на моей машине с Linux x86_64, компилятор JDK 1.8.0_60, i5-4300M генерирует следующий код:
mov %edx,0x10(%rax) ;*putfield x00
mov %edx,0x14(%rax) ;*putfield x01
mov %edx,0x18(%rax) ;*putfield x02
mov %edx,0x1c(%rax) ;*putfield x03
...
movb $0x0,0x0(%r13,%rdx,1) ;*putfield o
но иногда:
mov %ebp,0x10(%rax) ;*putfield x00
...
mov %rax,0x18(%r10) ;*putfield o <--- publish here
mov %ebp,0x1c(%rax) ;*putfield x03
mov %ebp,0x18(%rax) ;*putfield x02
mov %ebp,0x14(%rax) ;*putfield x01
Обновление 2:
По вопросу о преимуществах производительности. В нашем случае такая оптимизация (переупорядочение) не приносит существенного выигрыша в производительности, это всего лишь побочный эффект от реализации компилятора. HotSpot использует sea of nodes
график для моделирования данных и управления потоком (вы можете прочитать о промежуточном представлении на основе графика здесь). На следующем рисунке показан график IR для нашего примера (-XX:+PrintIdeal -XX:PrintIdealGraphLevel=1 -XX:PrintIdealGraphFile=graph.xml
параметры + идеальный граф визуализатора): где входные данные для узла являются входными данными для работы узла. Каждый узел определяет значение на основе его входных данных и операции, и это значение доступно на всех выходных ребрах. Очевидно, что компилятор не видит никакой разницы между указателем и целочисленными узлами хранения, поэтому единственное, что его ограничивает, - это барьер памяти. В результате, чтобы уменьшить давление в регистре, размер целевого кода или что-то еще, компилятор решает запланировать инструкции в базовом блоке в этом странном (с точки зрения пользователя) порядке. Вы можете поиграть с расписанием команд в Hotspot, используя следующие опции (доступны в сборке fastdebug):
-XX:+StressLCM
а также -XX:+StressGCM
,