Барьеры и порядок выполнения

We use cookies. Read the Privacy and Cookie Policy

Барьеры и порядок выполнения

В случае, когда необходимо иметь дело с синхронизацией между разными процессорами или разными аппаратными устройствами, иногда возникает требование, чтобы чтение памяти (load) или запись в память (save) выполнялись в том же порядке, как это указано в исходном программном коде. При работе с аппаратными устройствами часто необходимо, чтобы некоторая указанная операция чтения была выполнена перед другими операциями чтения или записи. В дополнение к этому, на симметричной многопроцессорной системе может оказаться необходимым, чтобы операции записи выполнялись строго в том порядке, как это указано в исходном программном коде (обычно для того, чтобы гарантировать, что последовательные операции чтения получают данные в том же порядке). Эти проблемы усложняются тем, что как компилятор, так и процессор могут менять порядок операций чтения и записи[52] для повышения производительности. К счастью, все процессоры, которые переопределяют порядок операций чтения или записи предоставляют машинные инструкции, которые требуют выполнения операций чтения-записи памяти в указанном порядке. Также существует возможность дать инструкцию компилятору, что нельзя изменять порядок выполнения операций при переходе через определенную точку программы. Эти инструкции называются барьерами (barrier).

Рассмотрим следующий код.

а = 1;

b = 2;

На некоторых процессорах запись нового значения в область памяти, занимаемую переменной b, может выполниться до того, как будет записано новое значение в область памяти переменной а. Компилятор может выполнить такую перестановку статически и внести в файл объектного кода, что значение переменной b должно быть установлено перед переменной a. Процессор может изменить порядок выполнения динамически путем предварительной выборки и планирования выполнения внешне вроде бы независимых инструкций для повышения производительности. В большинстве случаев такая перестановка операций будет оптимальной, так как между переменными a и b нет никакой зависимости. Тем не менее иногда программисту все-таки виднее.

Хотя в предыдущем примере и может быть изменен порядок выполнения, ни процессор, ни компилятор никогда не будут менять порядок выполнения следующего кода, где переменные а и b являются глобальными.

а = 1;

b = а;

Это происходит потому, что в последнем случае четко видно зависимость между переменными a и b. Однако ни компилятор, ни процессор не имеют никакой информации о коде, который выполняется в других контекстах. Часто важно, чтобы результаты записи в память "виделись" в нужном порядке другим кодом, который выполняется за пределами нашей досягаемости. Такая ситуация часто имеет место при работе с аппаратными устройствами, а также возникает на многопроцессорных машинах.

Функция rmb() позволяет установить барьер чтения памяти (read memory barrier). Она гарантирует, что никакие операции чтения памяти, которые выполняются перед вызовом функции rmb(), не будут переставлены местами с операциями, которые выполняются после этого вызова. Иными словами, все операции чтения, которые указаны до этого вызова, будут выполнены перед этим вызовом, а все операции чтения, которые указаны после этого вызова никогда не будут выполняться перед ним.

Функция wmb() позволяет установить барьер записи памяти (write barrier). Она работает так же, как и функция rmb(), но не с операциями чтения, а с операциями записи — гарантируется, что операции записи, которые находятся по разные стороны барьера, никогда не будут переставлены местами друг с другом.

Функция mb() позволяет создать барьер на чтение и запись. Никакие операции чтения и записи, которые указаны по разные стороны вызова функции mb(), не будут переставлены местами друг с другом. Эта функция предоставляется пользователю, так как существует машинная инструкция (часто та же инструкция, что используется вызовом rmb()), которая позволяет установить барьер на чтение и запись.

Вариант функции rmb() — read_barrier_depends() — обеспечивает создание барьера чтения, но только для тех операций чтения, от которых зависят следующие, за ними операции чтения. Гарантируется, что все операции чтения, которые указаны перед барьером выполнятся перед теми операциями чтения, которые находятся после барьера и зависят от операций чтения, идущих перед барьером. Все понятно? В общем, эта функция позволяет создать барьер чтения, так же как и функция rmb(), но этот барьер будет установлен только для некоторых операций чтения — тех, которые зависят друг от друга.

Для некоторых аппаратных платформ функция read_barrier_depends() выполняется значительно быстрее, чем функция rmb(), так как для этих платформ функция read_barrier_depends() просто не нужна и вместо нее выполняется инструкция noop (нет операции).

Рассмотрим пример использования функций mb() и rmb(). Первоначальное значение переменной а равно 1, а переменной b равно 2.

Поток 1  Поток 2

а = 3;   -

mb();    -

b=4;     c=b;

-        rmb();

-        d=a;

Без использования барьеров памяти для некоторых процессоров возможна ситуация, в которой после выполнения этих фрагментов кода переменной с присвоится новое, значение переменной b, в то время как переменной d присвоится старое значение переменной а. Например, переменная с может стать равной 4 (что мы и хотим), а переменная d может остаться равной 1 (чего мы не хотим). Использование функции mb() позволяет гарантировать, что переменные a и b записываются в указанном порядке, а функция rmb() гарантирует, что чтение переменных b и а будет выполнено в указанном порядке.

Такое изменение порядка выполнения операций может возникнуть из-за того, что современные процессоры обрабатывают и передают на выполнение инструкции в измененном порядке для того, чтобы оптимизировать использование конвейеров. Это может привести к тому, что инструкции чтения переменных b и а выполнятся не в том порядке. Функции rmb() и wmb() соответствуют инструкциям, которые заставляют процессор выполнить все незаконченные операции чтения и записи перед тем, как продолжить работу далее.

Рассмотрим простой пример случая, когда можно использовать функцию read_barrier_depends() вместо функции rmb(). В этом примере изначально переменная а равна 1, b — 2, а p — &b.

Поток 1  Поток 2

а=3;    -

mb();   -

p=&а;   pp=p;

-       read_barrier_depends();

-       b=*pp;

Снова без использования барьеров памяти появляется возможность того, что переменной b будет присвоено значение *pp до того, как переменной pp будет присвоено значение переменной p. Функция read_barrier_depends() обеспечивает достаточный барьер, так как считывание значения *pp зависит от считывания переменной p. Здесь также будет достаточно использовать функцию rmb(), но поскольку операции чтения зависимы между собой, то можно использовать потенциально более быструю функцию read_barrier_depends(). Заметим, что в обоих случаях требуется использовать функцию mb() для того, чтобы гарантировать необходимый порядок выполнения операций чтения-записи в потоке 1.

Макросы smp_rmb(), smp_wmb(), smp_mb() и smpread_barrier_depends() позволяют выполнить полезную оптимизацию. Для SMP-ядра они определены как обычные барьеры памяти, а для ядра, рассчитанного на однопроцессорную машину, — только как барьер компилятора. Эти SMP-варианты барьеров можно использовать, когда ограничения на порядок выполнения операций являются специфичными для SMP-систем.

Функция barrier() предотвращает возможность оптимизации компилятором операций считывания и записи данных, если эти операции находятся по разные стороны от вызова данной функции (т.е. запрещает изменение порядка операций). Компилятор не изменяет порядок операций записи и считывания в случаях, когда это может повлиять на правильность выполнения кода, написанного на языке С, или на существующие зависимости между данными. Однако у компилятора нет информации о событиях, которые могут произойти вне текущего контекста. Например, компилятор не может иметь информацию о прерываниях, в контексте которых может выполняться считывание данных, которые в данный момент записываются. Например, по этой причине может оказаться необходимым гарантировать, что операция записи выполнится перед операцией считывания. Указанные ранее барьеры памяти работают и как барьеры компилятора, но барьер компилятора значительно быстрее, чем барьер памяти (практически не влияет на производительность). Использование барьера компилятора на практике является опциональным, так как он просто предотвращает возможность того, что компилятор что-либо изменит.

В табл. 9.10 приведен полный список функций установки барьеров памяти и компилятора, которые доступны для разных аппаратных платформ, поддерживаемых ядром Linux.

Таблица 9.10. Средства установки барьеров компилятора и памяти

Барьер Описание rmb() Предотвращает изменение порядка выполнения операций чтения данных из памяти при переходе через барьер read_barrier_depends() Предотвращает изменение порядка выполнения операций чтения данных из памяти при переходе через барьер, но только для операций чтения, которые зависимы друг от друга wmb() Предотвращает изменение порядка выполнения операций записи данных в память при переходе через барьер mb() Предотвращает изменение порядка выполнения операций чтения и записи данных при переходе через барьер smp_rmb() Для SMP-ядер эквивалентно функции rmb(), а для ядер, рассчитанных на однопроцессорные машины, эквивалентно функции barrier() smp_read_barrier_depends() Для SMP-ядер эквивалентно функции read_barrier_depends(), а для ядер, рассчитанных на однопроцессорные машины, эквивалентно функции barrier() smp_wmb() Для SMP-ядер эквивалентно функции wmb(), а для ядер, рассчитанных на однопроцессорные машины, эквивалентно функции barrier() smp_mb() Для SMP-ядер эквивалентно функции mb(), а для ядер, рассчитанных на однопроцессорные машины, эквивалентно функции barrier() barrier() Предотвращает оптимизации компилятора по чтению и записи данных при переходе через барьер

Следует заметить, что эффекты установки барьеров могут быть разными для разных аппаратных платформ. Например, если машина не изменяет порядок операций записи (как в случае набора микросхем Intel x86), то функция wmb() не выполняет никаких действий. Можно использовать соответствующий барьер памяти для самой плохой ситуации (т.е. для процессора с самым плохим порядком выполнения), и ваш код будет скомпилирован оптимально для вашей аппаратной платформы.