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

Может ли JIT свернуть два изменчивых чтения как один в определенных выражениях?

Предположим, что a volatile int a. Один поток выполняет

while (true) {
    a = 1;
    a = 0;
}

а другой поток

while (true) {
    System.out.println(a+a);
}

Теперь, было ли незаконным компилятор JIT испускать сборку, соответствующую 2*a вместо a+a?

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

С другой стороны, нет никакой точки синхронизации между двумя чтениями, поэтому я не вижу, что было бы незаконно обрабатывать a+a атомарно, и в этом случае я не вижу, как оптимизация, такая как 2*a нарушит спецификацию.

Приветствуются ссылки на JLS.

4b9b3361

Ответ 1

Короткий ответ:

Да, эта оптимизация разрешена. Свертывание двух последовательных операций чтения приводит к наблюдаемому поведению последовательности, являющейся атомарной, но не выглядит как переупорядочение операций. Любая последовательность действий, выполняемых в одном потоке выполнения, может быть выполнена как элементарный элемент. В общем, сложно обеспечить атомарную последовательность операций, и это редко приводит к увеличению производительности, поскольку в большинстве сред выполнения накладываются накладные расходы на выполнение элементов атомарно.

В примере, приведенном в исходном вопросе, последовательность рассматриваемых операций следующая:

read(a)
read(a)

Выполнение этих операций атомарно гарантирует, что значение, считываемое в первой строке, равно значению, считанному во второй строке. Кроме того, это означает, что значение, считываемое во второй строке, является значением, содержащимся в a во время первого чтения (и наоборот, поскольку атомарные обе операции чтения происходили одновременно в соответствии с наблюдаемым состоянием выполнения программы), Рассматриваемая оптимизация, которая повторно использует значение первого чтения для второго чтения, эквивалентна компилятору и/или JIT, выполняющему последовательность атомарно, и, таким образом, является допустимой.


Оригинальный более длинный ответ:

Модель памяти Java описывает операции с использованием частичного упорядочения "происходит до". Чтобы выразить ограничение, что первое чтение r1 и второе чтение r2 a не могут быть свернуты, вам нужно показать, что между ними семантически требуется некоторая операция.

Операции над потоком с r1 и r2 следующие:

--> r(a) --> r(a) --> add -->

Чтобы выразить требование, чтобы что-то (скажем, y) лежало между r1 и r2, вам нужно требовать, чтобы r1 происходило до того, как y и y произойдет до r2. Как это бывает, не существует правила, где операция чтения появляется с левой стороны отношения "происходит до". Самое близкое, что вы могли бы получить, это сказать, что y происходит до r2, но частичный порядок позволил бы y произойти и до r1, тем самым сворачивая операции чтения.

Если не существует сценария, который требует, чтобы операция находилась между r1 и r2, тогда вы можете объявить, что между r1 и r2 никогда не появляется операция, и не нарушать требуемую семантику языка. Использование одной операции чтения будет эквивалентно этому утверждению.

Редактировать Мой ответ отклоняется, поэтому я собираюсь вдаваться в дополнительные детали.

Вот несколько связанных вопросов:

  • Требуется ли компилятор Java или JVM для свертывания этих операций чтения?

    Нет. Выражения a и a используемые в выражении add, не являются константными выражениями, поэтому не требуется, чтобы они были свернуты.

  • JVM сворачивает эти операции чтения?

    На это я не уверен в ответе. javap -c программу и используя javap -c, легко увидеть, что компилятор Java не сворачивает эти операции чтения. К сожалению, доказать, что JVM не срывает операции (или даже сам процессор), не так просто.

  • Должна ли JVM свернуть эти операции чтения?

    Возможно нет. Каждая оптимизация требует времени для выполнения, поэтому существует баланс между временем, затрачиваемым на анализ кода, и ожидаемой выгодой. Некоторые оптимизации, такие как устранение проверки границ массива или проверка на нулевые ссылки, доказали, что имеют обширные преимущества для реальных приложений. Единственный случай, когда эта конкретная оптимизация имеет возможность улучшить производительность, - это случаи, когда две идентичные операции чтения появляются последовательно.

    Кроме того, как показано в ответе на этот ответ наряду с другими ответами, это конкретное изменение может привести к неожиданному изменению поведения для некоторых приложений, которые пользователи могут не желать.

Редактировать 2: Относительно Рафаэля описание претензии, что две операции чтения, которые не могут быть переупорядочены. Это выражение предназначено, чтобы подчеркнуть тот факт, что кэширование операции считывания в следующей последовательности может произвести неправильный результат: a

a1 = read(a)
b1 = read(b)
a2 = read(a)
result = op(a1, b1, a2)

Предположим, изначально a и b имеют значение по умолчанию 0. Затем вы выполняете только первое read(a).

Теперь предположим, что другой поток выполняет следующую последовательность:

a = 1
b = 1

Наконец, предположим, что первый поток выполняет строку read(b). Если бы вы кешировали изначально прочитанное значение a, вы бы получили следующий вызов:

op(0, 1, 0)

Это не правильно. Поскольку обновленное значение a было сохранено перед записью в b, невозможно прочитать значение b1 = 1 а затем прочитать значение a2 = 0. Без кеширования правильная последовательность событий приводит к следующему вызову.

op(0, 1, 1)

Однако, если вы зададите вопрос "Есть ли способ разрешить кэширование чтения a?", Ответ - да. Если вы можете выполнить все три операции чтения в первой последовательности потоков как элементарный элемент, то кэширование значения разрешено. Хотя синхронизация между несколькими переменными затруднена и редко дает преимущество при оппортунистической оптимизации, вполне возможно, что возникнет исключение. Например, предположим, и a b являются каждые 4 байта, и они появляются в памяти последовательно с выровнены по границе 8 байт. a 64-битный процесс может реализовать последовательность read(a) read(b) как атомарную 64-битную операцию загрузки, которая позволит кэшировать значение a (эффективно трактуя все три операции чтения как атомарную операцию, а не просто первые два).

Ответ 2

В моем первоначальном ответе я выступил против законности предлагаемой оптимизации. Я поддержал это главным образом из информации кулинарной книги JSR-133, в которой говорится, что изменчивое чтение не должно переупорядочиваться с другим изменчивым чтением и где оно далее что кэшированное чтение должно рассматриваться как переупорядочение. Последнее утверждение, однако, сформулировано с некоторой двусмысленностью, поэтому я прошел формальное определение JMM где я не нашел такого указания. Поэтому я бы теперь утверждал, что оптимизация разрешена. Однако JMM довольно сложна, и обсуждение на этой странице указывает на то, что этот краевой случай может быть по-другому решен кем-то с более глубоким пониманием формализма.

Обозначение потока 1 для выполнения

while (true) {
  System.out.println(a // r_1 
    + a); // r_2
} 

и поток 2 для выполнения:

while (true) {
  a = 0; // w_1
  a = 1; // w_2
}

Два чтения r_i и две записи w_i of a являются действиями синхронизации, поскольку a - volatile (JSR 17.4.2). Это внешние действия, поскольку переменная a используется в нескольких потоках. Эти действия содержатся в наборе всех действий a. Существует общий порядок всех действий синхронизации, порядок синхронизации, который согласуется с порядком выполнения программы для потока 1 и потока 2 (JSR 17.4.4). Из определения синхронности с частичным порядком в указанном выше коде не существует ребра, определенного для этого порядка. Как следствие, порядок "только-только" отражает только внутрипоточную семантику каждого потока (JSR 17.4.5).

При этом мы определяем W как функцию записи-записи, где W(r_i) = w_2 и записанную по значению функцию V(w_i) = w_2 (JLS 17.4.6). Я взял некоторую свободу и устранил w_1, поскольку это упрощает этот контур формального доказательства. Вопрос о предлагаемом выполнении E является корректным (JLS 17.5.7). Предложенное выполнение E подчиняется семантике внутри потока, происходит - до согласования, подчиняется синхронизированному порядку, и каждый прочитанный наблюдает согласованную запись. Проверка требований причинности тривиальна (JSR 17.4.8). Я также не понимаю, почему правила для не заканчивающихся исполнений будут актуальны, поскольку цикл охватывает весь обсуждаемый код (JLS 17.4.9), и нам не нужно различать наблюдаемые действия.

При этом я не могу найти никаких указаний о том, почему эта оптимизация будет запрещена. Тем не менее, он не применяется для volatile, прочитанного VM HotSpot, как это можно наблюдать с помощью -XX:+PrintAssembly. Я предполагаю, что преимущества производительности, однако, незначительны, и эта картина обычно не наблюдается.

Примечание. После просмотра модели праймитов памяти Java (несколько раз), я уверен, это рассуждение верно.

Ответ 3

Немного изменила проблему ОП

   volatile int a

    //thread 1
    while (true) {
        a = some_oddNumber;
        a = some_evenNumber;
    }

    // Thread 2 
    while (true) {
        if(isOdd(a+a)) {
            break;
        }
    }

Если приведенный выше код был выполнен последовательно, то существует допустимое последовательное последовательное выполнение, которое разбивает цикл thread2 while.

Если компилятор оптимизирует + a до 2a, тогда цикл thread2 while никогда не будет.

Таким образом, указанная выше оптимизация запретит одно конкретное выполнение, если это был код с последовательным исполнением.

Основной вопрос, является ли эта оптимизация проблемой?

Q.   Is the Transformed code Sequentially Consistent.

Ans.. Программа корректно синхронизируется, если, когда она выполняется последовательно последовательным образом, нет расчётов данных. См. Пример 17.4.8-1 из главы 17 JLS

   Sequential consistency: the result of any execution is the same as
   if the read and write operations by all processes were executed in
   some sequential order and the operations of each individual
   process appear in this sequence in the order specified by its
   program [Lamport, 1979].

   Also see http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.4.3

Последовательная непротиворечивость - сильная гарантия. Путь выполнения, где компилятор оптимизирует + a как 2a, также является допустимым последовательным выполнением.  Итак, ответ "Да".

  Q.   Is the code violates happens before guarantees.

Ans. Последовательная последовательность подразумевает, что это происходит до того, как гарантия будет действительна здесь.       Таким образом, ответ "Да". JLS ref

Поэтому я не думаю, что оптимизация недействительна по крайней мере в случае с OP. Случай, когда поток 2, в то время как петли стекаются в infinte, также вполне возможен без преобразования компилятора.

Ответ 4

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

Это не так, как спецификация языка Java определяет изменчивость. JLS просто говорит:

Запись в изменчивую переменную v (§8.3.1.4) синхронизируется со всеми последующими чтениями v любым потоком (где "последующее" определяется в соответствии с порядком синхронизации).

Следовательно, запись в изменчивую переменную происходит до (и видна) для любых последующих чтений этой же переменной.

Это ограничение тривиально выполняется для чтения, которое не является последующим. То есть волатильность обеспечивает только видимость записи, если чтение, как известно, происходит после записи.

Это не относится к вашей программе. Для каждого хорошо сформированного выполнения, которое наблюдает a как 1, я могу построить еще одно хорошо сформированное выполнение, где a считается равным 0, просто перемещать считывание после записи. Это возможно, потому что отношение "бывает раньше" выглядит следующим образом:

write 1   -->   read 1                    write 1   -->   read 1
   |              |                          |              |
   |              v                          v              |
   v      -->   read 1                    write 0           v
write 0           |             vs.          |      -->   read 0
   |              |                          |              |
   v              v                          v              v
write 1   -->   read 1                    write 1   -->   read 1                 

Таким образом, все гарантии JMM для вашей программы заключаются в том, что a + a будет давать 0, 1 или 2. Это выполняется, если a + a всегда дает 0. Так же, как операционной системе разрешено выполнять эту программу на одно ядро ​​и всегда прерывать поток 1 до той же инструкции цикла, JVM разрешено повторно использовать значение - в конце концов, наблюдаемое поведение остается неизменным.

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

Ответ 5

Как указано в других ответах, есть два чтения и две записи. Представьте следующее выполнение (T1 и T2 обозначают два потока), используя аннотации, которые соответствуют заявлению JLS ниже:

  • T1: a = 0 //W(r)
  • T2: read temp1 = a //r_initial
  • T1: a = 1 //w
  • T2: read temp2 = a //r
  • T2: print temp1+temp2

В среде concurrrent это, безусловно, возможное чередование потоков. Тогда ваш вопрос: сможет ли JVM сделать r наблюдать W(r) и читать 0 вместо 1?

JLS # 17.4.5 утверждает:

Выполняется множество действий A - до согласованного, если для всех читает r в A, где W (r) - это действие записи, наблюдаемое r, то нет необходимости в том, чтобы либо hb (r, W (r)), или что существует запись w в такая, что wv = rv и hb (W (r), w) и hb (w, r).

Оптимизация, которую вы предлагаете (temp = a; print (2 * temp);), нарушит это требование. Таким образом, ваша оптимизация может работать только в том случае, если между r_initial и r нет промежуточной записи, которая не может быть гарантирована в типичной многопоточной инфраструктуре.

В качестве побочного комментария обратите внимание, однако, что нет никакой гарантии того, сколько времени потребуется, чтобы записи стали видимыми из потока чтения. См. Например: Подробная семантика изменчивости относительно своевременности видимости.