Ответ 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 - экспериментальная упряжь и набор тестов, помогающих исследованию в правильности поддержки concurrency в 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
options + идеальный графический визуализатор):
где входы в node являются входами в операцию node. Каждый node определяет значение, основанное на его входах и операции, и это значение доступно на всех выходных ребрах. Очевидно, что компилятор не видит разницы между узлами-указателями и целыми узлами, поэтому единственное, что его ограничивает, - это барьер памяти. В результате, чтобы уменьшить давление в регистре, размер целевого кода или что-то еще компилятор решает запланировать инструкции в базовом блоке в этом странном (с точки зрения пользователя) порядке. Вы можете играть с расписанием инструкций в Hotspot, используя следующие параметры (доступные в сборке fastdebug):
-XX:+StressLCM
и -XX:+StressGCM
.