Есть ли какое-либо переупорядочение команд, выполненное компилятором 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,

Другие вопросы по тегам