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. Средства установки барьеров компилятора и памяти