Могу ли я обнаружить переупорядочение инструкций по журналу компиляции байт-кода? - PullRequest
0 голосов
/ 29 марта 2020

У меня есть следующий образец кода:

class Shared {
    int x;
    int y;

    void increment() {
        x++;
        y++;
    }

    void check() {
        if (y > x) {
            System.out.println("Ooops! y > x");
        }
    }
}

Выглядит ясно? Но главная проблема возникает здесь, когда я пытаюсь увеличить и проверить в двух потоках:

Shared shared = new Shared();

        Thread writer = new Thread(() -> {
            for (int i = 0; i < N; i++) {
                shared.increment();
            }
        });

        Thread reader = new Thread(() -> {
            for (int i = 0; i < N; i++) {
                shared.check();
            }
        });

writer.start();
reader.start();

Вы можете заметить гонку данных (в некоторых случаях переупорядочивание инструкций?):

1. x++;
2. y++;

И теперь я знаю о специальных флагах ВМ, которые могут помочь мне распечатать журналы JIT-компилятора (-XX:+PrintCompilation).

...
    120  181       3       Shared::increment (21 bytes)
    120  182       3       Shared::check (20 bytes)
    120  183       4       Shared::increment (21 bytes)
    120  184       4       Shared::check (20 bytes)
    121  181       3       Shared::increment (21 bytes)   made not entrant
    121  182       3       Shared::check (20 bytes)   made not entrant
    121  185     n 0       java.lang.invoke.MethodHandle::linkToStatic(L)L (native)   (static)
    121  184       4       Shared::check (20 bytes)   made not entrant
    122  186       3       Shared::check (20 bytes)
    122  187     n 0       java.lang.Object::clone (native)   
    122  188       4       Shared::check (20 bytes)
    122  189 %     3       Main::lambda$main$0 @ 2 (19 bytes)
    122  190       3       Main::lambda$main$0 (19 bytes)
    123  186       3       Shared::check (20 bytes)   made not entrant
...

ОК, теперь я могу видеть, как обрабатывается компиляция метода приращения:

    120  181       3       Shared::increment (21 bytes)
    120  183       4       Shared::increment (21 bytes)
    121  181       3       Shared::increment (21 bytes)   made not entrant

Правильно ли я понимаю , что переупорядочение здесь происходит из-за многоуровневой компиляции ? Поскольку increment() - горячий метод, JIT-компилятор профилирует эту информацию и использует C2 серверный компилятор. И, как мне кажется, таким образом переупорядочить некоторые инструкции, но в некоторых случаях происходит оптимизация (made not entrant). Или это неправильно?

Также есть еще какие-то логи для компиляции:

    138  182       2       Shared::increment (21 bytes)
    138  184       4       Shared::increment (21 bytes)
    138  182       2       Shared::increment (21 bytes)   made not entrant

1 Ответ

2 голосов
/ 29 марта 2020

Это не относится к многоуровневой компиляции. Проблема также происходит без этого. Пусть JVM скомпилирует только один метод check и посмотрим, как он выглядит в скомпилированном в C2 коде:

java -XX:-TieredCompilation \
     -XX:CompileCommand=compileonly,Shared::check \
     -XX:CompileCommand=print,Shared::check \
     Shared

Вывод:

    0x00000000031a4160: mov     dword ptr [rsp+0ffffffffffffa000h],eax
    0x00000000031a4167: push    rbp
    0x00000000031a4168: sub     rsp,20h           ;*synchronization entry
                                                  ; - Shared::check@-1 (line 11)

(1) 0x00000000031a416c: mov     r10d,dword ptr [rdx+0ch]
                                                  ;*getfield x
                                                  ; - Shared::check@5 (line 11)

(2) 0x00000000031a4170: mov     r8d,dword ptr [rdx+10h]  ;*getfield y
                                                  ; - Shared::check@1 (line 11)

    0x00000000031a4174: cmp     r8d,r10d
    0x00000000031a4177: jnle    31a4185h          ;*if_icmple
                                                  ; - Shared::check@8 (line 11)

    0x00000000031a4179: add     rsp,20h
    0x00000000031a417d: pop     rbp
    0x00000000031a417e: test    dword ptr [1020000h],eax
                                                  ;   {poll_return}
    0x00000000031a4184: ret

Как видите, x загружается первым (строка 1), а y загружается после (строка 2). Между этими линиями другой поток может увеличиваться y в общее число раз, в результате чего y кажется больше, чем x.

В этом конкретном случае вы догадались о переупорядочении нагрузок относительно оригинальный порядок программы (в байт-коде getfield y идет перед getfield x). Однако, как упомянул @Andreas, это не единственная причина, по которой программа может сломаться. Даже если JIT-компилятор выдает load(y) до load(x), в зависимости от архитектуры процессора, может случиться так, что первая загрузка получит более новое значение, а вторая загрузку получит более старое, и это будет абсолютно правильно с точки зрения JMM.

...