이번에는 memory_order_consume에 대해 정리해보려 해.
consume operation은 acquire operation의 부분 집합이라고 보면 되는데, 따라서 모든 memory_order_consume은 memory_order_acquire로 바꿔도 맞게 동작해. 이 부분은 뒤에 필요한 내용이니까 알아두자.
value에 값을 쓰고 ready에 표시하는 이전 예제를 다시 살펴보자.
int value = 0;
std::atomic<bool> ready(false);
void writer() {
value = 3;
ready.store(true, std::memory_order_release);
}
void reader() {
while(!ready.load(std::memory_order_acquire)); // ready가 true가 될 때까지 기다림
assert(value == 3); // It never fires
}
이 때 ready를 bool로 안쓰고 value를 가리키는 포인터로 사용하면 다음과 같아.
int value = 0;
std::atomic<int*> ptr(nullptr);
void writer() {
value = 3;
ptr.store(&value, std::memory_order_release);
}
void reader() {
int* p;
while(!(p = ptr.load(std::memory_order_acquire)));
assert(*p == 3); // it never fires
}
그런데 만약에 다음과 같이 reader()의 acquire를 relaxed로 바꾸면 어떻게 될까?
void reader() {
int* p;
while(!(p = ptr.load(std::memory_order_relaxed)));
assert(*p == 3); // it may or may not fire
}
예상과는 다르게, 대부분의 아키텍처에서 저 assert문은 fire되지 않아. 이건 *p가 p에 dependent 하기 때문인데, 대부분의 아키텍처에서 dependent loads는 리오더되지 않기 때문이야.
여기서 대부분의 아키텍처라고 했는데 DEC alpha같은 숭악한 놈은 dependent load도 리오더할 수 있어. (https://en.wikipedia.org/wiki/Memory_ordering#In_symmetric_multiprocessing_.28SMP.29_microprocessor_systems)
대부분의 아키텍처에서 수행하지 않는 dependent loads 리오더를 막기 위해서 비용이 비싼 메모리 배리어를 무조건 사용하도록 하는건 너무 비효율적이잖아? 그래서 이런 경우에 memory_order_consume이 사용돼.
consume을 사용한 다음 코드를 보자.
void reader() {
int *p;
while(!(p = ptr.load(std::memory_order_consume)));
assert(*p == 3); // it never fires
}
consume의 경우 acquire와는 다르게 ptr을 load할 때 메모리 배리어를 사용하지 않고 ptr에 dependent한 operation을 사용할 때 필요한 경우 메모리 배리어를 사용하도록 되어 있어.
arm-gcc를 통해 확인해보자. arm은 dependent loads를 리오더하지 않으니까 메모리 배리어를 사용하지 않는 코드를 생성하겠지?
$ cat a.cpp
#include <atomic>
int value = 0;
std::atomic<int*> ptr(nullptr);
int reader()
{
int* p;
while(!(p = ptr.load(std::memory_order_consume)));
return *p;
}
어라 메모리 배리어가 사용되네? 분명 arm에서는 dependent loads를 리오더하지 않기 때문에 메모리 배리어가 필요가 없는데?
이건 컴파일러 구현의 문제에 따른 건데, 위에서 dependent load가 발생할 때 필요하면 메모리 배리어를 사용한다고 했잖아? 이를 위해서는 컴파일러가 dependency chain을 모두 추적해야하는데 이게 구현이 꽤 어려워. 그래서 대부분의 컴파일러가 consume을 그냥 acquire로 취급해. 앞에서 말한것처럼 consume이 acquire의 subset이니까 consume을 acquire로 바꾼다 하더라도 동작은 같아지거든.
http://en.cppreference.com/w/cpp/atomic/memory_order 에 따르면, 2015년 2월 현재 디펜던시 체인을 추적하는 컴파일러가 없다고 해. 'Note that currently (2/2015) no known production compilers track dependency chains: consume operations are lifted to acquire operations.'
'그럼 어떻게 해야하는가?'가 어려운 문제인데, 내가 예전에도 C/C++ 표준 코드에 대해 썼던 글에서처럼 선택을 해야해. 현재는 비효율적이지만 표준대로 consume을 사용하고 언젠가 컴파일러가 consume을 제대로 처리하기를 기다리거나, 아니면, 내가 사용하는 아키텍처가 dependent loads를 리오더 하지 않는다고 가정한 채 relaxed를 사용하고 주석으로 설명해놓는다던가.
메모리 베리어 인스트럭션의 비용조차 아까울 정도의 상황이 아니라면(아마도 대부분의 경우가 그렇겠지) consume을 그냥 사용하길 추천해.
그냥 쓰자. consume이 펜스에만 관련이 있는 줄 알았는데 컴파일러 최적화에도 영향을 준다 그러네. 딱히 예제는 못찾겠지만 원래 의미대로 쓰는게 젤 낫겠다.
DCLP에서 consume을 사용하는 내용을 쓰려 했는데 오늘 좀 지치네. 이 부분은 다음에 계속해서 정리해볼게.
굿밤~
귀찮다고 생각없이 썼다가 예제코드에 븅신같은 실수들이 가득해서 수정.
모으고 있읍니다