Download - Модель памяти C++ - Андрей Янковский, Яндекс
Phil Karlton
There are only two hard things in Computer Science: cache invalidation, naming things and off-by-one errors.
<thread>
<mutex>
<conditional_variable>
<future>
<atomic>
4
Многопоточность в C++11
Ура!
Теперь мы можем писать кросс-платформенный многопоточный код?
6
Ожидание
7
Реальность
Что пошло не так? Кто виноват?
Процесс
Поток
Разделяемая память
Атомарность операций
9
Компилятор С++ слишком умный
Singleton* Singleton::Get() { if (instance == nullptr) { std::lock_guard<std::mutex> guard(lock); if (instance == nullptr) { instance = new Singleton(); ! } } return instance; }
10
Пример #1
Singleton* Singleton::Get() { if (instance == nullptr) { std::lock_guard<std::mutex> guard(lock); if (instance == nullptr) { instance = operator new(sizeof(Singleton)); // 1 new (instance) Singleton; // 2 } } return instance; }
11
Пример #1 (продолжение)
Memory reordering
13
Как работает процессор
CPU
Memory
14
Как работает процессор
CPU
Memory
Cache
15
Как работает процессор
CPU
Memory
Cache
CPU
Cache
int data; volatile bool ready = false; void foo() { // CPU 0 data = 42; ready = true; } void bar() { // CPU 1 if (ready) { assert(data == 42); } }
16
Пример #2
int data; volatile bool ready = false; void foo() { // CPU 0 ready = true; data = 42; } void bar() { // CPU 1 if (ready) { assert(data == 42); } }
17
Пример #2
CPU
int data; volatile bool ready = false; void foo() { // CPU 0 data = 42; ready = true; } void bar() { // CPU 1 int tmp = data; if (ready) { assert(tmp == 42); } }
18
Пример #2
int data; volatile bool ready = false; void foo() { // CPU 0 data = 42; ____________ ready = true; } void bar() { // CPU 1 if (ready) { __________ assert(data == 42); } }
19
Пример #2
int data; volatile bool ready = false; void foo() { // CPU 0 data = 42; STORE_______ ready = true; } void bar() { // CPU 1 if (ready) { __________ assert(data == 42); } }
20
Пример #2
int data; volatile bool ready = false; void foo() { // CPU 0 ready = true; data = 42; STORE_______ ready = true; } void bar() { // CPU 1 if (ready) { __________ assert(data == 42); } }
21
Пример #2
int data; volatile bool ready = false; void foo() { // CPU 0 data = 42; STORE__STORE ready = true; } void bar() { // CPU 1 if (ready) { __________ assert(data == 42); } }
22
Пример #2
int data; volatile bool ready = false; void foo() { // CPU 0 data = 42; STORE__STORE ready = true; } void bar() { // CPU 1 if (ready) { LOAD______ assert(data == 42); } }
23
Пример #2
int data; volatile bool ready = false; void foo() { // CPU 0 data = 42; STORE__STORE ready = true; } void bar() { // CPU 1 if (ready) { LOAD__LOAD assert(data == 42); } }
24
Пример #2
XX_YY — гарантирует, что все XX-операции до барьера будут выполнены до того, как начнут выполняться YY-операции после барьера.
25
Барьер памяти
XX_YY — гарантирует, что все XX-операции до барьера будут выполнены до того, как начнут выполняться YY-операции после барьера.
26
Барьер памяти
LoadLoad LoadStore
StoreLoad StoreStore
XX_YY — гарантирует, что все XX-операции до барьера будут выполнены до того, как начнут выполняться YY-операции после барьера.
27
Барьер памяти
LoadLoad LoadStore
StoreLoad StoreStore
Acquire
Release
Acquire — гарантирует, что любые операции после барьера будут выполнены после того, как будут выполнены все Load-операции до барьера.
28
Acquire
Acquire — гарантирует, что любые операции после барьера будут выполнены после того, как будут выполнены все Load-операции до барьера.
29
Acquire
mov [x], 1 mov eax, [y] acquire_fence mov ebx, [w] mov ecx, [e] mov [q], 3
Acquire — гарантирует, что любые операции после барьера будут выполнены после того, как будут выполнены все Load-операции до барьера.
30
Acquire
mov [x], 1 mov ebx, [w] <— mov eax, [y] acquire_fence mov ebx, [w] mov ecx, [e] mov [q], 3
Acquire — гарантирует, что любые операции после барьера будут выполнены после того, как будут выполнены все Load-операции до барьера.
31
Acquire
mov [x], 1 mov eax, [y] acquire_fence mov [x], 1 <— mov ebx, [w] mov ecx, [e] mov [q], 3
Release — гарантирует, что любые операции до барьера будут выполнены до того, как начнут выполняться Store-операции после барьера.
32
Release
Release — гарантирует, что любые операции до барьера будут выполнены до того, как начнут выполняться Store-операции после барьера.
33
Release
mov [x], 1 mov eax, [y] mov [z], 2 release_fence mov [w], 3 mov ebx, [k]
Release — гарантирует, что любые операции до барьера будут выполнены до того, как начнут выполняться Store-операции после барьера.
34
Release
mov [x], 1 mov eax, [y] mov [z], 2 release_fence mov [w], 3 mov [z], 2 <— mov ebx, [k]
Release — гарантирует, что любые операции до барьера будут выполнены до того, как начнут выполняться Store-операции после барьера.
35
Release
mov [x], 1 mov eax, [y] mov [z], 2 mov ebx, [k] <— release_fence mov [w], 3 mov ebx, [k]
void function_with_lock() { ... if (can_enter) {
acquire_fence(); // LoadLoad + LoadStore // all instructions // stay between // these fences release_fence(); // StoreStore + LoadStore can_enter = true; } ... }
36
Что такое acquire/release?
bool x = true; bool y = true; void foo() { // CPU 0 x = false; assert(y); } void bar() { // CPU 1 y = false; assert(x); }
37
Пример #3
bool x = true; bool y = true; void foo() { // CPU 0 assert(y); x = false; } void bar() { // CPU 1 y = false; assert(x); }
38
Пример #3
bool x = true; bool y = true; void foo() { // CPU 0 x = false; ___________ assert(y); } void bar() { // CPU 1 y = false; ___________ assert(x); }
39
Пример #3
bool x = true; bool y = true; void foo() { // CPU 0 x = false; Store______ assert(y); } void bar() { // CPU 1 y = false; Store______ assert(x); }
40
Пример #3
bool x = true; bool y = true; void foo() { // CPU 0 x = false; Store__Load assert(y); } void bar() { // CPU 1 y = false; Store__Load assert(x); }
41
Пример #3
bool x = true; bool y = true; void foo() { // CPU 0 x = false; Store__Load assert(y); } void bar() { // CPU 1 y = false; Store__Load assert(x); }
42
Пример #3
Это самый “тяжелый” из барьеров
StoreLoad означает полную синхронизацию всех кэшей процессоров.
Ситуации, где он действительно нужен, довольно редки
43
Почему StoreLoad стоит отдельно?
Memory models
Sequential consistency
Strongly-ordered
Weakly-ordered (data dependency ordering)
Super-weak
45
Модель памяти
Никаких memory reordering
Гарантируется, что каждый процессор видит все изменения в системе в том же порядке, что и остальные процессоры.
46
Sequential consistency
Каждая операция имеет acquire и release семантику
Все процессоры видят последовательности чтений и записей в одном порядке, но перестановки StoreLoad уже возможны !!
Пример: x86/64
47
Strongly-ordered
Гарантируется лишь упорядоченность для операций, зависимых по данным
int* ptr = GlobalPointer; if (ptr) int value = *ptr; !!
Примеры: ARMv7, PowerPC, Itanium
48
Weakly-ordered
Никаких гарантий: процессор может делать, что угодно
Примеры: Alpha
49
Super-weak (no guaranties)
enum memory_order { memory_order_relaxed, memory_order_consume, memory_order_acquire, memory_order_release, memory_order_acq_rel, memory_order_seq_cst };
50
#include <atomic>
#include <atomic> !!!T load(memory_order = memory_order_seq_cst) const; void store(T desired, memory_order = memory_order_seq_cst); void atomic_thread_fence(memory_order order);
51
Барьеры памяти С++
std::atomic<bool> ready; !void foo() { // CPU 0 data = 42; ready.store(true); } void bar() { // CPU 1 if (ready.load()) { assert(data == 42); } }
52
Барьеры памяти С++
std::atomic<bool> ready; !void foo() { // CPU 0 data = 42; ready.store(true, std::memory_order_release); } void bar() { // CPU 1 if (ready.load(std::memory_order_acquire)) { assert(data == 42); } }
53
Барьеры памяти С++
std::atomic<bool> ready; void foo() { // CPU 0 data = 42; std::atomic_thread_fence(std::memory_order_release); ready.store(true, std::memory_order_relaxed); } void bar() { // CPU 1 if (ready.load(true, std::memory_order_relaxed)) { std::atomic_thread_fence(std::memory_order_acquire); assert(data == 42); } }
54
Барьеры памяти С++
std::atomic<Foo*> p; Foo payload; int value = 42; void foo() { // CPU 0 payload.name = "Andy"; value = 314; p.store(&payload, std::memory_order_release); } void bar() { // CPU 1 if (auto f = p.load(std::memory_order_consume)) { std::cout << f->name << std::endl; // Andy assert(value == 314); // can fire (but probably won't) } }
55
Зачем std::memory_order_consume?
std::atomic<Foo*> p; Foo payload; int value = 42; void foo() { // CPU 0 payload.name = "Andy"; value = 314; p.store(&payload, std::memory_order_release); } void bar() { // CPU 1 int tmp = value; if (auto f = p.load(std::memory_order_consume)) { std::cout << f->name << std::endl; // Andy assert(tmp == 314); } }
56
Зачем std::memory_order_consume?
Компиляторы не поддерживают это поведение (поэтому на weak-процессорах все равно скорее всего будет барьер)
x86/64 и без этого прекрасно работает
57
Почему probably won’t?
Зачем мне все это знать? Я не пишу lock-free код!
Singleton* Singleton::Get() { if (instance.load() == nullptr) { std::lock_guard<std::mutex> guard(lock); if (instance.load() == nullptr) { instance.store(new Singleton()); } } return instance.load(); }
59
Пример #1 (almost fixed)
Singleton* Singleton::Get() { auto tmp = instance.load(std::memory_order_relaxed); std::atomic_thread_fence(std::memory_order_acquire); if (tmp == nullptr) { std::lock_guard<std::mutex> guard(lock); tmp = instance.load(std::memory_order_relaxed); if (tmp == nullptr) { tmp = new Singleton(); std::atomic_thread_fence(std::memory_order_release); instance.store(tmp, std::memory_order_relaxed); } } return tmp; }
60
Пример #1 (fixed)
Все это проявляется только в многопроцессорной/многоядерной среде
И только в lock-free коде
61
Но как мы жили раньше?
Спасибо за внимание, пишите хороший код :)