ЛЕКЦИЯ 6. Атомарные операции. Внеочередное выполнение инструкций. Барьеры памяти. Семантика захвата-освобождения. Модель памяти C++
Курс "Параллельные вычислительные технологии" (ПВТ), осень 2014
Сибирский государственный университет телекоммуникаций и информатики
преподаватель:
Пазников Алексей Александрович
к.т.н., доцент кафедры вычислительных систем СибГУТИ
разработка серверов и серверных приложений лекция №3
Similaire à ПВТ - осень 2014 - Лекция 6 - Атомарные операции. Внеочередное выполнение инструкций. Барьеры памяти. Семантика захвата-освобождения. Модель па
Евгений Крутько — Опыт внедрения технологий параллельных вычислений для повыш...Yandex
Similaire à ПВТ - осень 2014 - Лекция 6 - Атомарные операции. Внеочередное выполнение инструкций. Барьеры памяти. Семантика захвата-освобождения. Модель па (20)
4. ˑ̃˿˽˱́˾̌˶˿̀˶́˱̇˹˹
▪ Операция над разделяемой переменной
атомарная, если она выполняется потоком
за один неделимый шаг. Ни один из других
потоков не может обнаружить эту переменную
в промежуточном состоянии.
▪ Если операции, которые совершают потоки над
раздялемыми переменными, не атомарны, то это
приведёт к гонкам данных.
▪ Гонки данных являются причиной неопредлённого
поведения, поскольку они приводят к частичным
(фрагментированным, “разорванным”)
чтениям и записям переменных.
3
5. ˥́˱˴˽˶˾̃˹́˿˳˱˾˾̌˶̈̃˶˾˹̐˹˸˱̀˹̂˹
uint64_t shared;
int main() {
shared = 0x100000002;
...
gcc -m32 -S -masm=intel -O1 prog.c
mov DWORD PTR shared, 2
mov DWORD PTR shared+4, 1
запись младших 32 бит
запись старших 32 бит
▪ Выполнение присваивания 64-битного целого shared = 42
на 32-разрядной архитектуре выполняется
за 2 инструкции.
▪ Операция записи не атомарная.
4
6. ˥́˱˴˽˶˾̃˹́˿˳˱˾˾̌˶̈̃˶˾˹̐˹˸˱̀˹̂˹
uint64_t shared;
int main() {
shared = 0x100000002;
...
gcc -m32 -S -masm=intel -O1 prog.c
mov DWORD PTR shared, 2
mov DWORD PTR shared+4, 1
1 поток
запись старших 32 бит
▪ Вытеснение потока после записи младших бит приведёт к
тому, что эти биты останутся в памяти и будут
использованы другими потоками.
▪ На многоядерных системах даже не требуется
вытеснения. 5
7. ˥́˱˴˽˶˾̃˹́˿˳˱˾˾̌˶̈̃˶˾˹̐˹˸˱̀˹̂˹
uint64_t shared;
int main() {
shared = 0x100000002;
...
gcc -m32 -S -masm=intel -O1 prog.c
mov DWORD PTR shared, 2
mov DWORD PTR shared+4, 1
1 поток
2 поток
▪ Вытеснение потока после записи младших бит приведёт к
тому, что эти биты останутся в памяти, а старшие будут
записаны другим потоком.
▪ На многоядерных системах даже не требуется
вытеснения. 6
8. ˥́˱˴˽˶˾̃˹́˿˳˱˾˾̌˶̈̃˶˾˹̐˹˸˱̀˹̂˹
uint64_t shared;
uint64_t getShared() {
return shared;
}
gcc -m32 -S -masm=intel -O1 prog.c
mov eax, DWORD PTR shared
mov edx, DWORD PTR shared+4
ret
чтение младших 32 бит
чтение старших 32 бит
▪ Выполнение чтения 64-битного целого shared на 32-
разрядной архитектуре выполняется
за 2 инструкции.
▪ Операция чтения не атомарная.
7
9. ˥́˱˴˽˶˾̃˹́˿˳˱˾˾̌˶̈̃˶˾˹̐˹˸˱̀˹̂˹
▪ Инструкции могут быть неатомарными, даже если
выполняются одной процессорной инструкцией.
Например, в ARMv7 инструкция для помещения содержимого
двух 32-битных регистров в один 64-битный:
strd r0, r1, [r2]
На некоторых процессорах эта инструкция реализуются двумя
отдельными операциями сохранения.
32-битная операция mov атомарна только для выравненных
данных. В остальных случаях операция неатомарная
▪ Вытеснение потоков, выполняющих данные операции, или
выполнение операций в двугих потоках в многоядерных
системах приводит к неопределённому поведению.
8
10. ˑ̃˿˽˱́˾˿̂̃̍˳ˢ˹ˢ
▪ В языках С и С++ предполагается, что все
операции неатомарны.
▪ Операции могут быть атомарными в
большинстве случаев, например, операция
присваивания 32-битного целого значения.
▪ Тем не менее, все инструкции должны
рассматриваться как неатомарные.
▪ К счастью, в С и С++ есть набор шаблонов
атомарных типов данных.
9
11. ˑ̃˿˽˱́˾˿̂̃̍˳ˢ˹ˢ
▪ Атомарные операции в С++ неделимы. Из
любого потока нельзя обнаружить эту
операцию выполненной частично - она
либо выполнена, либо невыполнена.
▪ Это позволяет избежать гонок данных.
▪ Для атомарных типов определён метод
is_lock_free, позволяющий определить,
являются ли операции над ним напрямую
с помощью атомарных инструкций, или они
эмулируются.
10
12. ˑ̃˿˽˱́˾̌˶̃˹̀̌˳ˢ˹ˢ
▪ std::atomic_flag - единственный тип,
который не имеет функции is_lock_free. Он
предельно простой, всецело атомарный и
поддерживает одну операцию: test_and_set -
проверить и установить.
▪ Остальные типы определяются
специализацией шаблона std::atomic,
например std::atomicint и std::
atomicvoid*
11
14. ˟̀˶́˱̇˹˹˾˱˵˱̃˿˽˱́˾̌˽˹̃˹̀˱˽˹
▪ Операции сохранения: store, clear, etc.
Упорядочение memory_order_relaxed,
memory_order_release, memory_order_seq_cst.
▪ Операции загрузки: load, etc.
Упорядочение memory_order_relaxed,
memory_order_consume, memory_order_acquire,
memory_order_seq_cst.
▪ Операции чтения-модификации-записи:
compare_exchange, fetch_add, test_and_set, etc.
Упорядочение memory_order_relaxed,
memory_order_consume, memory_order_acquire,
memory_order_release, memory_order_seq_cst,
memory_order_acq_rel.
13
15. ˑ̃˿˽˱́˾̌˺̅˼˱˴VWGDWRPLFBIODJ
std::atomic_flag должен быть проиниализирован:
std::atomic_flag flag = ATOMIC_FLAG_INIT
Очистить флаг (операция сохранения): установить значение
false:
flag.clear(std::memory_order_release);
Установить значение флага в true и вернуть предыдущее
значение:
bool x = flag.test_and_set();
Для атомарного флага запрещены операции копирования и
присваивания.
14
16. ˡ˶˱˼˹˸˱̇˹̐̂̀˹˾˼˿˻˱˾˱˿̂˾˿˳˶˱̃˿˽˱́˾˿˴˿̅˼˱˴˱
class spinlock_mutex
{
std::atomic_flag flag;
public:
spinlock_mutex():
flag{ATOMIC_FLAG_INIT} { }
void lock() {
while (flag.test_and_set(
std::memory_order_acquire));
}
void unlock() {
flag.clear(std::memory_order_release);
}
};
n.b. Можно использовать с lock_guard и unique_guard! 15
17. ˟̀˶́˱̇˹˹̂˿̆́˱˾˶˾˹̐˹˸˱˴́̄˸˻˹˱̃˿˽˱́˾̌̆̃˹̀˿˳
// Объявить переменную и проициализировать true
std::atomicbool b(true);
// Загрузить значение в переменной в неатомарную
// переменную x
bool x = b.load(std::memory_order_acquire);
// Записать в переменную b значение true
b.store(true);
// Обменять значение переменной b со значением false,
// вернуть предыдущее значение b в переменную x.
x = b.exchange(false, std::memory_order_acq_rel);
16
18. ˟̀˶́˱̇˹̐q̂́˱˳˾˹̃̍˹˿˲˽˶˾̐̃̍r
Операция compare_exchange (“сравнить и обменять”):
1. Сравнить текущее значение атомарной переменной с
ожидаемым expected.
2. Если значения совпали, сохранить новое значение
desired и вернуть true.
3. Если значения не совпадают, то ожидаемое значение
expected заменяется фактическим значением
переменной, функция возвращает false.
bool compare_exchange_weak(T expected, T desired,
std::memory_order order =
std::memory_order_seq_cst);
bool compare_exchange_strong(T expected, T desired,
std::memory_order order =
std::memory_order_seq_cst);
17
19. ˟̀˶́˱̇˹̐q̂́˱˳˾˹̃̍˹˿˲˽˶˾̐̃̍r
compare_exchange_weak() - сохранение может не
произойти, даже если текущее значение совпадает с
ожидаемым. Значение переменной не изменится, функция
возвращает false.
Последнее возможно при отсутствии аппаратной
поддержки команды сравнить-и-обменять, из-за того, что
поток может быть вытеснен в середине требуемой
последовательности команд (ложный отказ).
Из-за возможного ложного отказа функцию
compare_exchange_weak() обычно вызывают в цикле:
bool expected = false;
extern atomicbool b;
...
while (!b.compare_exchange_weak(expected, true);
18
20. ˟̀˶́˱̇˹̐q̂́˱˳˾˹̃̍˹˿˲˽˶˾̐̃̍r
compare_exchange_strong() гарантирует замену
переменной в случае выполнения условия.
▪ compare_exchange_strong() выгодно использовать в
случае однократном выполнении операции, т.е. при
необходимости заменить значение на жалаемое, и в
случае, если вычисление нового значения занимает
длительное время.
▪ Если функция compare_exchange вызывается в цикле,
тогда предпочтительнее использовать
compare_exchange_weak, чтобы избежать двойного
цикла (compare_exchange_strong реализован в виде
цикла на системах, которые не поддерживают
атомарной операции сравнения и замены).
19
21. ˑ̃˿˽˱́˾̌˺̃˹̀DWRPLF7
!
Функции для типа atomicT*:
▪ is_lock_free, load, store, exchange,
compare_exchange_weak, compare_exchange_strong
▪ обменять и прибавить:
fetch_add, operator++, operator+=
▪ обменять и вычесть:
fetch_sub, operator--, operator-=
class C {};
C arr[10];
std::atomicC* ptr(arr);
C* x = ptr.fetch_add(2);
assert(x == arr);
assert(p.load() == some_array[2]);
p--; x = p;
assert(x == arr[1]);
операции
чтения-
модификации-
записи
20
22. ˢ̃˱˾˵˱́̃˾̌˶˱̃˿˽˱́˾̌˶̇˶˼˿̈˹̂˼˶˾˾̌˶̃˹̀̌
Функции для типов стандартных атомарных
целочисленных типов (int, unsigned int, long,
unsigned long, etc):
▪ is_lock_free, load, store, exchange,
compare_exchange_weak,
compare_exchange_strong
▪ обменять и прибавить (fetch_add, operator++,
operator+=) обменять и вычесть (fetch_sub,
operator--, operator-=)
▪ operator=, operator|=, operator^=
Отсутствуют операции:
▫ умножения, деления, сдвига
21
23. ˠ˿˼̍˸˿˳˱̃˶˼̍̂˻˹˶˱̃˿˽˱́˾̌˶̃˹̀̌
В качестве шаблона std::atomic может
выступать тип, он должен удовлетворять
требованиям
▪ В нём должен присутствовать тривиальный
оператор присваивания. Нет виртуальных
функций и виртуальных базовых классов, а
оператор присваивания генерируется
автоматически (например, memcpy)
▪ Тип должен допускать побитовое сравнение на
равенство (например, с помощью memcmp)
22
25. ˠ́˹˽˶́̌̀˶́˶̄̀˿́̐˵˿̈˹˳˱˾˹̐˳ˢ
x = y = v = w = 0
thread1() {
x = 1
v = y
}
thread2() {
y = 1
w = x
}
main() {
start_threads(thread1, thread2)
join_all_threads()
assert(v != 0 || w != 0) assert может сработать!
24
26. ˠ́˹˽˶́̌̀˶́˶̄̀˿́̐˵˿̈˹˳˱˾˹̐˳ˢ
ready = false
producer() {
data = 42
ready = true
}
consumer() {
while (!ready)
{ }
print(data)
}
data = 42 не гарантируется
25
27. ˠ́˹˽˶́̌̀˶́˶̄̀˿́̐˵˿̈˹˳˱˾˹̐˳ˢ
x = y = false, z = 0
thread_store_x() { x = true }
thread_store_y() { y = true }
thread_read_x_then_y() {
while (!x) { }
if (y) z++
}
thread_read_y_then_x() {
while (!y) { }
if (x) z++
}
main {
run_all_threads(store_x, store_y,
read_x_then_y, read_y_then_x)
join_all_threads()
assert(z != 0) assert может сработать! 26
31. ˟˵˹˳˾̌˺˾˿˳̌˺˹̀́˶˻́˱̂˾̌˺̀˱́˱˼˼˶˼̍˾̌˺˽˹́
Выполняет ли компьютер программу, которую вы
написали?
НЕТ Просто дело в том что...
иерархическая структура памяти, внеочередное
выполнение команд процессора и компиляторная
оптимизация. 30
38. ˛˿˽̀˹˼̐̃˿́˾˱̐˿̀̃˹˽˹˸˱̇˹̐˳˹˵̌˿̀̃˹˽˹˸˱̇˹˺
▪ Peephole-оптимизация
▪ Локальная оптимизация
▪ Внутрипроцедурная оптимизация
▪ Оптимизация циклов
▫ Анализ индуктивных переменных
▫ Деление цикла на части
▫ Объединение циклов
▫ Инверсия цикла
▫ Расщепление цикла
▪ Межпроцедурная оптимизация 37
40. ˛˿˽̀˹˼̐̃˿́˾˱̐˿̀̃˹˽˹˸˱̇˹̐̀́˹˽˶́̌˿̀̃˹˽˹˸˱̇˹˺
x = 1;
y = 2;
x = 3;
for (i = 0; i n; i++)
sum += a[i];
y = 2;
x = 3;
r1 = sum;
for (i = 0; i n; i++)
r1 += a[i];
sum = r1;
for (i = 0; i n; i++)
j = 42 * i;
j = -42
for (i = 0; i n; i++)
j = j + 42;
39
41. ˛˿˽̀˹˼̐̃˿́˾˱̐˿̀̃˹˽˹˸˱̇˹̐̀́˹˽˶́̌˿̀̃˹˽˹˸˱̇˹˺
x = Johann;
y = Sebastian;
z = Bach;
for (i = 0; i n; i++)
for (j = 0; j m;j++);
a[i][j] = 1;
z = Bach;
x = Johann;
y = Sebastian;
for (j = 0; j n; j++)
for (i = 0; i m;
i++);
a[i][j] = 1;
for (i = 0; i n; i++)
a[i] = 1;
for (i = 0; i n; i++)
b[i] = 2;
for (i = 0; i n; i++)
{
a[i] = 1;
b[i] = 2;
} 40
42. ˛˿˽̀˹˼̐̃˿́˾˱̐˿̀̃˹˽˹˸˱̇˹̐̀́˹˽˶́̌˿̀̃˹˽˹˸˱̇˹˺
Что компилятор знает:
Все операции с памятью совершаются в текущем
потоке, что они в точности означают, и какие
существуют зависимости по данным.
Что компилятор не знает:
Какие области памяти доступны и изменяются в
разных потоках.
Как решить:
Сказать! Как-то пометить операции, которые
выполняются с разделяемыми переменными.
41
43. ˮ̃˱̀̌̃́˱˾̂̅˿́˽˱̇˹˹̀́˿˴́˱˽˽̌
Исходный код
Компилятор
удаление подвыражений, свёртка
констант, оптимизация циклов, ...
Процессор
предвыборка, спекулятивное
выполнение инструкций,
буферизация, HTM, ...
Кэш
частные и общие кэши, буферы
записи, ...
Реальное выполнение
программы
“Гораздо лучше
выполнять другую
программу - не ту, что
вы написали. Вам на
самом деле даже не
хочется выполнять эту
вашу чушь - вы хотите
запускать другую
программу! Всё это
делается для вашего
блага.”
42
44. ˮ̃˱̀̌̃́˱˾̂̅˿́˽˱̇˹˹̀́˿˴́˱˽˽̌
▪ Как правило нельзя
определить, на каком уровне
произошла трансформация.
▪ Трансформации на всех
уровнях эквивалентны, что
позволяет рассматривать их
как переупорядочивание
операций загрузки (loads)
и записи (stores).
▪ Необходимое условие при
выполнении трансформаций
- сохранение иллюзии
последовательно
согласованного кода.
43
Исходный код
Компилятор
удаление подвыражений, свёртка
констант, оптимизация циклов, ...
Процессор
предвыборка, спекулятивное
выполнение инструкций,
буферизация, HTM, ...
Кэш
частные и общие кэши, буферы
записи, ...
Реальное выполнение
программы
46. “Результат выполнения программы такой, как если бы
операции всех процессоров выполнялись последовательно и
результат операции каждого отдельного процессора
появлялся бы в этой последовательности в порядке,
который определяется программой.” (Л. Лэмпорт, 1979)
▪ Рассмотрим многопроцессорную систему, состоящую из
нескольких последовательных процессоров.
▪ Операции, выполняемые процессорами над некоторой
областью памяти (страница, объект, адрес, ...),
появляются в одном и том же порядке для всех
процессоров, несмотря на то, что фактическая
последовательность выполнения операций может
быть другой.
44
48. “Результат выполнения программы такой, как если бы
операции всех процессоров выполнялись последовательно и
результат операции каждого отдельного процессора
появлялся бы в этой последовательности в порядке,
который определяется программой.” (Л. Лэмпорт, 1979)
▪ Рассмотрим многопроцессорную систему, состоящую из
нескольких последовательных процессоров.
▪ Операции, выполняемые процессорами над некоторой
областью памяти (страница, объект, адрес, ...),
появляются в одном и том же порядке для всех
процессоров, несмотря на то, что фактическая
последовательность выполнения операций может
быть другой.
И это прекрасно!
45
50. “Результат выполнения программы такой, как если бы
операции всех процессоров выполнялись последовательно и
результат операции каждого отдельного процессора
появлялся бы в этой последовательности в порядке,
который определяется программой.” (Л. Лэмпорт, 1979)
▪ Рассмотрим многопроцессорную систему, состоящую из
нескольких последовательных процессоров.
▪ Операции, выполняемые процессорами над некоторой
областью памяти (страница, объект, адрес, ...),
появляются в одном и том же порядке для всех
процессоров, несмотря на то, что фактическая
последовательность выполнения операций может
быть другой.
И это прекрасно!
но...46
52. … но вы этого не хотите!
▪ Скорее всего очень нерационально выполнять в точности
то, что вы написали.
▪ Гораздо лучше выполнить нечто иное, которое бы работало
так же, как и то, что вы написали, но выполнялось бы
гораздо быстрее.
Поэтому
▪ Мы (программное обеспечение ПО: компилятор и
аппаратное обеспечение АО: кэш, процессор) будем это
делать!
▪ А вы (программисты), в свою очередь, должны обеспечить
возможность корректной трансформации и выполнения
программы так, чтобы сохранялась иллюзия
последовательной согласованности, включив в свою
программу необходимые ограничения. 47
53. ˝˿˵˶˼̍̀˱˽̐̃˹̎̃˿˵˿˴˿˳˿́
Вы обещаете
Корректно
реализовать
синхронизацию
в вашей
программе
(путём добавления
необходимых
инструкций в
программу,
делающих её
безопасной
относительно гонок)
Система
обещает
Обеспечить
иллюзию
выполнения той
программы,
которую вы
написали.
(путём компиляции
и выполнения)
48
54. ˝˿˵˶˼̍̀˱˽̐̃˹̎̃˿˵˿˴˿˳˿́
Вы обещаете
Корректно
реализовать
синхронизацию
в вашей
программе
(путём добавления
необходимых
инструкций в
программу,
делающих её
безопасной
относительно гонок)
Система
обещает
Обеспечить
иллюзию
выполнения той
программы,
которую вы
написали.
(путём компиляции
и выполнения)
Модель памяти определяет, какие действия вы должны
совершить и как должна отреагировать система, чтобы
обеспечить выполнение операций с памятью в необходимой
последовательности. 49
56. ˠ˶́˶̄̀˿́̐˵˿̈˹˳˱˾˹˶˾˱̀́˹˽˶́˶˱˼˴˿́˹̃˽˱˕˶˻˻˶́˱
Алгоритм Деккера позволяет решать проблему взаимного
исключения и был опубликован в 1965 г.
Он не приводит к взаимным исключениям (deadlock) и
свободен от голодания (starvation).
Поток 1
flag1 = 1;
if (flag2 != 0)
// ожидать освобождения
// критической секции
else
// войти в критическую
// секцию
Поток 2
flag2 = 1;
if (flag1 != 0)
// ожидать освобождения
// критической секции
else
// войти в критическую
// секцию
51
64. ˠ˶́˶̄̀˿́̐˵˿̈˹˳˱˾˹˶˳́˱˸˼˹̈˾̌̆̀́˿̇˶̂̂˿́˱̆
более сильное упорядочивание
(strong model, sequential consistency)
IBM Blue Gene
Смартфоны
DEC Alpha
Xbox 360
PowerPC
ARM
x86 / 64
SPARC TSO
более слабое упорядочивание
(weak model, relaxed ordering)
Как правило сильное
упорядочивание
Ослабленное упорядочивание
с поддержкой зависимостей
по данным
Вполне
ослабленное
упорядочивание
PowerMac
59
65. ˠ˶́˶̄̀˿́̐˵˿̈˹˳˱˾˹˶˳́˱˸˼˹̈˾̌̆̀́˿̇˶̂̂˿́˱̆
более сильное упорядочивание
(strong model, sequential consistency)
IBM Blue Gene
Смартфоны
DEC Alpha
Xbox 360
PowerPC
ARM
x86 / 64
SPARC TSO
более слабое упорядочивание
(weak model, relaxed ordering)
Как правило сильное
упорядочивание
Ослабленное упорядочивание
с поддержкой зависимостей
по данным
Вполне
ослабленное
упорядочивание
PowerMac
При сильном
упорядочивании
(сильная модель памяти)
каждая инструкция
неявно реализует
семантику захвата и
освобождения.
При слабом
упорядочивании
(слабая модель памяти)
не накладывается
никаких ограничений на
порядок выполнения
инструкций.
60
66. ˠ˶́˶̄̀˿́̐˵˿̈˹˳˱˾˹˶˳̀́˿̇˶̂̂˿́˱̆˱́̆˹̃˶˻̃̄́̌[
более сильное упорядочивание
(strong model, sequential consistency)
IBM Blue Gene
Смартфоны
DEC Alpha
Xbox 360
PowerPC
ARM
x86 / 64
SPARC TSO
более слабое упорядочивание
(weak model, relaxed ordering)
Как правило сильное
упорядочивание
Ослабленное упорядочивание
с поддержкой зависимостей
по данным
Вполне
ослабленное
упорядочивание
PowerMac
61
67. ˠ˶́˶̄̀˿́̐˵˿̈˹˳˱˾˹˶˳̀́˿̇˶̂̂˿́˱̆˱́̆˹̃˶˻̃̄́̌[
Intel Architectures Software Developer’s Manual, Vol. 3:
▪ Операции чтения не могут быть переупорядочены с другими
операциями чтения
▪ Операции записи не могут быть переупорядочены с другими
операциями записями
▪ Операции записи не могут быть переупорядочены с другими
операциями записи, кроме следующих исключений: …
▪ Операции чтения могут быть переупорядочены с более
старыми операциями записи в другие области памяти,
но не с операциями записи в ту же область.
▪ Операции чтения не могут перейти раньше инструкций LFENCE,
MFENCE, операции записи - раньше инструкций LFENCE,
SFENCE, MFENCE.
▪ LFENCE, SFENCE, MFENCE не могут выполниться раньше
операций чтения, записи, того или другого соответственно.
62
68. ˠ˶́˶̄̀˿́̐˵˿̈˹˳˱˾˹˶˳̀́˿̇˶̂̂˿́˱̆˱́̆˹̃˶˻̃̄́̌[
x = 0, y = 0
Процессор 1
mov x, 1 ; запись (load)
; 1 в x
mov r1, y ; загрузка (store)
; из y в регистр
Процессор 2
mov y, 1 ; запись (load)
; 1 в x
mov r2, x ; загрузка (store)
; из y в регистр
Возможные варианты:
▪ r1 = 0, r2 = 1
▪ r1 = 1, r2 = 0
▪ r1 = 1, r2 = 1
▪ r1 = 0, r2 = 0
63
69. ˠ˶́˶̄̀˿́̐˵˿̈˹˳˱˾˹˶˳̀́˿̇˶̂̂˿́˱̆˱́̆˹̃˶˻̃̄́̌[
x = 0, y = 0
Процессор 1
mov x, 1 ; запись (load)
; 1 в x
StoreLoad
mov r1, y ; загрузка (store)
; из y в регистр
Процессор 2
mov y, 1 ; запись (load)
; 1 в x
mov r2, x ; загрузка (store)
; из y в регистр
▪ r1 = 0, r2 = 0
В процессорах архитектуры x86 достустима перестановка
операция load после store. Или, то же, операции store могут
выполняться до операций load.
64
76. ˠ˶́˶̄̀˿́̐˵˿̈˹˳˱˾˹˶˳̀́˿̇˶̂̂˿́˱̆̂˿̂˼˱˲˼˶˾˾̌˽̄̀˿́̐˵˿̈˹˳˱˾˹˶˽
более сильное упорядочивание
(strong model, sequential consistency)
IBM Blue Gene
Смартфоны
DEC Alpha
Xbox 360
PowerPC
ARM
x86 / 64
SPARC TSO
более слабое упорядочивание
(weak model, relaxed ordering)
Как правило сильное
упорядочивание
Ослабленное упорядочивание
с поддержкой зависимостей
по данным
Вполне
ослабленное
упорядочивание
PowerMac
71
77. ˠ˶́˶̄̀˿́̐˵˿̈˹˳˱˾˹˶˳̀́˿̇˶̂̂˿́˱̆̂˿̂˼˱˲˼˶˾˾̌˽̄̀˿́̐˵˿̈˹˳˱˾˹˶˽
Для ослабленного упорядочивания характерно то, что одно ядро может
видеть изменения в общей памяти в порядке, отличном от порядка, в
котором другое ядро вносит изменения.
int sharedCount; // глобальный счётчик
void IncShared() {
int count = 0; // локальный счётчик
while (count N) {
randomBusyWork(); // случайная задержка
// наивная реализация мьютекса
int expected = 0;
if (flag.compare_exchange_strong(expected, 1,
std::memory_order_relaxed)); {
sharedVal++; // выполняется под защитой мьютекса
flag.store(0, std::memory_order_relaxed))
count++;
}
}
72
78. ˠ˶́˶̄̀˿́̐˵˿̈˹˳˱˾˹˶˳̀́˿̇˶̂̂˿́˱̆̂˿̂˼˱˲˼˶˾˾̌˽̄̀˿́̐˵˿̈˹˳˱˾˹˶˽
Для ослабленного упорядочивания характерно то, что одно ядро может
видеть изменения в общей памяти в порядке, отличном от порядка, в
котором другое ядро вносит изменения.
int sharedCount; // глобальный счётчик
void IncShared() {
int count = 0; // локальный счётчик
while (count N) {
randomBusyWork(); // случайная задержка
// наивная реализация мьютекса
int expected = 0;
if (flag.compare_exchange_strong(expected, 1,
std::memory_order_relaxed)); {
sharedVal++; // выполняется под защитой мьютекса
flag.store(0, std::memory_order_relaxed))
count++;
}
} StoreStore
73
79. ˠ˶́˶̄̀˿́̐˵˿̈˹˳˱˾˹˶˳̀́˿̇˶̂̂˿́˱̆̂˿̂˼˱˲˼˶˾˾̌˽̄̀˿́̐˵˿̈˹˳˱˾˹˶˽
Для ослабленного упорядочивания характерно то, что одно ядро может
видеть изменения в общей памяти в порядке, отличном от порядка, в
котором другое ядро вносит изменения.
int sharedCount; // глобальный счётчик
void IncShared() {
int count = 0; // локальный счётчик
while (count N) {
randomBusyWork(); // случайная задержка
// наивная реализация мьютекса
int expected = 0;
if (flag.compare_exchange_strong(expected, 1,
std::memory_order_relaxed)); {
sharedVal++; // выполняется под защитой мьютекса
asm volatile( ::: memory);
flag.store(0, std::memory_order_relaxed))
count++;
}
}
StoreStore
запрет компиляторного
переупорядочивания 74
80. ˠ˶́˶̄̀˿́̐˵˿̈˹˳˱˾˹˶˳̀́˿̇˶̂̂˿́˱̆̂˿̂˼˱˲˼˶˾˾̌˽̄̀˿́̐˵˿̈˹˳˱˾˹˶˽
Поток 1
int sharedCount;
void IncShared() {
int count = 0;
while (count N) {
randomBusyWork();
int expected = 0;
if (...) {
sharedVal++;
asm volatile(...);
flag.store(0, ...)
count++;
}
}
Поток 2
int sharedCount;
void IncShared() {
int count = 0;
while (count N) {
randomBusyWork();
int expected = 0;
if (...) {
sharedVal++;
asm volatile(...);
flag.store(0, ...)
count++;
}
}
75
81. ˠ˶́˶̄̀˿́̐˵˿̈˹˳˱˾˹˶˳̀́˿̇˶̂̂˿́˱̆̂˿̂˼˱˲˼˶˾˾̌˽̄̀˿́̐˵˿̈˹˳˱˾˹˶˽
Поток 1
int sharedCount;
void IncShared() {
int count = 0;
while (count N) {
randomBusyWork();
int expected = 0;
if (...) {
sharedVal++;
asm volatile(...);
(1) flag.store(0, ...);
count++;
}
}
Поток 2
int sharedCount;
void IncShared() {
int count = 0;
while (count N) {
randomBusyWork();
int expected = 0;
if (...) {
sharedVal++;
asm volatile(...);
flag.store(0, ...)
count++;
}
}
76
82. ˠ˶́˶̄̀˿́̐˵˿̈˹˳˱˾˹˶˳̀́˿̇˶̂̂˿́˱̆̂˿̂˼˱˲˼˶˾˾̌˽̄̀˿́̐˵˿̈˹˳˱˾˹˶˽
Поток 1
int sharedCount;
void IncShared() {
int count = 0;
while (count N) {
randomBusyWork();
int expected = 0;
if (...) {
(2) sharedVal++;
asm volatile(...);
(1) flag.store(0, ...);
count++;
}
}
Поток 2
int sharedCount;
void IncShared() {
int count = 0;
while (count N) {
randomBusyWork();
int expected = 0;
if (...) {
(3) sharedVal++;
asm volatile(...);
flag.store(0, ...)
count++;
}
}
77
83. ˠ˶́˶̄̀˿́̐˵˿̈˹˳˱˾˹˶˳̀́˿̇˶̂̂˿́˱̆̂˿̂˼˱˲˼˶˾˾̌˽̄̀˿́̐˵˿̈˹˳˱˾˹˶˽
Допустим N = 100000
Каждый из 2 потоков выполняет 100000 операций
инкремента разделяемой переменной sharedCount++.
Ожидаемое значение: sharedVal = 200000
В реальности:
$ ./prog 100000 // N = 100000
sharedCount = 199348
sharedCount = 199034
sharedCount = 199517
sharedCount = 199829
sharedCount = 199113
sharedCount = 199566
78
84. ˠ˶́˶̄̀˿́̐˵˿̈˹˳˱˾˹˶̀́˹˳̌̀˿˼˾˶˾˹˹˿̀˶́˱̇˹̐˸˱̆˳˱̃˱˿̂˳˿˲˿˷˵˶˾˹̐˳
̀́˿̇˶̂̂˿́˱̆˱́̆˹̃˶˻̃̄́̌32:(5
Поток 1 (процессор 1)
// Подготавливаем данные
// и устанавливаем флаг
// готовности
for (i = 0; i n; i++) {
widget[i].x = x;
widget[i].y = y;
widget[i].ready = true;
}
Поток 2 (процессор 2)
// Если данные готовы,
// обрабатываем их
for (i = 0; i n; i++) {
if (widget[i].ready) {
do_some_stuff(widget[i]);
}
}
Буфер 1 Буфер 2 Буфер N
L2-кэш
... ...
79
85. ˠ˶́˶̄̀˿́̐˵˿̈˹˳˱˾˹˶̀́˹˳̌̀˿˼˾˶˾˹˹˿̀˶́˱̇˹̐˸˱̆˳˱̃˱˿̂˳˿˲˿˷˵˶˾˹̐˳
̀́˿̇˶̂̂˿́˱̆˱́̆˹̃˶˻̃̄́̌32:(5
Поток 1 (процессор 1)
// Подготавливаем данные
// и устанавливаем флаг
// готовности
for (i = 0; i n; i++) {
widget[i].x = x;
widget[i].y = y;
widget[i].ready = true;
}
Поток 2 (процессор 2)
// Если данные готовы,
// обрабатываем их
for (i = 0; i n; i++) {
if (widget[i].ready) {
do_some_stuff(widget[i]);
}
}
Буфер 1 Буфер 2 Буфер N
L2-кэш
... ...
80
86. ˠ˶́˶̄̀˿́̐˵˿̈˹˳˱˾˹˶̀́˹˳̌̀˿˼˾˶˾˹˹˿̀˶́˱̇˹̐˸˱̆˳˱̃˱˿̂˳˿˲˿˷˵˶˾˹̐˳
̀́˿̇˶̂̂˿́˱̆˱́̆˹̃˶˻̃̄́̌32:(5
Поток 1 (процессор 1)
// Подготавливаем данные
// и устанавливаем флаг
// готовности
for (i = 0; i n; i++) {
widget[i].x = x;
widget[i].y = y;
widget[i].ready = true;
}
Поток 2 (процессор 2)
// Если данные готовы,
// обрабатываем их
for (i = 0; i n; i++) {
if (widget[i].ready) {
do_some_stuff(widget[i]);
}
}
Буфер 1 Буфер 2 Буфер N
L2-кэш
... ...
81
87. ˠ˶́˶̄̀˿́̐˵˿̈˹˳˱˾˹˶̀́˹˳̌̀˿˼˾˶˾˹˹˿̀˶́˱̇˹̐˸˱̆˳˱̃˱˿̂˳˿˲˿˷˵˶˾˹̐˳
̀́˿̇˶̂̂˿́˱̆˱́̆˹̃˶˻̃̄́̌32:(5
Поток 1 (процессор 1)
// Подготавливаем данные
// и устанавливаем флаг
// готовности
for (i = 0; i n; i++) {
widget[i].x = x;
widget[i].y = y;
widget[i].ready = true;
}
Поток 2 (процессор 2)
// Если данные готовы,
// обрабатываем их
for (i = 0; i n; i++) {
if (widget[i].ready) {
do_some_stuff(widget[i]);
}
}
Буфер 1 Буфер 2 Буфер N
L2-кэш
... ...
82
88. ˠ˶́˶̄̀˿́̐˵˿̈˹˳˱˾˹˶̀́˹˳̌̀˿˼˾˶˾˹˹˿̀˶́˱̇˹̐˸˱̆˳˱̃˱˿̂˳˿˲˿˷˵˶˾˹̐˳
̀́˿̇˶̂̂˿́˱̆˱́̆˹̃˶˻̃̄́̌32:(5
Поток 1 (процессор 1)
// Подготавливаем данные
// и устанавливаем флаг
// готовности
for (i = 0; i n; i++) {
widget[i].x = x;
widget[i].y = y;
widget[i].ready = true;
}
Поток 2 (процессор 2)
// Если данные готовы,
// обрабатываем их
for (i = 0; i n; i++) {
if (widget[i].ready) {
do_some_stuff(widget[i]);
}
}
... ...
L2-кэш: widget[i].y = y; widget[i].ready = true;
83
89. ˠ˶́˶̄̀˿́̐˵˿̈˹˳˱˾˹˶̀́˹˳̌̀˿˼˾˶˾˹˹˿̀˶́˱̇˹̐˸˱̆˳˱̃˱˿̂˳˿˲˿˷˵˶˾˹̐˳
̀́˿̇˶̂̂˿́˱̆˱́̆˹̃˶˻̃̄́̌32:(5
Поток 1 (процессор 1)
// Подготавливаем данные
// и устанавливаем StoreStore
флаг
// готовности
for (i = 0; i n; i++) {
widget[i].x = x;
widget[i].y = y;
widget[i].ready = true;
}
Поток 2 (процессор 2)
// Если данные готовы,
// обрабатываем их
for (i = 0; i n; i++) {
if (widget[i].ready) {
do_some_stuff(widget[i]);
}
}
... ...
L2-кэш: widget[i].y = y; widget[i].ready = true;
84
91. ˠ́˿˴́˱˽˽˾˿˶̀˶́˶̄̀˿́̐˵˿̈˹˳˱˾˹˶˹˾̂̃́̄˻̇˹˺
Первое правило робототехники компиляторов и
процессоров при упорядочивании доступа к памяти:
Нельзя изменять поведение
однопоточной программы.
▪ В однопоточных программах переупорядочивания
остаются незамеченными.
▪ То же самое - при многопоточном программировании
на основе мьютексов, семафоров и т.д.
▪ Но не при использовании атомарных переменных
и техник программирования без блокировок.
86
92. ˠ́˿˴́˱˽˽˾˿˶̀˶́˶̄̀˿́̐˵˿̈˹˳˱˾˹˶˹˾̂̃́̄˻̇˹˺
int x, y;
int main() {
x = y + 111;
y = 222;
printf(%d%d, x, y);
mov eax, DWORD PTR y[rip]
add eax, 111
mov DWORD PTR x[rip], eax
mov DWORD PTR y[rip], 222
gcc -S -masm=intel prog.c
выполнение команды
x = y + 111 завершено
выполнение команды
y = 222 завершено
87
93. ˠ́˿˴́˱˽˽˾˿˶̀˶́˶̄̀˿́̐˵˿̈˹˳˱˾˹˶˹˾̂̃́̄˻̇˹˺
int x, y;
int main() {
x = y + 111;
y = 222;
printf(%d%d, x, y);
mov eax, DWORD PTR y[rip]
mov edx, 222
...
mov DWORD PTR y[rip], 222
lea esi, [rax+111]
...
mov DWORD PTR x[rip], esi
gcc -S -masm=intel -O2 prog.c
y = 222 выполняется
раньше x = y + 111
выполнение команды
y = 222 завершено
выполнение команды
x = y + 111 завершено
88
96. int data;
bool isReleased = false; // данные опубликованы?
void releaseData(int val) // опубликовать данные
{
data = val; // записать данные
isReleased = true; // данные опубликованы,
} // можно с ними работать
90
98. Поток 1
int data;
bool isReleased = false;
void releaseData(int val)
{
data = val;
isReleased = true;
}
Поток 2
void utilizeData()
{
while (!isReleased);
doSomething(data);
}
Данные должны быть
проинициализированы в 1 потоке
перед тем, как они будут
использованы во 2 потоке.
91
106. Поток 1
int data;
bool isReleased = false;
void releaseData(int val)
{
data = val;
isReleased = true;
}
Поток 2
void utilizeData()
{
while (!isReleased);
doSomething(data);
}
Из-за переупорядочивания
инструкций компилятором флаг
выставляется до того, как
данные готовы.
95
108. Поток 1
StoreStore
int data;
bool isReleased = false;
void releaseData(int val)
{
data = val;
isReleased = true;
}
Поток 2
void utilizeData()
{
while (!isReleased);
doSomething(data);
}
Из-за переупорядочивания
инструкций компилятором флаг
выставляется до того, как
данные готовы.
96
110. Поток 1
int data;
bool isReleased = false;
void releaseData(int val)
{
data = val;
isReleased = true;
}
Поток 2
void utilizeData()
{
while (!isReleased);
doSomething(data);
}
Из-за переупорядочивания
инструкций компилятором флаг
выставляется до того, как
данные готовы.
97
118. if (x 0)
y++;
register int r = y;
if (x 0)
r++;
y = r;
Размещение переменной в регистре
в процессе оптимизирующей
компиляции
создание новой регистровой
переменной
Новая операция сохранения
(store) “из чистого воздуха”
Создание операций сохранения “из чистого
воздуха” не допустимо в соответствии с
последним стандартом, однако... 101
120. однако в С PThreads такая оптимизация допускается:
static pthread_mutex_t lock = PTHREAD_MUTEX_INITALIZER;
static int count = 0;
int trylock() {
int rc;
rc = pthread_mutex_trylock(mutex);
if (rc == 0)
count++;
...
int trylock() {
register int r = count;
int rc;
rc = pthread_mutex_trylock(mutex);
if (rc == 0)
count++;
count = r;
новый store
и новая гонка данных! 102
124. Отношение happens-before определяет, какие операции видят
последствия других операций.
Допустим A и В - операции в многопоточной программе. Тогда если
А происходит-раньше В, тогда эффект (результат операции,
который отразился в памяти) операции А становится видим для
потока, выполняющего операцию В.
x = 10; // A
y = x + 1; // B
104
126. Отношение happens-before определяет, какие операции видят
последствия других операций.
Допустим A и В - операции в многопоточной программе. Тогда если
А происходит-раньше В, тогда эффект (результат операции,
который отразился в памяти) операции А становится видим для
потока, выполняющего операцию В.
x = 10;
y = x + 1;
z = sqrt(x * y);
Все соотношения
happens-before для
операции А (x = 10)
105
128. Отношение happens-before определяет, какие операции видят
последствия других операций.
Допустим A и В - операции в многопоточной программе. Тогда если
А происходит-раньше В, тогда эффект (результат операции,
который отразился в памяти) операции А становится видим для
потока, выполняющего операцию В.
x = 10;
y = x + 1;
z = sqrt(x * y);
m = k - 5;
print(m)
print(x)
106
130. Отношение happens-before определяет, какие операции видят
последствия других операций.
Допустим A и В - операции в многопоточной программе. Тогда если
А происходит-раньше В, тогда эффект (результат операции,
который отразился в памяти) операции А становится видим для
потока, выполняющего операцию В.
Поток 1 Поток 2
x = 10;
y = x + 1;
z = sqrt(x * y);
m = k - 5;
print(m)
w = x + 2
107
131. ˠ́˿˹̂̆˿˵˹̃́˱˾̍̉˶Ƨ̀́˿˹̂̆˿˵˹̃́˱˾̍̉˶
Отношение happens-before определяет, какие операции видят
последствия других операций.
Допустим A и В - операции в многопоточной программе. Тогда если
А происходит-раньше В, тогда эффект (результат операции,
который отразился в памяти) операции А становится видим для
потока, выполняющего операцию В.
1. Из того, что А происходит-раньше В не следует, что А
происходит раньше В.
2. Из того, что А происходит раньше В не следует, что А
происходит-раньше В
108
132. ˠ́˿˹̂̆˿˵˹̃́˱˾̍̉˶˾˶˿˸˾˱̈˱˶̃̀́˿˹̂̆˿˵˹̃́˱˾̍̉˶
1. Из того, что А происходит-раньше В не следует, что А
происходит раньше В.
int x, y;
int main() {
x = y + 111; // A
y = 222; // B
printf(%d%d, x, y);
109
133. ˠ́˿˹̂̆˿˵˹̃́˱˾̍̉˶˾˶˿˸˾˱̈˱˶̃̀́˿˹̂̆˿˵˹̃́˱˾̍̉˶
1. Из того, что А происходит-раньше В не следует, что А
int x, y;
int main() {
x = y + 111; // A
y = 222; // B
printf(%d%d, x, y);
mov eax, DWORD PTR y[rip]
mov edx, 222
...
mov DWORD PTR y[rip], 222
lea esi, [rax+111]
...
mov DWORD PTR x[rip], esi
gcc -S -masm=intel -O2 prog.c
происходит раньше В.
110
134. ˠ́˿˹̂̆˿˵˹̃́˱˾̍̉˶˾˶˿˸˾˱̈˱˶̃̀́˿˹̂̆˿˵˹̃́˱˾̍̉˶
1. Из того, что А происходит-раньше В не следует, что А
int x, y;
int main() {
x = y + 111;
y = 222;
printf(%d%d, x, y);
mov eax, DWORD PTR y[rip]
mov edx, 222
...
mov DWORD PTR y[rip], 222
lea esi, [rax+111]
...
mov DWORD PTR x[rip], esi
gcc -S -masm=intel -O2 prog.c
y = 222 выполняется
раньше x = y + 111
выполнение команды
y = 222 завершено
выполнение команды
x = y + 111 завершено
происходит раньше В.
111
135. ˠ́˿˹̂̆˿˵˹̃́˱˾̍̉˶˾˶˿˸˾˱̈˱˶̃̀́˿˹̂̆˿˵˹̃́˱˾̍̉˶
2. Из того, что А происходит раньше В не следует, что А
происходит-раньше В
Поток 2
if (ready) // B
print(x);
int x;
bool ready = false;
Поток 1
x = 42
ready = true; // A
112
136. ˠ́˿˹̂̆˿˵˹̃́˱˾̍̉˶˾˶˿˸˾˱̈˱˶̃̀́˿˹̂̆˿˵˹̃́˱˾̍̉˶
2. Из того, что А происходит раньше В не следует, что А
происходит-раньше В
Поток 2
if (ready) // B
print(x);
int x;
bool ready = false;
Поток 1
x = 42
ready = true; // A
113
137. ˠ́˿˹̂̆˿˵˹̃́˱˾̍̉˶˾˶˿˸˾˱̈˱˶̃̀́˿˹̂̆˿˵˹̃́˱˾̍̉˶
2. Из того, что А происходит раньше В не следует, что А
происходит-раньше В
Поток 2
if (ready)
print(x);
int x;
bool ready = false;
Поток 1
x = 42
ready = true;
114
138. ˠ́˿˹̂̆˿˵˹̃́˱˾̍̉˶˾˶˿˸˾˱̈˱˶̃̀́˿˹̂̆˿˵˹̃́˱˾̍̉˶
2. Из того, что А происходит раньше В не следует, что А
происходит-раньше В
Поток 2
if (ready)
print(x);
int x;
bool ready = false;
Поток 1
x = 42
ready = true;
115
139. ˠ́˿˹̂̆˿˵˹̃́˱˾̍̉˶Ƨ̀́˿˹̂̆˿˵˹̃́˱˾̍̉˶
Отношение happens-before определяет, какие операции видят
последствия других операций.
Допустим A и В - операции в многопоточной программе. Тогда если
А происходит-раньше В, тогда эффект (результат операции,
который отразился в памяти) операции А становится видим для
потока, выполняющего операцию В.
1. Из того, что А происходит-раньше В не следует, что А
происходит раньше В.
2. Из того, что А происходит раньше В не следует, что А
происходит-раньше В
Отношение происходит-раньше имеет место тогда (и только
тогда), когда это определёно стандартом языка.
116
166. ˝˶̃˱̅˿́˱́˶̀˿˸˹̃˿́˹̐
1
2
Рекс
Мухтар
Утечка данных из
центрального
репозитория в
локальный и обратно
Центральный
репозиторий
137
167. ˝˶̃˱̅˿́˱́˶̀˿˸˹̃˿́˹̐
1
2
Рекс
Мухтар
Утечка данных из
центрального
репозитория в
локальный и обратно
Центральный
репозиторий
mov [x], 1
x = 1
x = 0
x = 0
138
168. ˝˶̃˱̅˿́˱́˶̀˿˸˹̃˿́˹̐
1
2
Рекс
Мухтар
Утечка данных из
центрального
репозитория в
локальный и обратно
Центральный
репозиторий
mov [x], 1
x = 1
x = 1
x = 0
139
169. ˝˶̃˱̅˿́˱́˶̀˿˸˹̃˿́˹̐
1
2
Рекс
Мухтар
Утечка данных из
центрального
репозитория в
локальный и обратно
Центральный
репозиторий
mov [x], 1
x = 1
x = 1
x = 1
140
170. ˝˶̃˱̅˿́˱́˶̀˿˸˹̃˿́˹̐
1
2
Рекс
Мухтар
Центральный
репозиторий
mov [x], 1
x = 1
x = ?
x = ?
Неизвестно, когда
изменения распространятся
на другие потоки.
141
171. ˝˶̃˱̅˿́˱́˶̀˿˸˹̃˿́˹̐
1
2
x = 0
y = 0
Рекс
Мухтар
Центральный
репозиторий
mov [x], 1
mov r1, [y]
mov [y], 1
mov r2, [x]
x = 0
y = 0
x = 0
y = 0
142
172. ˝˶̃˱̅˿́˱́˶̀˿˸˹̃˿́˹̐
1
2
x = 1
y = 0
Рекс
Мухтар
Центральный
репозиторий
mov [x], 1
mov r1, [y]
mov [y], 1
mov r2, [x]
x = 0
y = 1
x = 0
y = 0
143
173. ˝˶̃˱̅˿́˱́˶̀˿˸˹̃˿́˹̐
1
2
x = 1
y = ? {0,1}
Рекс
Мухтар
Центральный
репозиторий
mov [x], 1
mov r1, [y]
mov [y], 1
mov r2, [x]
x = ? {0,1}
y = 1
x = ? {0,1}
y = ? {0,1}
144
175. ˒˱́̍˶́/RDG/RDG
1
Рекс
Центральный
репозиторий
LoadLoad
x = 1
y = 0
x = 1
y = 0
git pull, hg pull,
svn update,
cvs update
Барьер LoadLoad:
▪ Предотвращает переупорядочивания между
загрузками до барьера и загрузками после
барьера.
▪ Гарантирует, что загруженные из центрального
репозитория (памяти) в локальный репозиторий
(кэш) значения будут по крайней мере такие же
новые, как и последнее значение, которое
“просочилось” из центрального репозитория. 146
176. ˒˱́̍˶́/RDG/RDG
1
Рекс
Центральный
репозиторий
LoadLoad
x = 1
y = 0
x = 1
y = 0
git pull, hg pull,
svn update,
cvs update
// Так получилось, что
// widget.ready = true утекло
// в локальный репозиторий
if (widget.ready) {
// Подгрузить всё остальное
// содержимое widget -
// такое же “свежее”, как ready
LoadLoadFence();
do_something(widget);
} 147
177. ˒˱́̍˶́6WRUH6WRUH
1
Рекс
Центральный
репозиторий
StoreStore
x = 1
y = 0
Барьер StoreStore:
▪ Предотвращает переупорядочивания между
сохранениями до барьера и сохранениями после
барьера.
▪ Гарантирует, что загруженные из локального
репозитория (кэш) в локальный репозиторий
(память) значения будут по крайней мере такие
же новые, как и последнее значение, которое
“просочилось” из локального репозитория.
x = 1
y = 0
git push, hg push,
svn commit, cvs
commit
148
178. ˒˱́̍˶́6WRUH6WRUH
1
Рекс
Центральный
репозиторий
StoreStore
x = 1
y = 0
x = 1
y = 0
git push, hg push,
svn commit, cvs
commit
widget.x = x;
widget.y = y;
StoreStoreFence();
widget.ready = true;
// Если ready = true просочится
// Мухтару, то он увидит увидит
// на центральном репозитории
// всё остальные поля widget,
// которые для него подготовил Рекс 149
179. ˒˱́̍˶́/RDG6WRUH
1
mov r1, [y]
mov r2, [x]
mov [z], 42
mov [w], r3
mov [v], r4
Рекс
Операции загрузки
load
Операции сохранения
store
Переупорядочивание LoadStore
1. Есть набор инструкций, состоящий из операций
сохранения и загрузки.
150
180. ˒˱́̍˶́/RDG6WRUH
1
mov r1, [y]
mov r2, [x]
mov [z], 42
mov [w], r3
mov [v], r4
Рекс
Операции загрузки
load - отложены
Операции сохранения
store - выполнены
Переупорядочивание LoadStore
1. Есть набор инструкций, состоящий из операций
сохранения и загрузки.
2. Если Рекс встречает операцию загрузки, то он
просматривает следующие операции сохранения,
и если они абсолютно не связаны с текущей
операцией загрузки, то он откладывает
выполнение операции загрузки и в первую
очередь выполняет операции сохранения. 151
181. ˒˱́̍˶́/RDG6WRUH
1
mov r1, [y]
mov r2, [x]
mov [z], 42
mov [w], r3
mov [v], r4
Рекс
Будут промахи по
кэшу...
Будут попадания в
кэш...
Переупорядочивание LoadStore
1. Есть набор инструкций, состоящий из операций
сохранения и загрузки.
2. Если Рекс встречает операцию загрузки, которые
промахиваются по кэшу, то он просматривает следующие
операции сохранения, которые попадают в кэш, и если
они абсолютно не связаны с текущей операцией загрузки,
то он откладывает выполнение операции загрузки и в
первую очередь выполняет операции сохранения. 152
182. ˒˱́̍˶́/RDG6WRUH
1
mov r1, [y]
mov r2, [x]
LoadStore
mov [z], 42
mov [w], r3
mov [v], r4
Рекс
Будут промахи по
кэшу...
Будут попадания в
кэш...
Переупорядочивание LoadStore
1. Есть набор инструкций, состоящий из операций
сохранения и загрузки.
2. Если Рекс встречает операцию загрузки, которые
промахиваются по кэшу, то он просматривает следующие
операции сохранения, которые попадают в кэш, и если
они абсолютно не связаны с текущей операцией загрузки,
то он откладывает выполнение операции загрузки и в
первую очередь выполняет операции сохранения. 153
183. ˒˱́̍˶́6WRUH/RDG
1
mov [x], 1
mov r1, [y]
x = 1
y = 0
Рекс
2
mov [y], 1
mov r2, [x]
x = 0
y = 1
Мухтар
r1 = 0
r2 = 0
154
184. ˒˱́̍˶́6WRUH/RDG
1
mov [x], 1
StoreLoad
mov r1, [y]
x = 1
y = ?
Рекс
2
mov [y], 1
StoreLoad
mov r2, [x]
x = ?
y = 1
Мухтар
Барьер StoreLoad:
▪ Гарантирует видимость для других
процессоров всех операций
сохранения, выполненных до
барьера.
▪ Обеспечивает для всех операций
загрузки, выполненных после
барьера, получение результатов,
которые имеют место во время
барьера.
▪ Барьер предотвращает r1 = r2 = 0
▪ StoreLoad ≠ StoreStore +
LoadLoad
155
185. ˒˱́̍˶́6WRUH/RDG
1
mov [x], 1
StoreLoad
mov r1, [y]
x = 1
y = ?
Рекс
Центральный
репозиторий
Барьер StoreLoad (≠ StoreStore + LoadLoad):
1. Отправка (push) всех изменений в центральный репозиторий.
156
186. ˒˱́̍˶́6WRUH/RDG
1
mov [x], 1
StoreLoad
mov r1, [y]
x = 1
y = ?
Рекс
Центральный
репозиторий
Барьер StoreLoad (≠ StoreStore + LoadLoad):
1. Отправка (push) всех изменений в центральный репозиторий.
2. Ожидание завершения выполнения операции отправки (в
отличие от StoreStore, который может выполняться с задержкой).
157
187. ˒˱́̍˶́6WRUH/RDG
1
mov [x], 1
StoreLoad
mov r1, [y]
x = 1
y = ?
Рекс
Центральный
репозиторий
Барьер StoreLoad (≠ StoreStore + LoadLoad):
1. Отправка (push) всех изменений в центральный репозиторий.
2. Ожидание завершения выполнения операции отправки (в
отличие от StoreStore, который может выполняться с задержкой).
3. Загрузка (pull) всех последних изменений из центрального
репозитория (в отличие от LoadLoad, который не загружает
абсолютно последние изменения)
158
190. ˢ˶˽˱˾̃˹˻˱˸˱̆˳˱̃˱˹˿̂˳˿˲˿˷˵˶˾˹̐
Семантика захвата (acquire)
▪ Применяется к операциям чтения или чтения-
модификации-записи, при этом такая операция
становится операцией чтения-захвата (read-acquire).
▪ Предотвращает переупорядочивание инструкции чтения-
захвата и всех следующих в программе операций чтения
или записи.
▪ Применяется к операциям записи или чтения-
модификации-записи, причём такая операция становится
операцией записи-освобождения (write-release).
▪ Предотвращает переупорядочивание инструкции записи-
освобождения со всеми предшествующими в программе
операциями чтения или записи.
Семантика освобождения (release) 160
201. ˢ˶˽˱˾̃˹˻˱˸˱̆˳˱̃˿̂˳˿˲˿˷˵˶˾˹˶˾˱̀́˹˽˶́˶̂̀̄˲˼˹˻˱̇˹˶˺
Поток 1
x = 42
ready.store(true,
memory_order_release);
Synchronizes-with
Поток 2
ready.load(
memory_order_acquire);
print(x);
int x;
bool ready = false;
Синхронизируется-с
Всё, что
здесь...
… будет
видно здесь
171
202. ˢ˶˽˱˾̃˹˻˱˸˱̆˳˱̃˿̂˳˿˲˿˷˵˶˾˹˶˾˱̀́˹˽˶́˶̂̀̄˲˼˹˻˱̇˹˶˺
Поток 1
x = 42
ready.store(true,
memory_order_release);
Synchronizes-with
Поток 2
ready.load(
memory_order_acquire);
print(x);
int x;
bool ready = false;
Синхронизируется-с
… будет
видно здесь
Всё, что
здесь...
Операция записи-
освобождения
Операция чтения-
захвата
172
204. ˢ˶˽˱˾̃˹˻˱˸˱̆˳˱̃˿̂˳˿˲˿˷˵˶˾˹˶˾˱̀́˹˽˶́˶̂̀̄˲˼˹˻˱̇˹˶˺
Synchronizes-with
Поток 2
void useWidget() {
while (!ready.load(
memory_order_acquire))
{}
doSomething(w);
}
widget w;
bool ready = false;
Поток 1
void initWidget(x, y, z) {
w.x = x;
w.y = x;
w.z = z;
ready.store(true,
memory_order_release);
}
Синхронизируется-с
Всё, что
здесь...
… будет
видно
здесь
Операция записи-
освобождения
Операция
чтения-захвата
174
205. ˢ˶˽˱˾̃˹˻˱˸˱̆˳˱̃˿̂˳˿˲˿˷˵˶˾˹˶˾˱̀́˹˽˶́˶̂̀̄˲˼˹˻˱̇˹˶˺
Synchronizes-with
Поток 2
void useWidget() {
while (!ready.load(
memory_order_acquire))
{}
s1 = to_string(w);
s2 = number + s1;
print(s2);
}
int x, y, z;
bool ready = false;
Поток 1
void initWidget(x, y, z) {
x = 10
y = x + 20;
z = x + 12;
ready.store(true,
memory_order_release);
}
Синхронизируется-с
175
206. ˢ˶˽˱˾̃˹˻˱˸˱̆˳˱̃˿̂˳˿˲˿˷˵˶˾˹˶˾˱̀́˹˽˶́˶̂̀̄˲˼˹˻˱̇˹˶˺
Synchronizes-with
Поток 2
void useWidget() {
while (!ready.load(
memory_order_acquire))
{}
s1 = to_string(w);
s2 = number + s1;
print(s2);
}
int x, y, z;
bool ready = false;
Поток 1
void initWidget(x, y, z) {
x = 10
y = x + 20;
z = x + 12;
ready.store(true,
memory_order_release);
}
Синхронизируется-с
Операция записи-
освобождения
Операция
чтения-захвата
176