26 февраля 2013 г.

Нет никакой ложки...

...и нет никаких реордерингов. Reordering — это поэтическая метафора. Процессоры вовсе не переставляют инструкции. Процессоры просто выполняют код хуй знает какиначе, чем он написан. Как именно? — да как считают нужным, не нравится — не покупайте. Компиляторы — сюрприз — тоже не переставляют инструкции. Компиляторы делают с вашим кодом что-то противоестественноекакую-то неведомую и непонятную херню. Не верите — выведите дизасм достаточно длинного метода на яве, и попробуйте точно объяснить смысл хотя бы пары дюжин подряд идущих инструкций. Там есть комментарии, но вряд ли это вам поможет. Не нравится — пишите свой, с перестановками и циклами причинности.

В формальной части JMM нет никаких перестановок. Можете перечитать JLS-17.*, термин reordering там используется исключительно в разделах "пояснения и обсуждение", чтобы человеческим языком (ну, тем, что авторы считают человеческим языком) объяснить смысл сформулированных определений.

На этом уровне — на уровне метафоры, наглядного образного объяснения, и существуют разные эвристики, вроде "обычную запись можно переставлять до волатильной, но нельзя после нее". Надо четко понимать, что это именно эвристические правила, а не строгие утверждения. Нигде в JMM вы не найдете утверждения, в каком направлении обычные записи можно или нельзя переставлять с волатильными.

Почему так? Да потому что у понятия reordering слишком расплывчатый смысл. Может ли компилятор переставить инструкцию А с инструкцией Б? — лично мне, как программисту, пофигу, пока я не знаю, что от этого изменится для моей программы. Допустим, компилятор их переставил — значит ли это, что я теперь увижу результат А до результата Б? Вот это тот вопрос, который важен для написания кода, для доказательства сохранения каких-то инвариантов при его выполнении. И если я знаю ответ на этот вопрос, то какая мне разница, в каком порядке переставились инструкции А и Б? Может, сначала А потом Б? А может компилятор выплюнул сначала Б потом А, а потом барьер памяти, сделавший их результат видимым одновременно? А может компилятор выплюнул Б перед А, но процессор его перехитрил, и таки спекулятивно выполнил А перед Б?...

Вот что я имею в виду по расплывчатостью понятия reordering. Именно от всего этого пытается абстрагировать программиста JMM. JMM просто-напросто (да, я издеваюсь) дает возможность для каждой инструкции чтения в программе ответить на вопрос "какую инструкцию записи это чтение может увидеть?" И это именно то, что интересует меня, как программиста.

Проблема в том, что "просто-напросто", к сожалению, совсем не так просто. Это "просто-напросто" в некоторых местах настолько непросто, что даже его создатели ухитрились с ним кое-где накосячить. И поэтому даже расплывчатое и дырявое понятие реордерингов для большинства является более понятным и приемлемым, чем формальная спецификация.

Очень похожая картина, как мне кажется, существует вокруг мифического spurious wakeup. Всем впадлу объяснять очередному поколению программистов тонкую смысловую разницу между "ожиданием определенного состояния (выполнения некого условия)" и "ожиданием извещения" — гораздо проще помянуть про легендарное ВНЕЗАПНО, и заставить писать проверку в цикле. Кажется, если бы spurious wakeup не существовал, его стоило бы придумать хотя бы ради экономии времени.

Точно так же очередное поколение java-программистов поднимает руки перед мистическим семнадцатым разделом JLS, и остается на полумифическом уровне рассуждений о возможных перестановках.


О автор, ты не мудри, ты пальцем покажипрости мне мою глупость, но я так и не понял, может ли великолепная Виртуальная Машина Явы переставлять обычную запись с волатильной?
Позвольте я переформулирую ваш вопрос для наших читателей:


volatile int va = 0;
int a = 0;
a = 42;
va = 1;
if( va == 1 ){
   print a;
}

volatile int va = 0;
int a = 0;
va = 1;
a = 42;
if( a == 42 ){
   print va;
}

Благоволит ли Аллах такому чуду, чтобы код в первом примере выведет 0? И достанет ли ли Его доброты, чтобы и второй код еще раз явил миру ноль — красивейшее из чисел?

Согласно сутре Пророка, в первом примере, для случая well-formed executions это неугодно Аллаху. Во втором примере, в общем случае это возможно (т.е. доказать невозможность в общем случае нельзя), но в частных случаях это тоже может оказаться противным воле Всевышнего (см. пример из предыдущего поста). В обоих случаях все может измениться от воли Его, и более широкого контекста — от кода, который окружает эти инструкции. Так восславим же Аллаха за его доброту!

P.S. Точности ради: говоря о реордерингах, надо упомянуть еще другой уровень — уровень разработчиков JVM. Уровень людей, которым нужно прописать в JIT-е для конкретной архитектуры процессора конкретные правила преобразования кода, правила допустимости или недопустимости каких-то оптимизаций в каком-то контексте, правила расстановки барьеров памяти. Для них как раз актуальна задача "что может, и что должна делать с кодом JVM, чтобы результат выполнения не противоречил гарантиям, предоставляемым JMM?" И здесь reordering обретает конкретный смысл, потому что для известной архитектуры процессора наблюдаемые эффекты от перестановки двух известных инструкций можно узнать из его спецификации. И эти эффекты можно сравнить с тем, что требует JMM, и решить, подходят ли они нам.

8 комментариев:

  1. Не верите — выведите дизасм достаточно длинного метода на яве, и попробуйте точно объяснить смысл хотя бы пары дюжин подряд идущих инструкций.

    Речь про -XX:+PrintAssembly?
    Байткод-то прозрачен, как слеза младенца.

    ОтветитьУдалить
    Ответы
    1. Да, сгенерированный машинный код. Байткод я в жизни видел раза два.

      Удалить
  2. Я всегда подозревал, что эти засранцы на собеседованиях только делают вид, что он есть!
    И все же, если никто ничего не переставлял, а в процессор руководствуется фазой луны, как обеспечивается HB?

    ОтветитьУдалить
    Ответы
    1. То, что там реально происходит -- многократно сложнее простого понятия "перестановка инструкций". Либо вы знаете всю эту сложность для конкретного процессора, и тогда вы можете рассуждать о корректности вашего кода исходя из этого, но только для конкретного процессора. Это уровень разработчиков JVM -- на нем они и обеспечивают HB с помощью специфичных хардверных инструкций. Либо вы всего этого не знаете, и тогда "перестановки" это лишь расплывчатая метафора, которая никакой точности не дает, и никакую корректность доказать не позволяет.

      А JMM со своей нотацией HB как раз находится посередине: дает вомзожность доказывать корректность не влезая в дикие детали реального выполнения кода.

      Удалить
  3. смысловую разницу между "ожиданием определенного состояния (выполнения некого условия)" и "ожиданием извещения"

    А где про это можно читануть?

    ОтветитьУдалить
    Ответы
    1. Честно говоря, не помню. Но вроде и так все ясно -- есть посылка оповещения, а есть ожидаемое состояние, которое задается определенными значениями в определенных ячейках разделяемой памяти. Они прямо друг с другом не связаны, и служат вообще-то разным целям. Цель ждать состояния -- способ busy loop. Цель экономить циклы процессора -- способ wait()/notify(). Из совмещения этих целей и рождается стандартная идиома.

      Удалить
  4. Руслан, поясни плз эту фразу- "В обоих случаях все может измениться от воли Его, и более широкого контекста". То есть, при каком же контексте в первом случае может быть выведен 0?

    ОтветитьУдалить
    Ответы
    1. Например, в первом случае если значение 1 присваивается волатильной переменной больше, чем однажды -- то что вы там в не-волатильной получите снова становится неопределенным, потому что вы не сможете уже однозначно восстановить synchronization order по значению va прочитанному во втором потоке

      Удалить