안녕하세요. 테크 랩에서 이미지 인식 기술 연구 개발을 하고 있는 Zhan(湛)입니다. 이번 글에서는 사내에서 진행하는 CPython 스터디에서 발표한 내용을 바탕으로 메모리 모델의 기초를 설명하겠습니다. Sequential Consistency(이하 SC)와 Total Store Order(이하 TSO)라는 두 가지 메모리 모델을 다룰 텐데요. 각 메모리 모델을 형식화해 이해하는 과정에서 스스로 메모리 모델에 대해 생각해 볼 수 있다면 좋겠습니다.
CPython 스터디를 시작한 계기와 PEP 703
메모리 모델에 대한 설명을 시작하기 전에 왜 CPython 스터디를 하게 되었는지, 어떤 계기로 이 주제를 발표하게 됐는지 설명하겠습니다. 이 장은 본론과 관련없는 내용이니 메모리 모델이 궁금해 들어오신 분이라면 바로 다음 장으로 건너가셔도 괜찮습니다.
저는 주 업무를 하고 있는 테크 랩이라는 조직 외에, 사내 Python 이용을 지원하는 조직인 Python 팀에도 소속돼 있습니다. Python 팀에서는 사내에서 CPython의 RPM 패키지를 빌드하거나 CI를 위해 Screwdriver.cd 템플릿을 제공하는 등의 활동을 하는데요. 몇 달 전부터 사내 Python 스킬 향상을 위해 'CPython 스터디'라는 Python 중급자 대상의 스터디 모임을 진행하고 있습니다.
이 글은 CPython 스터디에서 발표한 내용을 바탕으로 작성했습니다. PEP 703과 관련해 참조 카운트 구현을 읽다가 SEQ_CST와 RELAXED 등 메모리 모델 관련 키워드를 만났습니다(예: pyatomic_gcc.h). CPython 코드를 읽기 위해 메모리 모델를 꼭 이해해야 하는 것은 아니지만, 저 수준(low level) 코드를 읽다 보면 가끔씩 나오던 개념이라서 이번 기회에 공부해 봤습니다.
PEP 703에 대해서 조사한 것은 단순한 호기심 때문이기도 하지만, 주 업무인 딥 러닝에도 도움이 될 것이라고 생각했기 때문입니다. PEP 703의 Motivation 절에 나와 있듯 멀티 스레드로 확장하면 PyTorch의 DataLoader 성능 및 사용 편의성이 향상될 가능성이 있는데요. 이미지 관련 딥 러닝에서 데이터 로딩과 전처리에서 병목 현상이 발생하는 경우가 많기 때문에 PEP 703이 구현되면 꼭 업무로 검증해 보고 싶습니다.
SC는 명령이 작성된 순서대로 실행되는 프로그래머의 직관에 부합하는 모델입니다. 반면, x86이나 ARM, RISC-V와 같은 현대의 일반적인 CPU는 명령의 실행 순서를 바꿀 수 있다는 점에서 좀 더 느슨한 모델에 따라 작동합니다. 이번에 설명할 TSO는 x86 CPU의 동작을 형식화한 모델입니다(ARM이나 RISC-V는 TSO 보다 더 느슨한 모델(Relaxed Memory Consistency)을 따르고 있지만 여기서는 자세히 설명하지 않겠습니다).
글은 다음과 같은 순서로 진행됩니다.
- SC에서는 허용되지 않는 이상한 동작이 실제 CPU에서 발생하는 중요한 예시를 살펴보기
- 프로그래머의 직관에 부합하는 SC 형식화하기
- 실제 CPU에서 왜 SC에서 허용하지 않는 동작이 발생하는지 성능 관점에서 살펴보기
- TSO를 형식화해 x86 CPU의 메모리 모델 정확히 이해하기
- 고급 언어와 메모리 모델의 관계 보충 설명
이 글은 A Primer on Memory Consistency and Cache Coherence라는 책을 전반적으로 참고해 작성했으며, 인용한 부분에는 [1]과 함께 인용한 위치를 남겨 놓았습니다.
SC에서는 허용되지 않는 이상한 동작이 실제 CPU에서 발생하는 예시
다음 프로그램([1]의 표 3.3)을 '예시 1'이라고 합니다. 여기서 S1, S2는 저장 명령, L1, L2는 로드 명령입니다. x, y는 메모리 주소이고, r1, r2는 레지스터를 나타내는데요. 이 프로그램을 실행하면 r1과 r2의 값은 어떻게 될까요?
코어 C1 | 코어 C2 | 코멘트 |
---|---|---|
S1: x = NEW; | S2: y = NEW; | /* 초깃값은 x = 0 & y = 0 */ |
L1: r1 = y; | L2: r2 = x; |
병렬 프로그램에는 비결정성이 있기 때문에 결과가 하나로 결정되지 않습니다. 몇 가지 패턴을 생각해 보면 r1, r2 = (NEW, NEW), (0, NEW), (NEW, 0)의 가능성이 있음을 알 수 있습니다.
- S1 < S2 < L1 < L2 → (NEW, NEW)
- S1 < L1 < S2 < L2 → (0, NEW)
- S2 < L2 < S1 < L1 → (NEW, 0)
그렇다면 r1, r2 = (0, 0)이라는 결과는 나올 수 있을까요? 조금 생각해 보면 그럴 것 같지 않습니다.
- r1 = 0으로 가정한다.
- 이때 L1 < S2이다.
- 각 코어의 프로그램 순서에 따라 S1 < L1, S2 < L2이다.
- 2번과 3번에 의해 S1 < L1 < S2 < L2와 같은 순서가 결정된다.
- S1 < L2이므로 r2 = NEW이다.
위와 같은 '소박한 추론'은 맞을까요? 하드웨어에 따라 다르다는 것이 정확한 답입니다. 예를 들어, MIPS R10000이라는 CPU에서는 맞지만, x86이나 ARM, RISC-V 등 현대의 일반적인 CPU에서는 잘못된 것입니다. 하드웨어에 따라 메모리 모델이 다르기 때문입니다(아래로 갈수록 느슨해짐).
- SC(Sequential Consistency): MIPS R10000
- TSO(Total Store Order): x86
- RC(Relaxed Consistency): ARM
r1, r2 = (0, 0)이라는 실행 결과를 허용하는 x86과 ARM의 동작은 언뜻 보기에 이상한데요. 이 글의 주제는 '이러한 동작을 어떻게 이해할 것인가'입니다.
SC 형식화
SC는 앞 절에서 전개한 '소박한 추론'을 형식화한 것입니다. 다음과 같이 정의를 설명합니다. 이는 [1]의 3.5절에서 설명한 것과 같습니다.
프로그램 순서(<p)와 메모리 순서(<m)라는 두 가지 순서를 도입한다. <p는 코어별 전체 순서이고 <m은 글로벌 전체 순서이다. 이 순서는 다음 두 가지 조건을 만족한다.
(1)각 코어의 프로그램 순서는 메모리 순서에 반영된다. 다음과 같이 네 가지 경우가 있다(여기서 a, b는 서로 같을 때와 그렇지 않을 때를 모두 포함함).
- If L(a) <p L(b) ⇒ L(a) <m L(b) /* Load → Load */
- If L(a) <p S(b) ⇒ L(a) <m S(b) /* Load → Store */
- If S(a) <p S(b) ⇒ S(a) <m S(b) /* Store → Store */
- If S(a) <p L(b) ⇒ S(a) <m L(b) /* Store → Load */
(2)주소 a에 대한 로드 명령으로 얻은 값은 직전 주소 a에 대한 저장 명령으로 저장된 값이다.
- Value of L(a) = Value of MAX<m {S(a) | S(a) <m L(a) }
다음 그림([1]의 그림 3.3)은 위 정의에 따라 앞서 살펴본 예시 1을 분석한 것입니다. 좌우 양 끝에 있는 아래쪽 화살표가 코어별 프로그램 순서(<p)를, 중앙에 있는 아래쪽 화살표가 글로벌 메모리 순서(<m)를 나타냅니다.
앞서 정의(1)에서 프로그램 순서는 메모리 순서에 반영되므로, 위 그림에서 a, b, c의 가로 방향 화살표는 교차되지 않고 직선으로 뻗어 있습니다. r1, r2 = (0, 0)이라는 실행 결과가 그림에서 d로 표시되어 있는데요. 이는 화살표가 교차돼 있기 때문에 SC의 정의에 어긋난다는 것을 알 수 있습니다.
이번에 한 프로그램 순서와 메모리 순서를 사용하여 'r1, r2 = (0, 0)이라는 실행 결과는 없다'는 앞 장의 논의를 다시 정리해 보겠습니다.
- r1 = 0으로 가정한다.
- 이때 정의(2)에 따라 L1 <m S2이다.
- 정의(1)에 따라 각 코어의 프로그램 순서 S1 <p L1, S2 <p L2는 메모리 순서에 반영되므로 S1 <m L1, S2 <m L2이다.
- 2번과 3번에 따라 S1 <m L1 <m L1 <m S2 <m L2라는 순서가 결정된다.
- S1 <m L2와 정의(2)에 따라 r2 = NEW이다.
이번 장에서 형식화한 SC는 직관적이고 다루기 쉬운 메모리 모델이지만, 실제 CPU가 해당 모델을 따르지 않는 이유는 성능 때문입니다. 다음 장에서는 이와 관련된 설명을 하겠습니다.
SC를 벗어나고 싶은 이유 - 저장 버퍼
TSO를 형식화하기 전에 왜 SC를 벗어나고 싶은지 그 이유를 설명하겠습니다. 현재의 CPU는 메모리 쓰기(저장) 지연을 감추기 위해 일반적으로 아래 그림([1]의 그림 4.4a)과 같이 각 코어가 저장 버퍼(store buffer)라는 영역을 갖고 있습니다.