카테고리 없음

컴퓨터 구조 복습 Data/Memory에서 Dependency로 인한 문제 및 해결

LetsCoding 2026. 9. 18. 20:12

파이프라이닝 → Hazard 3종류 → Data Hazard의 RAW/WAR/WAW → Forwarding/Stall → Out-of-Order/레지스터 리네이밍 순서.

 

먼저 출발점은 파이프라이닝(Pipelining) 이야. CPU가 명령어 하나를 끝낸 다음 다음 명령어를 시작하면 너무 느리니까, 여러 명령어를 겹쳐서 처리해.

고전적인 5-stage pipeline은 보통 이렇게 설명해.

IF  : Instruction Fetch
ID  : Instruction Decode / Register Read
EX  : Execute
MEM : Memory Access
WB  : Write Back
 

예를 들어 명령어 3개가 있으면:

Cycle →   1    2    3    4    5    6    7

Inst1     IF   ID   EX   MEM  WB
Inst2          IF   ID   EX   MEM  WB
Inst3               IF   ID   EX   MEM  WB
 

한 명령어가 완전히 끝나기 전에 다음 명령어를 시작하는 거지.

문제는 이렇게 겹쳐서 실행하다 보니 명령어끼리 서로 방해하는 상황, 즉 Hazard가 생긴다는 거야.

 

1. Data Hazard가 뭔가

이 코드를 보자.

ADD R1, R2, R3   ; R1 = R2 + R3
SUB R4, R1, R5   ; R4 = R1 - R5
 

두 번째 명령어는 R1이 필요해.

그런데 파이프라인에서는:

ADD : R1 계산 중
SUB : 벌써 R1을 읽으려고 함
 

이라는 상황이 생길 수 있어.

즉:

앞 명령어가 값을 만들기도 전에 뒤 명령어가 그 값을 사용하려고 하는 것

이 Data Hazard의 대표적인 형태야.

 

 

2. RAW — Read After Write

Data Hazard에서 제일 중요한 게 RAW야.

ADD R1, R2, R3   ; R1에 write
SUB R4, R1, R5   ; R1을 read
 

순서가:

Write R1
↓
Read R1
 

이어야 하는데 파이프라인 때문에 Read가 너무 빨리 일어나려고 하는 거지.

그래서 이름이:

Read After Write
 

야.

이걸 True Dependency(진짜 데이터 의존성) 라고도 해.

왜냐하면 두 번째 명령어는 정말 첫 번째 명령어의 결과가 필요하니까.

ADD 결과
  ↓
  R1
  ↓
SUB 입력
 

알고리즘적으로도 없앨 수 없는 진짜 의존성이야.

 

3. WAR — Write After Read

이번에는:

MOV R3, R1      ; R1을 읽음
ADD R1, R4, R5  ; R1에 씀
 

원래 프로그램 순서에서는:

첫 번째 명령어가 R1을 읽고
↓
두 번째 명령어가 R1을 변경
 

해야 해.

그런데 Out-of-Order CPU가 두 번째를 너무 빨리 실행해버리면:

R1 변경
↓
첫 번째 명령어가 R1 읽음
 

이 돼서 잘못된 값을 읽을 수 있어.

이게:

Write After Read
 

즉 WAR야.

이건 Anti-dependency라고도 불러.

중요한 차이는 RAW와 달리 실제 데이터가 연결된 게 아니라 같은 register 이름을 써서 생기는 충돌이라는 거야.

4. WAW — Write After Write

예를 들어:

MUL R1, R2, R3   ; R1 = ...
ADD R1, R4, R5   ; R1 = ...
 

원래는:

MUL이 R1 기록
↓
ADD가 R1 기록
 

해야 하니까 최종 R1은 ADD의 결과여야 해.

그런데 Out-of-Order 실행에서:

ADD가 먼저 끝남
↓
MUL이 나중에 끝남
 

하면서 MUL이 R1을 덮어쓰면 틀린 결과가 돼.

이게:

Write After Write
 

WAW.

이것도 실제 데이터 의존성이라기보다 같은 destination register 이름 때문에 생기는 dependency야.

그래서 WAR와 WAW는 흔히 Name Dependency라고 묶어서 봐.

 

요약하면,

기준은 뒤 명령어 기준으로 읽으면 쉬워.

RAW 앞: Write 뒤: Read → 뒤가 앞의 결과를 필요로 함

WAR 앞: Read 뒤: Write → 뒤가 너무 빨리 덮으면 안 됨

WAW 앞: Write 뒤: Write → 쓰기 순서가 바뀌면 안 됨

 

가장 중요한 건:

RAW = 진짜 데이터 의존성
WAR/WAW = 같은 이름을 사용해서 생기는 가짜 의존성

이라는 구분이야.

 

6. Forwarding / Bypassing

RAW Hazard를 해결하는 대표적인 방법.

아까:

ADD R1, R2, R3
SUB R4, R1, R5
 

에서 ADD의 결과는 EX 단계에서 이미 계산됐을 수도 있어.

그런데 register에 실제 기록되는 건 WB 단계야.

ADD

IF → ID → EX → MEM → WB
           ↑          ↑
         값 계산      R1 기록
 

SUB가 굳이 WB까지 기다릴 필요가 있을까?

아니지.

그래서:

ADD의 EX 결과
     ↓
SUB의 EX 입력
 

으로 바로 전달한다.

이게 Forwarding, 또는 Bypassing이야.

ADD EX
 │
 │ forwarding
 ▼
SUB EX
 

즉:

결과를 register file에 쓰고 다시 읽는 것까지 기다리지 않고 CPU 내부에서 바로 전달

하는 최적화야.


7. Stall과 Bubble

그런데 forwarding으로도 해결 못하는 경우가 있어.

대표적인 게 Load-Use Hazard.

LOAD R1, 0(R2)
ADD  R3, R1, R4
 

LOAD는 메모리에서 값을 읽어와야 하니까 R1의 값이 비교적 늦게 나와.

LOAD

IF → ID → EX → MEM → WB
                ↑
              값 나옴
 

그런데 ADD는 EX에서 R1이 필요해.

ADD
     IF → ID → EX
               ↑
             너무 빠름
 

이 경우에는 한 cycle 정도 기다려야 할 수 있어.

그래서:

LOAD   IF ID EX MEM WB
ADD       IF ID ST EX MEM WB
 

중간의 ST가 Stall.

파이프라인에 빈 칸을 넣는 것을 Bubble이라고도 해.

개념적으로:

"잠깐, 앞 명령어 결과 아직 안 나왔어.
 한 사이클 기다려."
 

야.


8. Pipeline Interlock

그럼 누가 Hazard를 발견해서 stall 시키냐?

CPU 안의 Hazard Detection Logic / Pipeline Interlock이 해.

예를 들어:

현재 EX 단계의 destination register = R1
현재 ID 단계의 source register = R1
 

이면 CPU가:

"어? dependency 있네"
 

를 감지해서 forwarding을 하거나 stall을 삽입할 수 있어.

즉 개발자가 직접:

 
NOP
 

을 넣지 않아도 현대 CPU는 대부분 이런 걸 하드웨어적으로 처리해.


9. Control Hazard

Data Hazard와 같이 공부하면 좋은 게 Control Hazard야.

 
CMP R1, R2
BEQ Label
ADD R3, R4, R5
 

CPU 입장에서는 BEQ의 결과가 나오기 전에:

ADD를 실행해야 하나?
Label로 가야 하나?
 

를 모르는 상태가 생겨.

이게 Control Hazard.

해결책 중 하나가 Branch Prediction이야.

"아마 branch 안 할 것 같은데?"
↓
미리 다음 명령 실행
 

맞으면 빠름.

틀리면:

잘못 실행한 명령 취소
↓
Pipeline Flush
↓
올바른 경로 다시 Fetch
 

해야 해서 penalty가 생겨.


10. Structural Hazard

예를 들어 CPU에 메모리 포트가 하나밖에 없다고 해보자.

Instruction Fetch
→ 메모리 사용하고 싶음

동시에

LOAD instruction
→ 메모리 사용하고 싶음
 

둘 다 같은 자원을 동시에 원한다면:

"한 명 기다려"
 

해야 하지.

이게 Structural Hazard.

현대 CPU에서는 하드웨어 자원을 여러 개 두거나 instruction/data cache를 분리하는 방식 등으로 많이 완화해.


11. Out-of-Order Execution

여기부터 현대 CPU랑 연결돼.

예를 들어:

 
LOAD R1, [memory]      ; 오래 걸림
ADD  R2, R1, R3       ; R1 필요 → 기다려야 함

MUL  R5, R6, R7       ; R1과 아무 관계 없음
 

순서대로만 실행하면:

LOAD 기다림
ADD 기다림
MUL도 기다림
 

이 돼.

하지만 MUL은 사실 아무 관련 없잖아.

그래서 CPU가:

LOAD → 기다림
ADD  → dependency 때문에 기다림

MUL → 먼저 실행!
 

할 수 있어.

이게 Out-of-Order Execution이야.

즉:

프로그램 순서와 반드시 동일하게 실행하지 않고, dependency 없는 명령어를 먼저 실행해서 CPU를 놀리지 않는 것.

이때 RAW/WAR/WAW 관리가 훨씬 중요해져.


12. Register Renaming

WAR/WAW를 해결하는 아주 중요한 기술.

예를 들어:

 
MUL R1, R2, R3
ADD R1, R4, R5
 

둘 다 논리적으로 R1을 쓰니까 WAW dependency가 생겼어.

그런데 CPU 내부에는 실제 physical register가 훨씬 많이 있을 수 있어.

그래서:

프로그램의 R1
 

을 내부적으로:

첫 번째 R1 → P10
두 번째 R1 → P17
 

처럼 다른 physical register에 배치해.

 
MUL P10, P2, P3
ADD P17, P4, P5
 

그러면 두 명령어가 더 이상 같은 실제 저장공간에 쓰지 않으니까 WAW 충돌이 사라져.

이게 Register Renaming.

WAR도 같은 방식으로 없앨 수 있어.

그래서:

RAW
→ 진짜 dependency
→ Rename으로 제거 불가능

WAR/WAW
→ 이름 때문에 생긴 dependency
→ Register Renaming으로 제거 가능
 

이게 정말 중요한 시험/면접 포인트야.


13. Scoreboarding / Tomasulo Algorithm

Out-of-Order 쪽으로 들어가면 나오는 대표적인 두 개가 있어.

Scoreboarding은:

이 명령어가 필요한 operand 준비됐나?
Functional Unit 비었나?
다른 instruction과 충돌 없나?
 

같은 상태를 CPU가 표처럼 관리하는 방식.

Tomasulo Algorithm은 여기서 더 발전해서:

Reservation Station
Register Renaming
Common Data Bus
 

같은 구조를 이용해 dependency를 동적으로 처리해.

특히 Tomasulo는 register renaming을 통해 WAR/WAW를 제거하고 RAW dependency가 해결되는 순간 명령어를 실행할 수 있다는 게 핵심이야.

컴퓨터 구조 수업에서 Data Hazard 다음에 Tomasulo가 나왔다면 바로 이 연결 때문이야.


14. Memory Dependency도 있음

레지스터만 dependency가 있는 게 아니야.

 
STORE [A], R1
LOAD  R2, [A]
 

여기서는 memory address A가 dependency를 만들어.

CPU가 LOAD를 STORE보다 먼저 실행하면 잘못된 값을 읽을 수 있어.

그래서 현대 CPU는:

Load Queue
Store Queue
Memory Disambiguation
 

등을 사용해서 메모리 dependency도 추적해.

이건 고급 단계지만 게임 프로그래머 면접에서 CPU 최적화 얘기가 깊어지면 연결될 수 있어.

 

15. 전체 관계를 한 번에 보면

                Pipelining
                    │
                    ▼
                 Hazards
                    │
        ┌───────────┼───────────┐
        ▼           ▼           ▼
      Data        Control    Structural
        │
   ┌────┼────┐
   ▼    ▼    ▼
  RAW  WAR  WAW
   │    │    │
   │    └────┴──────────┐
   │                     ▼
   │              Register Renaming
   │
   ├─ Forwarding
   └─ Stall / Bubble

                    ↓

             Out-of-Order Execution
                    │
          ┌─────────┴─────────┐
          ▼                   ▼
     Scoreboarding          Tomasulo
 

이게 Data Hazard를 중심으로 본 컴퓨터 구조의 큰 그림이야.

 

게임 프로그래밍하고 연결하면 이 지식은 결국 CPU가 왜 “독립적인 데이터에 연속적으로 접근하는 코드”를 좋아하는지 이해하는 기반이 돼.

예를 들어:

for (int i = 0; i < count; ++i)
{
    result[i] = a[i] + b[i];
}
 

처럼 서로 독립적인 계산은 CPU가 pipeline, SIMD, out-of-order execution 등을 활용하기 좋지만,

A 결과가 나와야 B 가능
B 결과가 나와야 C 가능
C 결과가 나와야 D 가능
 

처럼 dependency chain이 길면 CPU가 아무리 빠르더라도 기다려야 하는 시간이 생겨.

그래서 Data Hazard → Dependency → Instruction-Level Parallelism(ILP)까지 이어지는 거야.

면접용으로 아주 짧게 정리하면:

Data Hazard는 파이프라인에서 명령어 간 데이터 의존성 때문에 올바른 실행 순서가 깨질 수 있는 문제입니다. 대표적으로 RAW, WAR, WAW가 있으며 RAW는 실제 데이터 의존성이고 WAR/WAW는 register 이름 때문에 발생하는 name dependency입니다. RAW는 forwarding이나 stall로 처리할 수 있고, WAR/WAW는 out-of-order CPU에서 register renaming으로 제거할 수 있습니다. 현대 CPU는 여기에 branch prediction, register renaming, Tomasulo 등의 기법을 사용해 instruction-level parallelism을 높입니다.

 

 

+ Memory Dependency에 관해서

 

이 부분은 레지스터 의존성보다 한 단계 더 까다로운 이유가 “주소가 실행 중에 계산돼야 해서, 두 메모리 명령이 같은 곳을 건드리는지 CPU가 바로 모를 수 있다”는 데 있어.

예를 들어:

STORE [A], R1   ; 메모리 A에 R1 저장
LOAD  R2, [A]   ; 메모리 A에서 읽어서 R2에 저장
 

프로그램 의미상으로는 반드시:

STORE가 A를 갱신
↓
LOAD가 그 새 값을 읽음
 

이어야 해.

그런데 CPU가 Out-of-Order로 실행하면서 LOAD를 먼저 해버리면:

LOAD가 예전 A 값을 읽음
↓
나중에 STORE가 A를 갱신
 

이 돼서 틀린 결과가 나와.

이게 메모리 의존성의 기본 문제야.


레지스터 의존성과 뭐가 다르냐

레지스터는 비교적 명확해.

 
ADD R1, R2, R3
SUB R4, R1, R5
 

CPU는 decode 단계에서 바로 알 수 있어.

첫 명령어 destination = R1
둘째 명령어 source = R1

→ dependency 있음
 

그런데 메모리는:

 
STORE [RAX + RBX*4], R1
LOAD  R2, [RCX + 16]
 

처럼 주소가 레지스터 계산 결과일 수 있어.

처음에는 CPU가:

[RAX + RBX*4]

[RCX + 16]
 

이 둘이 같은 주소인지 다른 주소인지 바로 모를 수도 있어.

예를 들어 실행 결과가:

RAX + RBX*4 = 0x1000
RCX + 16    = 0x1000
 

이면 같은 메모리야.

반대로:

0x1000
0x9000
 

이면 완전히 독립적이고.

그래서 메모리 의존성 처리가 어렵다.


1. Store Queue

CPU가 STORE를 실행했다고 해서 바로 cache/memory에 써버리는 건 아니야.

개념적으로 Store Queue에 보관할 수 있어.

 
STORE [0x1000], 42
 

그러면:

Store Queue

┌──────────────────────┐
│ Address = 0x1000     │
│ Data    = 42         │
│ 상태 = 아직 commit X │
└──────────────────────┘
 

왜 바로 메모리에 쓰지 않느냐?

Out-of-Order CPU에서는 명령어를 미리 실행했다가 나중에:

branch prediction 틀림
exception 발생
 

같은 이유로 취소해야 할 수도 있기 때문이야.

레지스터 연산은 rename 등을 통해 speculative 상태를 관리할 수 있는데, 메모리를 실제로 바꿔버리면 rollback이 훨씬 까다롭지.

그래서 store는:

계산은 미리 함
↓
주소/값을 Store Queue에 저장
↓
이 명령어가 정말 retire 가능한 게 확정됨
↓
cache/memory 쪽에 반영
 

하는 구조를 사용할 수 있어.

 

Retire = 이 계산 결과를 진짜 프로그램 결과로 확정


2. Load Queue

LOAD도 비슷하게 추적해야 해.

 
LOAD R2, [0x1000]
 

CPU가 실행하면 Load Queue에:

Load Queue

┌──────────────────────┐
│ Address = 0x1000     │
│ 읽은 값 = 42         │
│ 어떤 명령인지         │
└──────────────────────┘
 

같은 정보를 보관할 수 있어.

왜?

나중에 알고 보니:

앞선 STORE가 같은 주소에 있었는데
LOAD가 너무 일찍 읽었음
 

같은 상황이 발견될 수 있기 때문이야.

그러면 CPU가 해당 LOAD와 그 뒤에 의존하는 명령어들을 다시 실행해야 할 수 있어.


3. 가장 중요한 Store-to-Load Forwarding

이건 레지스터의 forwarding과 꽤 비슷해.

 
STORE [A], R1
LOAD  R2, [A]
 

STORE가 아직 cache에 값을 쓰지 않았다고 해보자.

그래도 Store Queue에는 이미:

A → 100
 

이 들어 있을 수 있어.

그러면 LOAD가 굳이 cache까지 내려가서 읽을 필요가 없어.

Store Queue
A = 100
   │
   │ forwarding
   ▼
LOAD
R2 = 100
 

이걸 Store-to-Load Forwarding이라고 해.

즉:

앞선 STORE의 값을 메모리에 실제 반영하기 전에, 뒤의 LOAD에 바로 전달하는 것.

이건 성능상 굉장히 중요해.

예를 들어:

 
int x;
x = 10;
int y = x;
 

같은 흐름에서 매번 실제 cache를 거칠 필요가 없는 거지.


4. Memory Disambiguation

이게 핵심 기술이야.

CPU가 이런 상황을 만났다고 하자.

STORE [RAX], R1
...
LOAD  R2, [RBX]
 

문제는 STORE의 주소가 아직 계산 안 됐는데 LOAD의 주소는 이미 나왔을 수 있다는 거야.

older STORE
주소 = 아직 모름

younger LOAD
주소 = 0x5000
 

CPU 입장에서는 선택지가 두 개야.

보수적으로:

"앞의 STORE 주소를 모르는 이상
LOAD를 기다리게 하자"
 

하면 안전해.

하지만 성능이 떨어져.

왜냐하면 실제로 STORE가:

0x1000
 

에 쓰고 LOAD는:

0x5000
 

을 읽는다면 둘은 아무 관계가 없는데 괜히 기다린 거잖아.

그래서 현대 CPU는 과거 패턴 등을 이용해서:

"이 LOAD는 앞의 STORE와 주소가 겹치지 않을 가능성이 높아."

라고 예측하고 LOAD를 먼저 실행할 수 있어.

이게 Memory Disambiguation / Memory Dependency Prediction의 핵심 아이디어야.


5. 예측이 맞으면

STORE 주소 미확정

LOAD [0x5000]
↓
CPU가 먼저 실행

나중에 STORE 주소 계산
→ 0x1000

서로 다름
 

문제 없음.

CPU는 LOAD를 미리 실행한 덕분에 시간을 절약했어.


6. 예측이 틀리면?

이게 재밌는 부분.

older STORE
주소 = 아직 몰라서 LOAD를 먼저 실행

LOAD [0x5000]
→ 기존 값 10 읽음

나중에 STORE 주소 계산

STORE [0x5000] = 20
 

아차.

프로그램 순서대로라면 LOAD는:

20
 

을 읽어야 했는데 실제로는:

10
 

을 읽었어.

이걸 CPU가 감지하면:

Memory Ordering Violation
 

이 발생했다고 볼 수 있어.

그러면 대략:

잘못 실행한 LOAD
↓
그 LOAD 결과에 의존한 이후 명령
↓
무효화
↓
LOAD부터 다시 실행
 

하게 돼.

일종의 작은 pipeline recovery야.


7. 예제로 보면 훨씬 명확함

 
STORE [RAX], 100
LOAD  R1, [RBX]
ADD   R2, R1, 1
 

처음에:

RAX 주소 계산이 느림

RBX = 0x2000은 이미 앎
 

CPU가 LOAD를 먼저 실행:

Memory[0x2000] = 50

R1 = 50
R2 = 51
 

나중에 STORE 주소가 계산됐는데:

Case A

RAX = 0x1000
 

이면:

STORE → 0x1000
LOAD  → 0x2000

독립
 

문제 없음.

Case B

RAX = 0x2000
 

이면:

STORE → 0x2000
LOAD  → 0x2000

같은 주소!
 

원래 LOAD는 STORE의 새 값을 읽어야 했으니 앞서 실행한:

R1 = 50
R2 = 51
 

은 잘못됐어.

그래서 재실행해야 해.


8. Load Queue와 Store Queue를 합쳐서 LSQ라고도 함

둘을 합쳐:

Load/Store Queue, LSQ

라고 많이 불러.

개념적으로:

              Load/Store Queue

Older
  ↓

STORE #1  Addr=0x1000  Data=10
LOAD  #2  Addr=0x3000
STORE #3  Addr=?       Data=50
LOAD  #4  Addr=0x2000

  ↓
Younger
 

CPU가 이 구조를 보고:

LOAD #4보다 앞선 STORE가 있나?
↓
주소가 같은가?
↓
같으면 forwarding 가능한가?
↓
주소 미확정 STORE가 있나?
↓
LOAD를 먼저 실행해도 될까?
 

등을 판단해.


9. 여기서 "순서"가 왜 중요한가

Out-of-Order CPU라고 해서 프로그램 의미까지 뒤집는 건 아니야.

예를 들어:

 
STORE [A], 10
LOAD  R1, [A]
 

프로그램에서 보장되는 결과가:

R1 = 10
 

이라면 CPU 내부에서는 마음대로 실행 순서를 바꾸더라도 최종적으로는 그렇게 실행한 것처럼 보여야 해.

즉:

내부 실행은 Out-of-Order여도 외부에서 관찰되는 결과는 ISA가 보장하는 규칙을 지켜야 함.

그래서 Load/Store Queue 같은 구조가 필요한 거야.


10. Speculation이랑 연결됨

Memory Disambiguation도 결국 speculation이야.

Branch Prediction에서:

"아마 branch 안 할 거야"
 

라고 예측하고 실행하듯,

메모리에서는:

"이 LOAD는 앞의 STORE와 주소 안 겹칠 거야"
 

라고 예측하고 먼저 실행하는 거야.

그래서:

Branch speculation
→ control dependency 예측

Memory speculation
→ memory dependency 예측
 

이라고 연결해도 돼.

둘 다 맞으면 성능 향상, 틀리면 rollback/replay 비용 발생.


11. CPU가 왜 이렇게까지 하냐

메모리 접근은 느려.

특히 cache miss가 발생하면 수십~수백 cycle을 기다릴 수도 있어.

CPU가:

앞의 STORE 주소 안 나왔으니까
모든 LOAD 다 기다려
 

하면 실행 유닛이 엄청 놀게 돼.

그래서 가능한 LOAD는 먼저 실행해서:

Memory-Level Parallelism
 

을 높이려고 해.

즉 여러 독립적인 메모리 접근을 동시에 진행시키는 거지.


12. 게임 프로그래밍과 어떻게 연결되냐

여기서부터 진짜 실용적인 부분이야.

예를 들어:

 
for (int i = 0; i < N; ++i)
{
    result[i] = positions[i].x + velocities[i].x;
}
 

각 iteration이 독립적이라면 CPU가:

Load positions[0]
Load velocities[0]

Load positions[1]
Load velocities[1]

Load positions[2]
...
 

를 상당히 겹쳐서 처리할 수 있어.

반면:

 
for (int i = 1; i < N; ++i)
{
    data[i] = data[i - 1] + 1;
}
 

는:

data[0]
 ↓
data[1]
 ↓
data[2]
 ↓
data[3]
 

처럼 dependency chain이 있어.

이런 코드는 OoO CPU가 자유롭게 병렬화하기 어렵지.


그리고 aliasing도 중요한 문제야.

 
void Foo(int* a, int* b)
{
    *a = 10;
    int x = *b;
}
 

컴파일러 입장에서는:

a와 b가 같은 주소일 수도 있음
 

을 고려해야 해.

같다면:

*a = 10
↓
*b read
 

순서를 보존해야 하지.

그래서 pointer aliasing은 컴파일러 최적화를 방해할 수도 있어.

C++에서는 특정 상황에서 restrict 계열 확장이나 설계를 통해 alias가 없다는 정보를 줄 수 있고, 데이터 구조를 잘 설계하면 compiler/CPU 최적화가 쉬워져.


13. 멀티스레딩의 Memory Ordering과는 또 다른 층

이것도 자주 섞여.

지금까지 이야기한 건 주로:

하나의 CPU core 내부에서 instruction들이 load/store 순서를 어떻게 처리하는가

에 가까워.

멀티스레딩에서는 더 나아가:

Thread A의 STORE를
Thread B가 언제 볼 수 있느냐?
 

문제가 생겨.

예:

 
// Thread A
data = 100;
ready = true;

// Thread B
if (ready)
    print(data);
 

CPU/compiler reordering까지 고려하면 그냥 이 코드만으로 기대한 순서가 보장되지 않을 수 있어.

그래서:

atomic
memory_order
memory barrier / fence
mutex
 

같은 게 등장해.

즉:

Load/Store Queue
Memory Disambiguation
Store-to-Load forwarding
 

은 CPU 내부 microarchitecture 이야기,

Acquire / Release
Memory Barrier
Atomic
 

은 프로그래머에게 보이는 memory model/동기화 이야기라고 구분하면 좋아.

물론 밑에서는 서로 연결되어 있어.


전체 흐름 한 번에

명령어

STORE [A], value
LOAD  R1, [B]

       ↓

CPU Out-of-Order 실행

       ↓

Load / Store Queue
       │
       ├─ 주소 추적
       ├─ 프로그램 순서 추적
       ├─ 같은 주소인지 검사
       └─ 값 forwarding

       ↓

B가 A와 다르다고 판단/예측
       ↓
LOAD 먼저 실행

       ↓

나중에 STORE 주소 확정

       ├─ 다른 주소
       │     → 그대로 진행
       │
       └─ 같은 주소
             → ordering violation
             → LOAD 및 dependent instructions replay
 

면접에서 설명한다면 이 정도가 딱 좋아:

메모리 의존성은 레지스터 dependency와 달리 load/store의 실제 주소가 실행 단계에서 계산될 수 있기 때문에 CPU가 두 명령어의 의존성을 미리 알기 어렵습니다. 그래서 현대 OoO(Out of Order)CPU는 Load/Store Queue에서 메모리 명령의 주소와 순서를 추적하고, 이전 Store와 같은 주소를 읽는 Load에는 Store-to-Load Forwarding을 사용할 수 있습니다. 주소가 아직 확정되지 않은 Store가 있을 때는 Memory Disambiguation을 통해 Load가 독립적이라고 예측해 먼저 실행할 수도 있고, 이후 같은 주소였음이 확인되면 잘못 실행한 Load와 dependent instruction을 replay합니다. 이를 통해 correctness를 유지하면서 Memory-Level Parallelism을 높입니다.