Подтвердить что ты не робот

Есть ли переупорядочение инструкций, сделанное компилятором Hotspot JIT, который можно воспроизвести?

Как известно, некоторые 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.

Итак, есть ли переупорядочение инструкций, сделанное компилером Jots Hotspot JIT, которое можно воспроизвести?


Обновление:  Я сделал test на моей машине (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 ACCEPTABLE Объект опубликован, видимо, видимо не менее 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

Кажется, что здесь нет переупорядочения компилятора.


Update2: @Иван поправил меня. Я использовал неправильную команду 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::[email protected] (line 156)
0x00007f76012b18dc: mov    DWORD PTR [rax+0x1c],ebp  ;*putfield x03

По-видимому, компилятор сделал переупорядочение, которое вызвало небезопасную публикацию.

4b9b3361

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