이번에는 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-none-linux-gnueabi-g++ -Wall -Wextra -std=c++14 -pedantic -O2 -mcpu=cortex-m4 -mthumb a.cpp -S -o -
...
.L2:
        ldr     r3, ptr    // r3 = LOAD ptr
        dmb     sy         // Unexpected unnecessary memory barrier
        cmp     r3, #0     // loop until r3 != 0
        beq     .L2
        ldr     r0, [r3]   // return LOAD *r3
...


어라 메모리 배리어가 사용되네? 분명 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을 사용하는 내용을 쓰려 했는데 오늘 좀 지치네. 이 부분은 다음에 계속해서 정리해볼게.


굿밤~