이번에는 consume에 대해서 좀 더 정리하고 DCLP에 적용하는걸 정리하려고 해. 아까 낮에 쓰던게 날아가서 좀 멘붕이라 이번에는 그냥 인터넷에서 긁어온 예제를 사용해야겠어.
전 글에서 consume하면 디펜던시를 물고 간다 그랬는데, 이를 명시하는 [[carries_dependency]] attribute를 살펴보자.
다음은 http://stackoverflow.com/a/6411703 에서 가져온 예제야.
void print(int* ptr);
void print2(int* [[carries_dependency]] ptr);
int main()
{
std::atomic<int*> p;
int* local=p.load(std::memory_order_consume);
if(local) {
// compiler may issue a memory acquire fence here
std::cout << *local << std::endl; // 1
}
if(local) {
// compiler may issue a memory acquire fence here
print(local); // 2
}
if(local) {
print2(local); // 3
}
}
void print(int* ptr) {
std::cout << *ptr <<std::endl;
}
void print2(int* [[carries_dependency]] ptr) {
// compiler may issue a memory acquire fence here
std::cout << *ptr << std::endl;
}
일단 1에서는 local이 consume한 포인터인걸 아니까 필요한 경우 펜스가 사용되겠지? 여기서 '필요한 경우'라는 말의 의미는, dependent loads를 리오더링하지 않는 환경이면 메모리 펜스가 필요하지 않을테고, DEC alpha처럼 dependent loads조차 리오더링하는 경우에는 memory acquire fence가 필요할테니 그 때 메모리 펜스가 사용될 거라는 의미야.
여기서 봐야할 건 2와 3의 차이인데, print는 그냥 포인터를 넘겨받잖아? print 내부에서 포인터를 dereference할지 안할지 모르니까, 2에서처럼 consume한 포인터를 넘겨주기 전에 필요한 경우 메모리 펜스를 사용해야겠지.
print2의 인자에는 [[carries_dependency]]라는 attribute가 사용이 되었는데, 이는 인자로 넘겨받는 포인터 ptr이 consume된 포인터라는걸 의미해. 그럼 print2 내부에서 필요한 경우 메모리 펜스를 사용할테니까 3에서는 메모리 펜스 없이 그냥 넘겨줄 수 있어. 넘겨받은 포인터가 dereference될 때 필요한 경우 print2 내부에서 메모리 펜스가 사용이 되겠지.
'어쨌든 메모리 펜스는 사용된다는거 아냐? 그럼 뭐하러 carries_dependency를 써?' 라고 생각할 수 있는데, 만약에 print와 print2 함수가 다음처럼 *ptr을 출력하는게 아니라 ptr을 출력한다면 어떨까?
void print(int* ptr) {
std::cout << ptr << std::endl;
}
void print2(int* [[carries_dependency]] ptr) {
std::cout << ptr << std::endl; // no memory fence needed
}
print2 함수 내부에서는 ptr을 dereference하지 않으니까 메모리 펜스가 필요하지 않다고 판단하고 메모리 펜스를 사용하지 않겠지? 하지만 print는 호출하는 쪽에서 print를 호출하기 전에 메모리 펜스를 사용해야 할거야.
디펜던시를 물고 가는건 [[carries_dependency]]라는건 이제 이해될거고, 그럼 디펜던시를 끊고 싶으면 어떻게 해야할까? 이 때는 std::kill_dependency()가 사용돼. 근데, 사실 나도 이 부분은 이해가 안되서 자세한 설명은 더 못하겠다. 궁금한 사람은 http://en.cppreference.com/w/cpp/atomic/kill_dependency 참고.
자 그럼 DCLP에 consume을 적용해보자. 이전에 사용한 DCLP 코드는 다음과 같아.
std::atomice<my_class*> instance(nullptr);
my_class* get_instance() {
my_class* p = instance.load(std::memory_order_acquire); // memory acquire fence is always issued here
if(!p) { // if instance is NULL
lock();
p = instance.load(std::memory_order_relaxed);
if(!p) { // if instance is still NULL
p = new my_class();
instance.store(p, std::memory_order_release);
}
unlock();
}
return p;
}
여기서 맨 윗줄의 acquire를 consume으로 바꾸면 되겠지? 그리고 consume한 p를 리턴할테니 리턴한 포인터가 dependency를 carry한다는걸 명시해주면 될거야. 그럼 DCLP 코드는 다음과 같아져.
[[carries_dependency]] my_class* get_instance() {
my_class* p = instance.load(std::memory_order_consume); // no memory acquire fence is needed.
if(!p) { // if instance is NULL
lock();
p = instance.load(std::memory_order_relaxed);
if(!p) { // if instance is still NULL
p = new my_class();
instance.store(p, std::memory_order_release);
}
unlock();
}
return p;
}
이제 효율적이면서도 쓰레드에 안전한 싱글톤 함수가 되었어. 이제 딱 하나만 더 고려하면 되는데, 만약에 get_instance() 함수가 포인터가 아니라 레퍼런스를 리턴하면 어떻게 될까?
[[carries_dependency]] my_class& get_instance() {
my_class* p = instance.load(std::memory_order_consume); // no memory acquire fence is needed.
if(!p) { // if instance is NULL
lock();
p = instance.load(std::memory_order_relaxed);
if(!p) { // if instance is still NULL
p = new my_class();
instance.store(p, std::memory_order_release);
}
unlock();
}
return *p; // Will a memory acquire fence be needed?
}
리턴할 때 *p 하니까 dereference하는거 아닌가 할 수 있지만, 레퍼런스라는게 사실상 포인터의 syntactic sugar이고 실제로는 dereference가 일어나지 않는다는게 중요해. 그러니까 마지막줄의 return *p;에서 메모리 펜스는 필요하지 않아.
뭔가 예제만 가득하고 설명이 부족한 느낌인데, 앞에서의 내용을 다 이해했다면 본 글에서의 내용이 그닥 어렵지 않을거야.
혹시 이해가 되지 않는 부분이나 잘못된 내용이 있다면 알려줘. 저번에 쓴 글들에 오류를 몇번 발견하다보니 급 의기소침해지고 자신이 없네 ㅋㅋ
다음에는 오퍼레이션 없이 메모리 펜스만 사용하는 경우에 대해 정리할게. atomic_thread_fence(http://en.cppreference.com/w/cpp/atomic/atomic_thread_fence)에서 acquire/release semantic이 앞선 글에서와 어떻게 다른지를 정리할거야.
좋은 꿈 꿔
http://lgstar.tk
커뮤니티 사이트 입니다 많은 이용 부탁 드립니다. - DCW