프로그램 - C++ 11은 표준화 된 메모리 모델을 도입했습니다. 무슨 뜻이에요? 그리고 C++ 프로그래밍에 어떤 영향을 미치게 될까요?




iso c++ 17 (4)

C ++ 11은 표준화 된 메모리 모델을 선보였지만 그게 정확히 무엇을 의미합니까? 그리고 C ++ 프로그래밍에 어떤 영향을 미치게 될까요?

허브 슬터 (Herb Sutter )를 인용 한 개빈 클라크 ( Gavin Clarke) 의이 기사에 따르면,

메모리 모델은 C ++ 코드가 컴파일러를 작성한 사람과 실행중인 플랫폼에 관계없이 이제는 표준화 된 라이브러리를 호출한다는 것을 의미합니다. 다른 스레드가 프로세서의 메모리와 통신하는 방법을 제어하는 ​​표준 방법이 있습니다.

"표준에있는 다른 코어에서 [코드]를 나누는 것에 대해 이야기 할 때, 우리는 메모리 모델에 대해 이야기하고 있습니다. 사람들이 코드에서 수행하게 될 다음의 가정을 깨지 않고 최적화 할 것입니다."라고 Sutter 는 말했습니다.

글쎄, 나는이 문단과 유사한 문단을 기억할 수있다. (나는 태어날 때부터 내 기억 모델을 가졌기 때문에) 다른 사람들이 물어 본 질문에 대한 답으로 게시 할 수도 있지만 솔직히 말해서 나는 정확하게 이해하지 못한다. 이.

그래서, 기본적으로 알고 싶은 것은 C ++ 프로그래머는 멀티 스레드 응용 프로그램을 개발하기 전에 사용했기 때문에 POSIX 스레드 나 Windows 스레드 또는 C ++ 11 스레드라면 어떻게 중요합니까? 이점은 무엇입니까? 저급 세부 사항을 이해하고 싶습니다.

C ++ 11 메모리 모델이 C ++ 11 멀티 스레딩 지원과 관련되어 있다는 느낌이 들었습니다. 그렇다면, 정확히 어떻게? 왜 그들이 관련되어야합니까?

멀티 스레딩의 내부 구조가 어떻게 작동하는지, 그리고 어떤 메모리 모델이 일반적으로 의미하는지는 알지 못하기 때문에 이러한 개념을 이해할 수 있도록 도와주십시오. :-)


나는 메모리 일관성 모델 (또는 메모리 모델, 간단히)을 이해하는 비유만을 제공 할 것이다. 레슬리 램 포트 (Leslie Lamport)의 "시간, 시계 및 분산 시스템에서 이벤트 주문"이라는 독창적 인 논문에서 영감을 얻은 것입니다. 비유는 적절하지만 근본적인 의미가 있지만 많은 사람들에게 과잉이라고 할 수 있습니다. 그러나, 메모리 일관성 모델에 대한 추론을 용이하게하는 정신적 이미지 (그림 표현)를 제공하기를 바랍니다.

수평축이 주소 공간을 나타내는 (즉, 각 메모리 위치는 해당 축의 한 점으로 표시됨) 시공간 다이어그램에서 모든 메모리 위치의 히스토리를보고 세로 축은 시간을 나타냅니다 일반적으로 시간에 대한 보편적 인 개념이 없다. 따라서 각 메모리 위치에 보유 된 값의 히스토리는 해당 메모리 주소에 세로 열로 표시됩니다. 각 값 변경은 해당 위치에 새 값을 쓰는 스레드 중 하나 때문입니다. 메모리 이미지 는 특정 스레드 가 특정 시간 에 관찰 할 수 있는 모든 메모리 위치의 집계 / 값 조합을 의미합니다.

"메모리 일관성 및 캐시 일관성에 대한 입문서" 에서 인용

직관적 인 (그리고 가장 제한적인) 메모리 모델은 스레드가 단일 코어 프로세서에서 시간 다중화 된 것처럼 다중 스레드 실행이 각 구성 스레드의 순차 실행의 인터리빙처럼 보이게하는 순차적 일관성 (SC)입니다.

전역 메모리 순서는 프로그램 실행마다 다를 수 있으며 사전에 알려지지 않을 수도 있습니다. SC의 특징 은 동시성 평면 (즉, 메모리 이미지)을 나타내는 주소 공간 시간 다이어그램의 수평 슬라이스 집합입니다. 주어진 평면에서 모든 이벤트 (또는 메모리 값)는 동시에 발생합니다. 모든 스레드가 어떤 메모리 값이 동시인지에 대해 절대 시간 이라는 개념이 있습니다. SC에서는 매 순간마다 모든 스레드가 공유하는 메모리 이미지가 하나뿐입니다. 즉, 모든 순간에 모든 프로세서가 메모리 이미지 (즉, 메모리의 전체 내용)에 동의합니다. 모든 스레드가 모든 메모리 위치에 대해 동일한 값 시퀀스를 볼뿐만 아니라 모든 프로세서가 모든 변수의 동일한 값 조합을 관찰한다는 것을 의미합니다. 이것은 (모든 메모리 위치에서) 모든 메모리 조작이 모든 스레드에 의해 동일한 전체 순서로 관찰된다는 것을 말하는 것과 같습니다.

느슨한 메모리 모델에서 각 스레드는 주소 공간 시간을 자체 방식으로 분할합니다. 유일한 제한 사항은 모든 스레드가 모든 개별 메모리 위치의 기록에 동의해야하기 때문에 각 스레드의 조각이 서로 교차하지 않아야한다는 것입니다 (물론 , 서로 다른 스레드 조각이 서로 교차 할 수 있습니다.) 그것을 조각 내기위한 보편적 인 방법은 없습니다 (주소 공간 시간의 특권없는 축소). 조각은 평면 (또는 선형) 일 필요는 없습니다. 그것들은 곡선이 될 수 있고 이것은 쓰레드가 쓰여진 순서대로 다른 쓰레드에 의해 쓰여진 값을 읽게 할 수있다. 다른 메모리 위치의 역사는 특정 쓰레드로 볼 때 서로에 대해 임의적으로 미끄러지거나 늘어날 수있다. . 각 스레드는 어떤 이벤트 (또는 동등한 메모리 값)가 동시에 발생하는지에 대해 다른 의미를 갖습니다. 하나의 스레드와 동시에 발생하는 이벤트 집합 (또는 메모리 값)은 다른 스레드와 동시에 발생하지 않습니다. 따라서, 완화 된 메모리 모델에서, 모든 스레드는 여전히 각 메모리 위치에 대해 동일한 히스토리 (즉, 값의 시퀀스)를 관측합니다. 그러나 그들은 서로 다른 메모리 이미지 (즉, 모든 메모리 위치의 값 조합)를 관찰 할 수 있습니다. 두 개의 다른 메모리 위치가 동일한 스레드에 의해 순차적으로 쓰여지더라도 새로 작성된 두 개의 값은 다른 스레드에 의해 다른 순서로 관찰 될 수 있습니다.

[Wikipedia의 그림]

아인슈타인의 특수 상대성 이론에 익숙한 독자는 내가 암시하는 것을 눈치 챌 것입니다. Minkowski의 말을 메모리 모델 영역으로 변환 : 주소 공간과 시간은 주소 공간 시간의 그림자입니다. 이 경우 각 관측자 (즉, 스레드)는 자신의 월드 라인 (즉 시간 축)과 자신의 동시성 평면 (자신의 주소 공간 축)에 이벤트의 그림자 (예 : 메모리 저장 /로드)를 투사합니다. . C ++ 11 메모리 모델의 스레드는 특수 상대성간에 서로에 대해 상대적으로 움직이는 관찰자에 해당합니다. 순차적 일관성은 갈릴리 시공간에 해당한다 (즉, 모든 관측통은 사건의 절대적 순서와 전지구 적 동시성에 동의한다).

메모리 모델과 특수 상대성 사이의 유사점은 부분적으로 정렬 된 일련의 이벤트를 정의한다는 사실 (종종 인과 관계 집합이라고도 함) 때문입니다. 일부 이벤트 (예 : 메모리 상점)는 다른 이벤트에 영향을 줄 수 있지만 영향을받지는 않습니다. C ++ 11 쓰래드 (또는 물리학 관측통)는 이벤트 (예 : 메모리로드 및 가능하면 다른 주소로의 저장)의 체인 (즉, 완전히 정렬 된 집합)을 넘지 않습니다.

상대성에있어서, 일부 관측자들은 부분적으로 순서가 지정된 사건의 겉으로보기에는 혼란스러운 그림으로 복원된다. 왜냐하면 모든 관측통이 동의하는 유일한 시간 순서는 "시간 상적인"사건들 (즉, 어떤 입자에 의해 원칙적으로 연결될 수있는 사건들) 진공에서 빛의 속도보다). timelike 관련 이벤트 만 불변하게 명령됩니다. 물리학의 시간, Craig Callender .

C ++ 11 메모리 모델에서 비슷한 메커니즘 (획득 - 릴리스 일관성 모델)이 이러한 지역 인과 관계 를 확립하는 데 사용됩니다.

메모리 일관성의 정의와 SC 포기 동기를 제공하기 위해 "메모리 일관성 및 캐시 일관성에 관한 입문서"

공유 메모리 머신의 경우 메모리 일관성 모델은 메모리 시스템의 구조적으로 보이는 동작을 정의합니다. 단일 프로세서 코어에 대한 정확성 기준은 " 올바른 결과 하나 "와 " 많은 잘못된 대안 "간의 동작을 분할합니다. 이는 프로세서의 아키텍처가 스레드의 실행이 특정 입력 상태를 out-of-order 코어에서도 단일 잘 정의 된 출력 상태로 변환하도록 요구하기 때문입니다. 그러나 공유 메모리 일관성 모델은 여러 스레드의로드 및 저장과 관련이 있으며 일반적으로 여러 가지 잘못된 실행 을 허용하지 않으면 서 올바른 실행 을 많이 허용합니다. ISA가 여러 스레드를 동시에 실행할 수 있도록하기 때문에 여러 번 올바르게 실행될 수 있습니다. 종종 다른 스레드의 여러 가지 명령 인터리빙이 가능합니다.

느슨 하거나 약한 메모리 일관성 모델은 강력한 모델에서 대부분의 메모리 정렬이 불필요하다는 사실에 기인합니다. 스레드가 10 개의 데이터 항목을 업데이트 한 다음 동기화 플래그를 업데이트하는 경우 프로그래머는 일반적으로 데이터 항목이 서로 순서대로 업데이트되지만 플래그가 업데이트되기 전에 모든 데이터 항목이 업데이트된다는 점에 신경 쓰지 않습니다 (일반적으로 FENCE 명령어를 사용하여 구현됩니다 ). 편안한 모델은 이러한 주문 유연성을 향상시키고 SC의 높은 성능과 정확성을 모두 얻으려면 프로그래머가 요구 하는 주문 만 보존하려고합니다. 예를 들어, 특정 아키텍처에서는 FIFO 쓰기 버퍼가 결과를 캐시에 기록하기 전에 커밋 된 (폐기 된) 저장소의 결과를 보유하기 위해 각 코어에서 사용됩니다. 이 최적화는 성능을 향상 시키지만 SC에 위배됩니다. 쓰기 버퍼는 저장소 실패를 처리하는 대기 시간을 숨 깁니다. 상점은 공통점이 있으므로 대부분의 상점에서의 실속을 피할 수 있다는 것이 중요한 이점입니다. 단일 코어 프로세서의 경우 A에 대한로드가 A에 대한 하나 이상의 상점이 쓰기 버퍼에 있어도 가장 최근의 저장소의 값을 A로 리턴하도록 보장하여 구조적으로 보이지 않도록 작성 버퍼를 작성할 수 있습니다. 이는 가장 최근의 저장소 값을 A에 대한 부하를 A에서 부하로 우회하거나 "가장 최근"이 프로그램 순서에 의해 결정되거나 A에 대한 저장소가 쓰기 버퍼에있을 경우 A의 부하를 지연시킴으로써 수행됩니다 . 여러 개의 코어가 사용되는 경우 각 코어에는 자체 바이 패스 쓰기 버퍼가 있습니다. 쓰기 버퍼가 없으면 하드웨어는 SC이지만 쓰기 버퍼가있는 경우 그렇지 않습니다. 멀티 코어 프로세서에서 구조적으로 볼 수있는 쓰기 버퍼를 만듭니다.

코어에 입력 된 순서와 다른 순서로 저장소를 출발시킬 수있는 비 FIFO 쓰기 버퍼가 코어에있는 경우 저장소 저장 순서 재 지정이 발생할 수 있습니다. 첫 번째 저장소가 두 번째 히트 또는 두 번째 저장소가 이전 저장소 (즉, 첫 번째 저장소 이전)와 병합 할 수있는 경우 캐시에서 첫 번째 저장소가 누락 된 경우에 발생할 수 있습니다. 로드 순서 재정렬은 프로그램 순서를 벗어난 명령을 실행하는 동적으로 스케줄 된 코어에서도 발생할 수 있습니다. 다른 코어에서 저장소를 재정렬하는 것과 똑같이 동작 할 수 있습니다 (두 스레드간에 예제 인터리빙을 할 수 있습니까?). 나중에 스토어 (로드 저장 순서 재 지정)로 이전로드를 재정렬하면 스토어가 잠금 해제 조작 인 경우이를 보호하는 잠금을 해제 한 후 값을로드하는 것과 같은 많은 잘못된 작동이 발생할 수 있습니다. 저장소 순서 재정렬은 프로그램 순서로 모든 명령을 실행하는 코어가 있더라도 일반적으로 구현 된 FIFO 쓰기 버퍼에서 로컬 우회로 인해 발생할 수 있습니다.

캐시 일관성과 메모리 일관성이 때때로 혼동되기 때문에이 인용문을 갖는 것이 좋습니다.

일관성과 달리 캐시 일관성 은 소프트웨어에 표시되거나 필요하지 않습니다. Coherence는 공유 메모리 시스템의 캐시를 단일 코어 시스템의 캐시처럼 기능적으로 보이지 않게 만들려고합니다. 정확한 일관성은 프로그래머가로드 및 저장 결과를 분석하여 시스템에 캐시가 있는지 여부 및 위치를 판별 할 수 없도록합니다. 이는 일관된 일관성으로 인해 캐시가 새롭거나 다른 기능적 동작을 허용하지 않기 때문입니다 (프로그래머는 타이밍 정보를 사용하여 캐시 구조를 추측 할 수 있습니다). 캐시 일관성 프로토콜의 주요 목적은 모든 메모리 위치에 대해 단일 기록기 - 다중 판독기 (SWMR)를 일정하게 유지하는 것입니다. 일관성과 일관성 사이의 중요한 차이점은 일관성이 모든 메모리 위치와 관련하여 지정되는 반면, 일관성은 메모리 별 위치 기준으로 지정된다는 입니다.

우리의 정신적 그림을 계속하면, SWMR 불변량은 어떤 한 위치에있는 하나의 입자가 존재한다는 물리적 요구 사항에 해당하지만, 어떤 위치의 관찰자도 무제한이 될 수 있습니다.


먼저, 언어 변호사처럼 생각하는 법을 배워야합니다.

C ++ 사양은 특정 컴파일러, 운영 체제 또는 CPU를 참조하지 않습니다. 실제 시스템의 일반화 된 추상 기계 를 참조합니다. 언어 변호사의 세계에서 프로그래머는 추상적 기계 용 코드를 작성해야합니다. 컴파일러의 역할은 콘크리트 시스템에서 그 코드를 실현하는 것입니다. 스펙을 엄격하게 코딩하면 현재 또는 향후 50 년 동안 호환되는 C ++ 컴파일러가있는 시스템에서 코드를 수정하지 않고 컴파일하고 실행할 수 있습니다.

C ++ 98 / C ++ 03 사양의 추상 기계는 근본적으로 단일 스레드입니다. 따라서 스펙과 관련하여 "완벽하게 이식 가능"한 멀티 쓰레드 C ++ 코드를 작성하는 것은 불가능합니다. 이 스펙은 메모리로드 및 스토어의 원 자성 또는로드 및 저장이 발생할 수있는 순서 에 대해 아무 것도 말하지 않으며 뮤텍스와 같은 것을 신경 쓰지 않습니다.

물론 pthread 나 Windows와 같은 특정 구체적인 시스템에 대해 실제로 멀티 스레드 코드를 작성할 수 있습니다. 그러나 C ++ 98 / C ++ 03에 대한 다중 스레드 코드를 작성하는 표준 방법은 없습니다.

C ++ 11의 추상 기계는 설계 상 멀티 스레드입니다. 또한 잘 정의 된 메모리 모델을 가지고 있습니다 . 즉, 컴파일러가 메모리에 액세스 할 때 수행 할 수도 있고 수행하지 않을 수도있는 작업을 말합니다.

두 개의 스레드에 의해 한 쌍의 전역 변수가 동시에 액세스되는 다음 예제를 고려하십시오.

           Global
           int x, y;

Thread 1            Thread 2
x = 17;             cout << y << " ";
y = 37;             cout << x << endl;

스레드 2 출력은 무엇입니까?

C ++ 98 / C ++ 03에서는 이것이 정의되지 않은 동작조차되지 않습니다. 표준은 "스레드"라고하는 것을 고려하지 않기 때문에 질문 자체는 의미 가 없습니다.

C ++ 11에서로드 및 저장은 일반적으로 원자적일 필요가 없으므로 결과는 정의되지 않은 동작입니다. 어느 정도 향상되지 않은 것처럼 보일 수도 있습니다 ... 그리고 그 자체로는 그렇지 않습니다.

하지만 C ++ 11에서는 다음과 같이 작성할 수 있습니다.

           Global
           atomic<int> x, y;

Thread 1                 Thread 2
x.store(17);             cout << y.load() << " ";
y.store(37);             cout << x.load() << endl;

이제 상황이 훨씬 더 재미 있습니다. 우선, 여기서의 행동이 정의됩니다 . 이제 스레드 2는 0 0 (스레드 1 이전에 실행되는 경우), 37 17 (스레드 1 이후에 실행되는 경우) 또는 0 17 (스레드 1이 x에 할당 된 후 y에 할당되기 전에 실행되는 경우)를 인쇄 할 수 있습니다.

C ++ 11의 원자 적로드 / 저장소에 대한 기본 모드는 순차적 일관성 을 적용하기 때문에 인쇄 할 수없는 것은 37 0 입니다. 이것은 단순히 모든로드 및 저장소가 각 스레드 내에서 작성한 순서대로 "있는 그대로"있어야한다는 것을 의미하지만 스레드간에 작업을 인터리브 할 수 있지만 시스템에서는 좋아합니다. 따라서 원자의 기본 동작은로드 및 저장에 원 자성순서 를 제공합니다.

이제 현대 CPU에서 순차적 일관성을 유지하는 데 많은 비용이 듭니다. 특히, 컴파일러는 모든 액세스간에 본격적인 메모리 장벽을 내고 있습니다. 그러나 알고리즘이 순서가 잘못된로드 및 저장을 허용 할 수 있다면 즉, 원 자성이 필요하지만 순서가 필요하지 않은 경우; 즉,이 프로그램의 출력으로 37 0 을 허용 할 수 있다면 다음과 같이 작성할 수 있습니다.

           Global
           atomic<int> x, y;

Thread 1                            Thread 2
x.store(17,memory_order_relaxed);   cout << y.load(memory_order_relaxed) << " ";
y.store(37,memory_order_relaxed);   cout << x.load(memory_order_relaxed) << endl;

CPU가 현대적 일수록 이전 예제보다 더 빠를 가능성이 높습니다.

마지막으로, 특정로드 및 저장을 순서대로 유지해야하는 경우 다음을 작성할 수 있습니다.

           Global
           atomic<int> x, y;

Thread 1                            Thread 2
x.store(17,memory_order_release);   cout << y.load(memory_order_acquire) << " ";
y.store(37,memory_order_release);   cout << x.load(memory_order_acquire) << endl;

이렇게하면 순서가 지정된로드 및 저장으로 되돌아갑니다. 따라서 37 0 은 더 이상 출력이 불가능하지만 최소한의 오버 헤드로 수행됩니다. (이 간단한 예제에서는 결과가 본격적인 일관성과 동일하지만 큰 프로그램에서는 그렇지 않습니다.)

물론보고 싶은 출력이 0 0 또는 37 17 이면 원래 코드 주위에 뮤텍스를 래핑 할 수 있습니다. 그러나 당신이 이것을 멀리 읽었다면, 당신은 이미 그것이 어떻게 작동하는지 알았을 것입니다. 그리고이 답변은 이미 의도 한 것보다 더 오래갑니다 :-).

그래서 결론. 뮤텍스는 훌륭하고 C ++ 11에서는이를 표준화합니다. 하지만 때로는 성능상의 이유로 저수준 프리미티브 (예 : 기본 이중 체크 잠금 패턴 )를 원합니다. 새로운 표준은 뮤텍스 및 조건 변수와 같은 상위 가제트를 제공하며 원자 유형 및 다양한 메모리 장벽과 같은 저수준 가젯도 제공합니다. 이제 표준에 의해 지정된 언어로 정교하고 고성능의 동시 루틴을 작성할 수 있으며, 현재 시스템과 미래의 시스템 모두에서 코드가 변경되지 않고 컴파일되고 실행될 수 있습니다.

솔직히 말해서, 전문가이고 심각한 저수준 코드로 작업하지 않는 한, 뮤텍스와 조건 변수를 고수해야합니다. 그것이 내가하려는 의도입니다.

이 내용에 대한 자세한 내용은 이 블로그 게시물을 참조하십시오.


뮤텍스를 사용하여 모든 데이터를 보호한다면 실제로 걱정할 필요가 없습니다. 뮤텍스는 항상 충분한 주문과 시정 보장을 제공합니다.

자, atomics 또는 lock-free 알고리즘을 사용한다면, 메모리 모델에 대해 생각할 필요가 있습니다. 메모리 모델은 atomics가 주문 및 가시성 보장을 제공 할 때 정확하게 설명하고 손으로 코드화 된 보증을위한 휴대용 울타리를 제공합니다.

이전에는 atomics가 컴파일러 내장 함수 또는 일부 상위 라이브러리를 사용하여 수행되었습니다. 울타리는 CPU 관련 지침 (메모리 장벽)을 사용하여 완료되었을 것입니다.


이것은 이제 표준이 멀티 스레딩을 정의하고 다중 스레드 컨텍스트에서 어떤 일이 발생하는지 정의합니다. 물론, 사람들은 다양한 구현을 사용했지만, 그것은 우리가 집에서 구르는 string 클래스를 사용할 수있을 때 우리가 std::string 가져야하는 이유를 묻는 것과 같습니다.

POSIX 쓰레드 나 윈도우 쓰레드에 대해서 이야기 할 때, x86 쓰레드에 대해 실제로 말하는 것처럼 환상적입니다. 동시에 실행되는 하드웨어 기능이기도합니다. C ++ 0x 메모리 모델은 사용자가 x86, ARM 또는 MIPS 하는지 여부와 상관없이 보장합니다.







memory-model