Почему такое поведение разрешено в модели памяти Java?

Причинность в JMM, кажется, самая запутанная часть этого. У меня есть несколько вопросов, касающихся причинности JMM и разрешенного поведения в параллельных программах.

Как я понимаю, текущий JMM всегда запрещает причинно-следственные связи. (Я прав?)

Теперь, согласно документу JSR-133, стр. 24, рис.16, у нас есть пример, где:

Первоначально x = y = 0

Тема 1:

r3 = x;
if (r3 == 0)
    x = 42;
r1 = x;
y = r1;

Тема 2:

r2 = y;
x = r2;

Наглядно, r1 = r2 = r3 = 42 кажется невозможным. Тем не менее, это упоминается не только как возможно, но и "разрешено" в JMM.

Для возможности объяснение из документа, который я не понимаю:

Компилятор может определить, что единственные значения, когда-либо назначенные x 0 и 42. Исходя из этого, компилятор может вывести это в точке, где мы выполняем r1 = xЛибо мы только что выполнили запись 42 xили мы только что прочитали x и видел значение 42. В любом случае это было бы законно для чтения x чтобы увидеть значение 42. Затем он может изменить r1 = x в r1 = 42; это позволило бы y = r1 быть преобразованным в y = 42 и выполнено ранее, что привело к соответствующему поведению. В этом случае пишите y совершается первым.

У меня вопрос, что это за оптимизация компилятора? (Я не знаю компилятора.) Поскольку 42 пишется только условно, когда if Утверждение выполнено, как компилятор может решить написать x?

Во-вторых, даже если компилятор делает эту спекулятивную оптимизацию и фиксирует y = 42 а потом наконец делает r3 = 42 не является ли это нарушением причинно-следственной связи, поскольку различий между причинами и следствиями не осталось?

Фактически, в одном и том же документе (стр. 15, рис. 7) есть один пример, где подобный причинный цикл упоминается как неприемлемый.

Так почему же этот порядок исполнения является законным в JMM?

3 ответа

Решение

Как объяснено, единственные значения, когда-либо написанные x 0 и 42. Тема 1:

r3 = x; // here we read either 0 or 42
if (r3 == 0)
  x = 42;  
// at this point x is definitely 42
r1 = x;

Поэтому JIT-компилятор может переписать r1 = x как r1 = 42, и далее y = 42, Дело в том, что поток 1 всегда безоговорочно записывает 42 y, r3 переменная на самом деле является избыточной и может быть полностью исключена из машинного кода. Таким образом, код в примере только дает вид причинной стрелки из x в yДетальный анализ показывает, что причинно-следственной связи нет. Удивительным последствием является то, что y может быть совершено рано.

Общее замечание по оптимизации: я так понимаю, вы знакомы с потерями производительности, возникающими при чтении из основной памяти. Вот почему JIT-компилятор склонен отказываться делать это всякий раз, когда это возможно, и в этом примере оказывается, что на самом деле ему не нужно читать x чтобы знать что писать y,

Общее примечание к обозначениям: r1, r2, r3 являются локальными переменными (они могут быть в стеке или в регистрах ЦП); x, y являются общими переменными (они находятся в основной памяти). Без учета этого примеры не будут иметь смысла.

Компилятор может выполнить некоторые анализы и оптимизации и закончить следующим кодом для Thread1:

y=42; // step 1
r3=x; // step 2
x=42; // step 3

Для однопоточного исполнения этот код эквивалентен исходному коду и поэтому является законным. Затем, если код Thread2 выполняется между этапом 1 и этапом 2 (что вполне возможно), тогда r3 также назначается 42.

Вся идея этого примера кода состоит в том, чтобы продемонстрировать необходимость правильной синхронизации.

Это ничего не стоит, что javac не оптимизирует код в значительной степени. JIT оптимизирует код, но довольно консервативен в отношении переупорядочения кода. Процессор может переупорядочить выполнение, и это делает это в небольшой степени довольно много.

Заставить ЦП не выполнять оптимизацию на уровне команд довольно дорого, например, это может замедлить его в 10 и более раз. AFAIK, разработчики Java хотели указать необходимый минимум гарантий, который бы эффективно работал на большинстве процессоров.

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