В формальной части 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, и решить, подходят ли они нам.
Не верите — выведите дизасм достаточно длинного метода на яве, и попробуйте точно объяснить смысл хотя бы пары дюжин подряд идущих инструкций.
ОтветитьУдалитьРечь про -XX:+PrintAssembly?
Байткод-то прозрачен, как слеза младенца.
Да, сгенерированный машинный код. Байткод я в жизни видел раза два.
УдалитьЯ всегда подозревал, что эти засранцы на собеседованиях только делают вид, что он есть!
ОтветитьУдалитьИ все же, если никто ничего не переставлял, а в процессор руководствуется фазой луны, как обеспечивается HB?
То, что там реально происходит -- многократно сложнее простого понятия "перестановка инструкций". Либо вы знаете всю эту сложность для конкретного процессора, и тогда вы можете рассуждать о корректности вашего кода исходя из этого, но только для конкретного процессора. Это уровень разработчиков JVM -- на нем они и обеспечивают HB с помощью специфичных хардверных инструкций. Либо вы всего этого не знаете, и тогда "перестановки" это лишь расплывчатая метафора, которая никакой точности не дает, и никакую корректность доказать не позволяет.
УдалитьА JMM со своей нотацией HB как раз находится посередине: дает вомзожность доказывать корректность не влезая в дикие детали реального выполнения кода.
смысловую разницу между "ожиданием определенного состояния (выполнения некого условия)" и "ожиданием извещения"
ОтветитьУдалитьА где про это можно читануть?
Честно говоря, не помню. Но вроде и так все ясно -- есть посылка оповещения, а есть ожидаемое состояние, которое задается определенными значениями в определенных ячейках разделяемой памяти. Они прямо друг с другом не связаны, и служат вообще-то разным целям. Цель ждать состояния -- способ busy loop. Цель экономить циклы процессора -- способ wait()/notify(). Из совмещения этих целей и рождается стандартная идиома.
УдалитьРуслан, поясни плз эту фразу- "В обоих случаях все может измениться от воли Его, и более широкого контекста". То есть, при каком же контексте в первом случае может быть выведен 0?
ОтветитьУдалитьНапример, в первом случае если значение 1 присваивается волатильной переменной больше, чем однажды -- то что вы там в не-волатильной получите снова становится неопределенным, потому что вы не сможете уже однозначно восстановить synchronization order по значению va прочитанному во втором потоке
Удалить