0 Предисловие
Во многих случаях доступ к переменной программы (полям экземпляра объекта, статическим полям класса и элементам массива) может выполняться в порядке, отличном от порядка, продиктованного семантикой программы. Конкретные ситуации следующие:
- переводчикМожет свободно менять порядок инструкций во имя оптимизации;
- При определенных обстоятельствах,процессорМожет выполнять инструкции не по порядку;
- данные могут быть вРегистры, буферы процессора и основная памятьв порядке, отличном от порядка, заданного программой;
Например, если поток записывает значение в полеa
, затем запишите значение в полеb
,иb
Значение не зависит отa
значение, то процессоры могут свободно регулировать свой порядок выполнения, а буферы могутa
обновить передb
значение в основную память.Существует много потенциальных источников переупорядочивания, таких как компиляторы, JIT-компиляторы и буферы..
Следовательно, от исходного кода Java до программы, которая может быть распознана и выполнена машиной (или виртуальной машиной), должен пройти как минимум период компиляции и период выполнения.В течение этих двух периодов переупорядочение делится на две категории: переупорядочение компилятора, переупорядочение процессора (выполнение вне очереди), соответствующие средам времени компиляции и времени выполнения соответственно.. Из-за существования переупорядочения фактический порядок выполнения инструкций не соответствует порядку, указанному в исходном коде.
1 Переупорядочивание компилятора
Компилятор может изменить порядок выполнения операторов без изменения семантики однопоточной программы.При условии неизменения семантики программы количество чтений и сохранений регистров должно быть максимально уменьшено, а сохраненные значения регистров должны полностью использоваться повторно..
Предположим, что первая инструкция вычисляет значение, присваивает его переменной A и сохраняет в регистре, вторая инструкция не имеет ничего общего с A, но должна занимать регистр (при условии, что она будет занимать тот же регистр, что и A), а третья инструкция использует значение A и ничего общего со второй инструкцией. Тогда, если согласно модели последовательной непротиворечивости, A помещается в регистр после выполнения первой инструкции, A больше не существует, когда выполняется вторая инструкция, и A снова считывается в регистр, когда выполняется третья инструкция, и в этот процесс, значение A не изменилось.Обычно компилятор меняет местами вторую и третью инструкции, так что A существует в регистре в конце первой инструкции, а затем значение A можно напрямую прочитать из регистра, уменьшая накладные расходы на повторное чтение..
Еще одна оптимизация компилятора: при чтении переменной в циклеЧтобы повысить скорость доступа, компилятор сначала прочитает переменную в регистр.; Когда значение переменной будет взято позже, оно будет взято непосредственно из регистра и больше не будет взято из памяти. Это уменьшает ненужные обращения к памяти. Но, повышая эффективность, он также создает новые проблемы.Если другой поток модифицирует значение переменной в памяти, поскольку значение переменной в регистре не изменилось, весьма вероятно, что цикл не может завершиться.. Компилятор выполняет оптимизацию кода, что повышает эффективность работы программы, но также может привести к неправильным результатам. Таким образом, программист должен предотвратить неправильную оптимизацию компилятора.
2 Изменение порядка процессоров
2.1 Параллельное переупорядочение инструкций
Компиляторы и процессоры могут переупорядочивать операции,Но чтобы соблюдать зависимости данных, компилятор и процессор не будут менять порядок выполнения двух операций, которые имеют зависимости данных.. Если две операции обращаются к одной и той же переменной, и одна из двух операций является операцией записи, между двумя операциями существует зависимость данных.Существует три типа зависимостей данных:
название | пример кода | иллюстрировать |
---|---|---|
читать после записи | a = 1;b = a; | После записи переменной прочитайте местоположение. |
пиши после пиши | a = 1;a = 2; | После записи переменной напишите переменную снова. |
писать после прочтения | a = b;b = 1; | После чтения переменной запишите переменную. |
В приведенных выше трех случаях, если порядок выполнения двух операций изменен, результат выполнения программы будет изменен. Операции с такими прямыми зависимостями не переупорядочиваются.Специальное примечание. Упомянутые здесь зависимости относятся только к одному потоку..
Пример:
class Demo {
int a = 0;
boolean flag = false;
public void write() {
a = 1; // 1
flag = true; // 2
}
public void read() {
if (flag) { // 3
int i = a * a; // 4
}
}
}
Поскольку операции 1 и 2 не имеют зависимостей по данным, компилятор и процессор могут изменить порядок этих двух операций; операции 3 и 4 не имеют зависимостей по данным, и компилятор и процессор также могут изменить порядок этих двух операций.
-
Каков может быть эффект, когда операция 1 и операция 2 переупорядочены?
Как показано на рисунке выше, операции 1 и 2 переупорядочены. Когда программа выполняется, поток A сначала записывает переменную флага flag, а затем поток B читает эту переменную. Поскольку условие оценивается как истинное, поток B будет читать переменную a.В этот момент переменная a вообще не была записана потоком A, а семантика многопоточных программ нарушается переупорядочением!
-
Какой может быть эффект, если переупорядочить операции 3 и 4? (При таком переупорядочении можно, кстати, объяснить зависимости управления)
В программе операция 3 и операция 4 имеют управляющую зависимость.Когда в коде есть управляющие зависимости, это влияет на параллелизм выполнения последовательности инструкций. Для этого компиляторы и процессоры используютИсполнение спекуляцийдля преодоления влияния управляющих зависимостей на параллелизм.В качестве примера возьмем угадывание выполнения процессора:
Процессор, выполняющий поток B, может читать вперед и вычислять
a * a
, а затем временно сохраните результаты расчета вбуфер переупорядочения ROBв аппаратном кэше. Когда условие следующей операции 3 признано истинным, результат вычисления записывается в переменную i.Из рисунка мы видим, чтоУгадай исполнениеСущественно переупорядочивание операций 3 и 4. Переупорядочивание здесь нарушает семантику многопоточных программ!
в однопоточной программе, переупорядочивание операций, имеющих управляющие зависимости, не изменит результат выполнения (это также
as-if-serial
причина, по которой семантика позволяет переупорядочивать операции, имеющие управляющие зависимости);в многопоточной программе, переупорядочивание операций, имеющих управляющие зависимости, может изменить результат выполнения программы.
2.2 Неупорядоченное изменение порядка инструкций
Современные процессоры обычно используют конвейеры для выполнения инструкций.Выполнение инструкции делится на несколько этапов: выборка, декодирование, выборка, выполнение, обратная запись и т. д.. Потом,Несколько инструкций могут существовать в конвейере одновременно и выполняться одновременно.. Конвейер инструкций не является последовательным, и поскольку очень длинная инструкция остается на стадии «выполнения» в течение длительного времени, последующие инструкции застревают на стадии до «выполнения». Напротив,Конвейер параллельный, и несколько инструкций могут находиться на одном этапе одновременно, пока соответствующие компоненты обработки внутри ЦП не заполнены.. Например, если ЦП имеет сумматор и делитель, то инструкция сложения и инструкция деления могут находиться в стадии «выполнения» одновременно, в то время как две инструкции сложения могут работать только последовательно в стадии «выполнения».
Однако таким образом может быть сгенерирован внеочередной порядок. Например: инструкция сложения изначально появилась после инструкции деления, но поскольку время выполнения деления очень велико, сложение может быть завершено до его выполнения. Другим примером являются две инструкции доступа к памяти, которые могут быть завершены до первой инструкции, потому что вторая инструкция попадает в кэш.В общем, инструкции не по порядку не означают, что ЦП преднамеренно корректирует порядок перед выполнением инструкций..ЦП всегда последовательно извлекает инструкции из памяти, а затем последовательно помещает их в конвейер инструкций.. Однако различные условия во время выполнения инструкции и взаимодействие между инструкциями и инструкциями могут привести к тому, что инструкции, которые последовательно помещаются в конвейер, будут выполняться не по порядку.Это так называемый «последовательный приток, неупорядоченный отток»..
Помимо того, что конвейер инструкций застревает в случае нехватки ресурсов (сумматор справляется с двумя инструкциями сложения, как было сказано выше), важной причиной блокировки конвейера является также корреляция между инструкциями.Неупорядоченное выполнение ЦП не является произвольным неупорядоченным выполнением, а основано на обеспечении причинно-следственной связи контекста программы.. С этой предпосылкой гарантируется правильность выполнения ЦП.
Например:
a++;
b=f(a);
c--;
потому чтоb=f(a)
Эта инструкция зависит от предыдущей инструкцииa++
результат выполнения , поэтомуb=f(a)
будет блокироваться перед фазой «выполнения», покаa++
Генерируется результат выполнения; иc--
Зависимости от предыдущего нет, может быть вb=f(a)
завершено раньше. (Обратите внимание, что здесьf(a)
не представляет собойa
является вызовом функции с аргументами, но вместо этого означаетa
инструкция для операнда. Вызовы функций на языке C требуют реализации нескольких инструкций, и ситуация более сложная).
Если инструкции с такими зависимостями расположены близко друг к другу, то последняя инструкция обязательно надолго заблокируется в конвейере, потому что она ждет результата предыдущего выполнения, занимая ресурсы конвейера.Переупорядочивание компилятора как средство оптимизации компиляции пытается разделить такие две инструкции посредством перестановки инструкций., чтобы при поступлении следующей инструкции в ЦП был получен результат предыдущей инструкции, поэтому нет необходимости блокироваться и ждать.Например, измените порядок инструкций следующим образом:
a++;
c--;
b=f(a);
По сравнению с нарушением порядка инструкций ЦП, нарушение порядка компилятора является реальной корректировкой порядка инструкций.. Но нарушение порядка компилятора также должно гарантировать, что причинно-следственная связь контекста программы не изменится.
Из-за существования переупорядочения и неупорядоченного выполнения, если синхронизация общих данных не выполняется должным образом в параллельном программировании, могут возникнуть различные, казалось бы, странные проблемы.