Introduction
메모리 컨시스턴시 모델은 여러 쓰레드가 메모리를 공유한 상태에서 나올 수 있는 실행 결과에 대한 규칙이다. 여기서 결과라 함은, 쓰레드 상태(레지스터 등 architecturally visible state)와 메모리 상태를 말한다.
외부로부터 쓰레드에 정보가 전달되는 경로가 메모리밖에 없다고 가정하면, 메모리 LOAD가 반환하는 값에 대한 규칙을 정하면 실행 결과 또한 정해진다. 그래서 메모리 컨시스턴시 모델을 메모리 LOAD가 반환하는 값에 대한 규칙으로 정의하는 경우도 있다[^1]. 다시 말해, 어떤 쓰레드에서 실행된 메모리 operation이 다른 쓰레드에 보여지는 시점과 순서에 대한 규칙이다.
왜 메모리 operation이 실행되는 즉시 다른 쓰레드에 보이게 하지 않고 이런 복잡한 모델을 정의하는 걸까? 그러한 하드웨어로는 고성능을 달성하기 힘들기 때문이다. 고성능을 달성하기 위해서 하드웨어 제약사항을 어디까지 해제하고 프로그램의 correctness를 달성하기 위해 프로그래머가 어떻게 프로그래밍 해야 하는지 엄밀하게 명시하기 위해 메모리 컨시스턴시 모델을 정의하는 것이다. 프로그래머와 시스템 사이의 계약이라고 볼수도 있다.
메모리 컨시스턴시 모델은 ISA(Instruction Set Architecture)에 정의되어 있을 수도 있고, 프로그래밍 언어의 스펙에 정의되어 있을 수도 있다. 전자의 경우 해당 ISA를 따르는 하드웨어에 적용이 되겠고, 후자의 경우 프로그래밍 언어가 가정하는 virtual machine에 적용이 된다고 생각하면 되겠다. 컴파일러는 프로그래밍 언어와 하드웨어의 메모리 컨시스턴시 모델을 모두 고려해서 correct하고 높은 성능의 프로그램을 생성해야 한다.
예를 들어, 다음 프로그램을 보자.
Initial state
data = 0
done = false
+------------------+------------------+
| Thread 0 | Thread 1 |
| data = 5 | while (not done) |
| done = true | print data |
+------------------+------------------+
data와 done이 메모리의 어떤 값이라고 생각하자. 쓰레드 0은 data에 5를 쓰고 done에 true를 넣는다. 쓰레드 1은 done이 false인 동안 루프를 돌다가 done이 true가 되면 data를 출력한다.
상식적으로 생각했을 때, data가 5가 된 후 done이 true가 되기 때문에 Thread 1에서는 항상 5가 출력이 될 것이다. 그러나 주변의 x86_64 CPU에서 위 프로그램을 실행해보면 0이 출력이 될 수도 있다. 이는 x86_64가 따르는 메모리 컨시스턴시 모델이 relax 되어 있어서, Thread 0이 실행한 메모리 operation 순서대로 Thread 1에 보여짐이 보장되지 않기 때문이다.
예시에서 알 수 있듯이, correct한 프로그램을 작성하려면 프로그래머는 메모리 컨시스턴시 모델을 숙지하고 있어야만 한다.
Relation with Cache Coherence
TODO
OOTA problem
TODO
Sequential Consistency
프로그램에 의해 정의된 operation의 순서를 program order라고 한다.
TODO
[^1]: 첫번째 방식으로 정의된 메모리 컨시스턴시 모델을 유형 A, 두번째 방식으로 정의된 모델을 유형 B라고 하자. 엄밀하게 보자면, LOAD는 제멋대로 값을 반환하는데 최종 실행 결과 자체는 규칙을 따르는 마법의 하드웨어가 존재할 수 있으므로 유형 A는 유형 B를 포함하는 넓은 개념이다. 다시 말해, 유형 B의 모델이 있으면 유형 A의 모델을 만들 수 있지만 그 반대는 성립하지 않는다. 그러나 유형 A로는 정의가 가능하면서 유형 B로는 정의가 불가능한 메모리 컨시스턴시 모델과 그에 부합하는 하드웨어는 만들기도 어렵고, 만들다 한들 크게 성능 이득을 보기도 어렵기 때문에 유형 B로 메모리 컨시스턴시 모델을 정의한다 해서 큰 문제가 되지는 않는다. (예를 들어, RISC-V의 메모리 컨시스턴시 모델은 유형 B로 정의되어 있음)