ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Effective Modern C++] std::atomic VS volatile
    C++/Effective Modern C++ 2023. 5. 12. 18:32

    volatile과 std::atomic은 서로 다른 기능을 하는 도구입니다.

    그러나 두 도구 모두 변수에 특정 속성을 부여하고, 최적화, 동기화와 관련된 도구이다보니 그 사용처가 혼동되고는 합니다.

    이번 글에서는 두 도구의 차이를 중점적으로 살펴보도록 하겠습니다.

     


     

    1. RMW(Read-Modify-Write) operation

     

    더보기

    다음 예제를 보도록 하겠습니다.

    std::atomic<int> ai(0); // ai = 0
    ai = 10; // ai = 10
    ++ai; // ai = 11
    --ai; // ai = 10

     

    위 코드의 std::atomic객체 ai에 대한 연산에 대하여, 관측 가능한 ai의 값은 주석과 동일한 0, 10, 11입니다.

    std::atmoic객체는 객체에 대한 연산을 원자적으로 (더 이상 쪼갤 수 없음) 수행합니다.

     

    하지만 volatile은 조금 다릅니다.

    volatile int vi(0); // vi = 0
    vi = 10; // vi = 10
    ++vi; // vi = 11
    --vi; // vi = 10

    위 코드의 volatile 객체 vi에 대한 연산에 대하여, 관측 가능한 vi의 값은 특정할 수 없습니다.

    vi의 메모리에 접근하는 Read와 Write사이에 순서가 정해지지 않아, Data race가 일어나기 때문입니다.

     

    2. 컴파일러 최적화

     

    더보기

    컴파일러는 코드에 대한 최적화를 수행합니다.

    이 과정에서 몇몇 코드가 다른 코드로 바뀌거나, 코드의 실행 순서가 바뀌거나, 코드가 아예 사라질 수 있습니다.

    하지만 컴파일러의 이러한 최적화가 독이 되는 상황이 존재합니다.

     

    다음의 코드는 이전 글 (스레드 간 단발성 이벤트 통신) 의 예제와 유사한, 이벤트의 반응과 검출을 분리한 경우 중 검출 부분입니다.

    bool valAvailable(false);
    auto imptValue = computeImportantValue();
    valAvailable = true;

    위 코드는 다른 과제가 사용할 특정 값을 계산하고, 그 값이 준비되었음을 알리는 코드입니다.

    다른 과제는 valAvailable의 값을 검사하여, true가 될 경우 imptValue에 대한 특정 연산을 수행하는 코드가 될 것입니다.

    이 경우, valAvailable을 true로 바꾸는 연산은 반드시 imptValue의 계산 이후에 수행되어야 합니다.

    하지만 컴파일러의 관점에서 두 코드는 배정 연산 두 줄에 불과하므로, 두 코드의 순서가 변경되어도 무관합니다.

     

    하지만 std::atmoic을 사용할 경우, 그러한 순서의 변경에 제약이 발생합니다.

    제약 중 하나는 atomic변수를 기록하는 코드 이전에 나오는 코드들은, 그 코드 이후에 실행될 수 없다는 것 입니다.

    따라서, 위 valAvailable을 atomic으로 선언할 경우, true배정과 imptValue계산의 순서가 변경되지 않음을 보장할 수 있습니다.

     

    하지만 volatile은 그러한 제약이 가해지지 않습니다.

    valAvailable을 volatile로 선언하더라도, 코드의 실행 순서는 컴파일러에 의해 변경될 수 있습니다.

     

    3. MMIO (Momory-Mapped I/O)

     

    더보기

    다음 코드를 보도록 하겠습니다.

    auto y = x;
    y = x;

    위 코드는 위와 아래가 동일한 연산을 수행합니다.

    따라서 컴파일러에 의해 아래쪽 코드가 삭제될 수 있습니다.

    x = 10;
    x = 20;

    위 코드도 마찬가지입니다.

    컴파일러에 의해 x = 10코드가 제거될 수 있습니다.

    즉, 컴파일러는 불필요한 읽기, 쓰기 (이를 Redundant load, Dead stores라고 합니다) 를 제거하는 최적화를 수행합니다.

     

    하지만 위와 같은 코드가 필요한 상황이 존재합니다.

    위 변수가 점유하는 메모리가 일반적인 메모리가 아니라, 특별한 역할을 하는 하드웨어 메모리일 경우, 이야기가 조금 달라집니다.

     

    문단 첫부분 y의 배정 연산의 경우, 코드상으로는 x에 변화가 없을 수 있지만, x가 특정 물리 값 (센서 값)이라고 할 경우, 첫 번째와 두 번째 값은 서로 다를 수 있습니다.

    그 다음 x에 값을 대입하는 연산도 특정 하드웨어의 작동을 지시하는 코드라고 할 경우, 두 번의 배정 모두 필요한 연산일 수 있습니다.

     

    volatile은 이러한 경우에 사용됩니다.

    volatile이 선언된 변수는 컴파일러에 의해 최적화 되지 않습니다.

    위의 최적화 가능성이 있는 두 코드 모두, volatile로 선언될 경우 코드 최적화가 발생하지 않습니다.

     

    하지만 std::atomic은 그러한 규칙이 존재하지 않습니다.

    atomic변수에 대하여, 컴파일러는 일반 변수와 동일하게 코드 최적화를 수행합니다.

     

    4. std::atmoic::load, store

     

    더보기

    std::atomic은 복사 불가능 객체입니다.

    예를 들어, 다음과 같은 코드는 컴파일 에러가 발생합니다.

    std::atomic<int> x;
    auto y = x;
    y = x;

    이는 atomic의 특성 때문입니다.

    위 auto y는 형식 연역에 의해 std::atomic<int>로 연역될 것 입니다.

    따라서 두 번째 코드는 x의 값을 읽고, 그것을 y에 기록하는 연산인 것에 더하여, 그 연산이 원자적으로 수행되어야 합니다.

    하지만 하드웨어 수준에서 이를 지원하지 않기 때문에, 표준 위원회는 std::atomic에 대한 복사를 지원하지 않기로 했습니다.

     

    하지만 x를 y에 대입하는 것은 가능합니다.

    std::atmoic<int> y(x.load());
    y.store(x.laod());

    std::atomic의 load 및 store연산은 읽기, 쓰기를 수행합니다.

    위 코드의 경우, x의 값을 읽어 y를 초기화하고 (첫 번째 코드)

    다시 x의 값을 읽어 y에 저장합니다 (두 번째 코드)

    하지만 x를 읽는 연산과 y를 초기화하는 연산은 개별적인 함수 호출이기 때문에, 두 연산이 하나의 원자적 연산이 될 것이라고 기대할 수 없습니다.

     

    컴파일러는 위 코드를 다음과 같이 최적화 할 수 있습니다.

    {register} = x.load();
    std::atomic<int> y({register});
    y.store({register});

    x의 값을 한 번만 읽어 레지스터에 저장하고, 그 값을 y의 초기화에 사용하고, 대입하는 것 입니다.

    결과적으로 바로 위 코드와 비교하여, x에 대한 읽기를 한 번만 수행하는 것으로 최적화가 되지만, 위 3번 문단에 이어서 생각했을 경우, 위와 같은 최적화는 적합하지 않을 수 있습니다. 

     


     

    std::atomic과 volatile은 서로 그 용도가 완벽하게 다르다는 것을 알아보았습니다.

    용도가 다르기 때문에, 두 키워드는 함께 사용하는 것 또한 가능합니다.

    volatile std::atomic<int> vai;

    위와 같은 변수는 MMIO를 사용하면서, 동기화 되어야 하는 멀티 스레드 프로그램에 사용될 변수로 적합할 것 입니다.

     

    감사합니다.

    댓글

Designed by Tistory.