Crack Concurrency (11): изменение порядка моделей памяти

Java задняя часть программист переводчик

0 Предисловие

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

  1. переводчикМожет свободно менять порядок инструкций во имя оптимизации;
  2. При определенных обстоятельствах,процессорМожет выполнять инструкции не по порядку;
  3. данные могут быть вРегистры, буферы процессора и основная памятьв порядке, отличном от порядка, заданного программой;

Например, если поток записывает значение в полеa, затем запишите значение в полеbbЗначение не зависит от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. Каков может быть эффект, когда операция 1 и операция 2 переупорядочены?

    当操作 1 和操作 2 重排序时

    Как показано на рисунке выше, операции 1 и 2 переупорядочены. Когда программа выполняется, поток A сначала записывает переменную флага flag, а затем поток B читает эту переменную. Поскольку условие оценивается как истинное, поток B будет читать переменную a.В этот момент переменная a вообще не была записана потоком A, а семантика многопоточных программ нарушается переупорядочением!

  2. Какой может быть эффект, если переупорядочить операции 3 и 4? (При таком переупорядочении можно, кстати, объяснить зависимости управления)

    当操作 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);

По сравнению с нарушением порядка инструкций ЦП, нарушение порядка компилятора является реальной корректировкой порядка инструкций.. Но нарушение порядка компилятора также должно гарантировать, что причинно-следственная связь контекста программы не изменится.

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