Memory Ordering 분석 — Acquire·Release·Seq-Cst·ARM Relaxed Model
#한 줄 요약
“Memory ordering은 다른 thread가 메모리 접근을 어떤 순서로 관찰하는지를 결정하며 CPU와 컴파일러의 재정렬을 통제합니다.”
#어떤 문제를 푸는가
현대 CPU는 out-of-order execution과 store buffer를 사용해 명령을 순서와 다르게 실행합니다. 단일 thread에서는 결과가 같아 보이지만, 다른 thread가 메모리를 보면 코드 순서와 다른 순서로 관찰할 수 있습니다. 컴파일러도 같은 종류의 재정렬을 합니다.
Lock 기반 코드에서는 lock 내부의 fence가 이 문제를 가려 줍니다. Lock-free 코드에서는 직접 ordering을 통제해야 하며, 잘못하면 producer가 데이터를 쓰기 전에 consumer가 flag를 보는 race가 발생합니다.
이 글에서는 C11/C++11의 6가지 memory order를 정리하고, acquire-release pair가 어떻게 synchronization point를 만드는지, 그리고 ARM과 x86의 실제 비용 차이를 살펴봅니다.
#C11/C++11 Memory Order
#include <stdatomic.h>
memory_order_relaxed /* 순서 무관, atomic만 보장 */memory_order_consume /* dependency만, deprecated */memory_order_acquire /* 이후 access가 위로 못 감 */memory_order_release /* 이전 access가 아래로 못 감 */memory_order_acq_rel /* 양쪽 */memory_order_seq_cst /* 모든 thread가 같은 순서로 관찰 */가장 비싼 것이 seq_cst이며 기본값이기도 합니다. 의도적으로 약한 order를 지정해야 성능 이점을 얻습니다.
#각 Order가 막는 방향
- relaxed — atomic 연산
A,B,C사이에 순서 보장이 전혀 없습니다. atomicity만 지켜집니다. - acquire —
load A뒤에 오는 access가 그 load보다 위로 올라가지 못합니다. - release —
store A앞에 있는 access가 그 store보다 아래로 내려가지 못합니다. - seq_cst — 모든 atomic 연산에 대해 global total order가 존재합니다.
Acquire와 release는 한쪽 방향만 막는 half-fence입니다. 두 개가 짝을 이루어야 synchronization이 성립합니다.
#Acquire-Release Pair
/* Producer */data = compute();atomic_store_explicit(&ready, true, memory_order_release);
/* Consumer */if (atomic_load_explicit(&ready, memory_order_acquire)) { use(data); /* data 변경이 가시화 보장됨 */}Release store와 acquire load가 같은 변수에 대해 짝을 이루면 synchronization point가 형성됩니다. Release 이전의 모든 write가 acquire 이후의 모든 read에서 관찰 가능해집니다.
이 pattern이 lock-free 자료구조의 핵심 building block입니다.
#Seq-Cst — Sequential Consistency
/* Dekker's algorithm */atomic_store(&flag1, true, seq_cst);if (!atomic_load(&flag2, seq_cst)) enter_critical();
atomic_store(&flag2, true, seq_cst);if (!atomic_load(&flag1, seq_cst)) enter_critical();Dekker나 Peterson 알고리즘은 acquire-release만으로는 깨집니다. 두 thread 모두 critical section에 들어갈 수 있는 race가 존재합니다.
seq_cst는 모든 atomic 연산에 글로벌 total order를 부여하므로 이런 알고리즘이 동작합니다. 비싼 만큼 확실한 보장을 줍니다.
#ARM 명령
| Operation | Order | 명령 |
|---|---|---|
| Load relaxed | — | LDR |
| Load acquire | acquire | LDAR (ARMv8) |
| Store relaxed | — | STR |
| Store release | release | STLR (ARMv8) |
| Load seq_cst | seq_cst | LDAR |
| Store seq_cst | seq_cst | STLR + barrier |
| CAS | varies | CASAL, CAS (ARMv8.1+) |
ARMv8의 LDAR과 STLR은 atomic과 half-fence를 single instruction으로 처리하므로 매우 효율적입니다. ARMv7 시절에는 atomic 명령에 DMB를 별도로 붙여야 했습니다.
#DMB, DSB, ISB
__DMB(); /* Data Memory Barrier */__DSB(); /* Data Synchronization Barrier */__ISB(); /* Instruction Synchronization */| Barrier | 의미 | 사용처 |
|---|---|---|
| DMB | 이전 memory access 완료 후 이후 access 진행 | acquire/release 보강 |
| DSB | 모든 memory와 이후 명령 완료까지 대기 | clock 변경, MPU 설정 |
| ISB | pipeline flush, instruction refetch | self-modifying code |
ARMv7에서는 atomic + DMB 조합으로 acquire-release를 구현했습니다. ARMv8에서는 LDAR/STLR이 그 역할을 합니다.
#DMB Variant
__DMB(); /* Full system */__DMB_ISH(); /* Inner Shareable, same cluster */__DMB_NSH(); /* Non-shareable, same CPU */__DMB_OSH(); /* Outer Shareable, across clusters */좁은 scope일수록 빠릅니다. SMP cluster 내부에만 영향을 주는 경우는 DMB ISH로 충분합니다. 멀티 cluster나 GPU와의 동기화에만 OSH가 필요합니다.
#x86 vs ARM — 비용 비교
x86 (strong ordering): Load relaxed/acquire 1 cycle Store relaxed/release 1 cycle Store seq_cst (MFENCE) 10 cycle CAS (LOCK CMPXCHG) 20 cycle
ARM (weak ordering): Load relaxed 1 cycle LDAR (acquire) 5 cycle STLR (release) 5 cycle Store seq_cst 5 cycle (LDAR/STLR과 동일) DMB ISH 20-50 cyclex86은 거의 모든 load와 store가 자동으로 acquire와 release 의미를 가지므로 비용이 거의 무료입니다. ARM은 명시적으로 acquire/release를 지정해야 하지만 그 비용이 명확히 보입니다.
이 차이 때문에 x86에서 잘 동작하던 코드가 ARM에서 race를 일으키는 경우가 자주 발생합니다. ARM에서는 항상 의도된 memory order를 명시해야 합니다.
#Release-Consume Pattern (deprecated)
/* Consumer */ptr = atomic_load_explicit(&shared, memory_order_consume);use(ptr->field); /* data dependency만 */consume은 acquire보다 가벼운 ordering으로, pointer dependency가 있는 access에만 ordering을 적용합니다.
문제는 컴파일러가 dependency를 정확히 추적하기 어려워 대부분 acquire로 fallback한다는 점입니다. C++17에서 deprecated 되었으며 새 코드에서는 acquire를 사용합니다.
#Lock-Free Queue에 적용
/* SPSC push */data[h] = value;atomic_store_explicit(&head, h + 1, memory_order_release); /* data write가 head 갱신 전에 가시화 */
/* SPSC pop */size_t h = atomic_load_explicit(&head, memory_order_acquire); /* data read가 head 관찰 후에 진행 */return data[t];Release-acquire pair만으로 충분하며 seq_cst는 필요 없습니다. Lock-free 자료구조에서 가장 흔한 ordering 조합입니다.
#Linux Kernel — smp_rmb, smp_wmb
smp_rmb(); /* read memory barrier */smp_wmb(); /* write memory barrier */smp_mb(); /* full barrier */Linux는 architecture별로 적절한 명령을 emit하는 wrapper를 제공합니다. Modern 코드에서는 smp_load_acquire(p)와 smp_store_release(p, v)를 사용해 C11 atomic과 비슷한 스타일로 작성합니다.
#Cortex-M Single Core
Cortex-M3/M4 같은 단일 코어 시스템에서는 thread 간 memory ordering이 거의 무의미합니다. Pipeline은 in-order이고 store buffer가 thread 간에 영향을 주지 않습니다.
DMB가 필요한 경우는 DMA나 MMIO와의 ordering입니다.
fill_buf();__DMB(); /* memory write 완료 보장 */DMA->CR = DMA_START;CPU가 buffer에 쓴 데이터가 메모리에 도달한 뒤에 DMA를 시작해야 하므로 DMB가 필요합니다.
#Dual-Core Cortex-M
RP2040의 dual Cortex-M0+나 Cortex-M55+M85 cluster에서는 cross-core memory ordering이 필요합니다.
/* Core 0 → Core 1 통신 */atomic_store_explicit(&shared, value, memory_order_release);
/* Core 1 */val = atomic_load_explicit(&shared, memory_order_acquire);Single core 시절의 가정이 더 이상 통하지 않으므로 lock-free IPC를 구현할 때는 ordering을 명시해야 합니다.
#자주 보는 함정과 안티패턴
⚠️ volatile을 atomic으로 가정
volatile int x;x++; /* read-modify-write, atomic이 아님 */volatile은 컴파일러에게 최적화를 막아 달라는 hint일 뿐 atomic과 ordering을 보장하지 않습니다. 두 개념은 독립적입니다.
⚠️ 기본 seq_cst의 비용 무시
atomic_int counter = 0;counter++; /* default seq_cst — 비쌈 */Counter 누적처럼 ordering이 필요 없는 경우에는 memory_order_relaxed를 명시해야 합니다.
⚠️ Store에 acquire, load에 release
atomic_store(&flag, 1, memory_order_acquire); /* invalid */Store는 release만, load는 acquire만 의미가 있습니다. acq_rel은 read-modify-write 연산에서만 의미가 있습니다.
⚠️ Barrier만 추가하고 non-atomic 변수 사용
*ptr_a = 1;__DMB();*ptr_b = 1;Barrier는 이미 atomic으로 발행된 access의 순서만 통제합니다. Non-atomic 변수에 대한 동시 접근은 여전히 race입니다.
#측정 — 실측 비용
Cortex-A72에서 측정한 단일 atomic 연산 비용입니다.
Latency 비고Load relaxed (LDR) 1 cycle cache hitLoad acquire (LDAR) 5 cycle pipeline drainStore relaxed (STR) 1 cycle store bufferStore release (STLR) 5 cycle store buffer drainCAS (CASAL) 15 cycle contention 없을 때DMB ISH 30 cycle cluster 내DMB OSH 80 cycle cluster 간ISB 40 cycle pipeline flush같은 cluster 내 DMB는 30 cycle이지만 cluster를 넘는 OSH는 80 cycle로 두 배 이상입니다. Scope를 좁히는 것이 중요합니다.
#정리
- Memory order는 relaxed, acquire, release, acq_rel, seq_cst로 강도가 올라갑니다.
- Acquire-release pair가 lock-free의 가장 흔한 synchronization 패턴입니다.
- Seq-cst는 Dekker나 Peterson 같은 알고리즘에 필요하며 비용이 가장 비쌉니다.
- ARMv8의 LDAR과 STLR은 atomic과 half-fence를 single instruction으로 결합합니다.
- DMB scope를 좁히면 비용이 줄어듭니다.
- Cortex-M single core에서는 DMA와 MMIO에 대한 ordering이 주 사용처입니다.
다음 편은 Cache Coherency — MESI 프로토콜과 멀티코어 동기화입니다.
#관련 항목
Embedded Performance Engineering · 38 of 57
- 1 Embedded Performance Engineering — 임베디드 성능 엔지니어링 시리즈 소개
- 2 임베디드 성능 분석 방법론 — Measure → Analyze → Optimize 사이클
- 3 성능 지표 정의 — Latency·Throughput·Utilization 분석
- 4 성능 측정의 기본 — Wall-Clock·CPU Cycle·Instruction Count
- 5 성능 데이터 통계적 분석 — Percentile·Histogram·평균의 함정
- 6 실시간 성능 분석 — WCET·Jitter·Deadline Miss 측정
- 7 임베디드 벤치마킹 기초 — 재현성·Warmup·노이즈 제거
- 8 성능 모델링 — Amdahl·Gustafson·Roofline Model 적용
- 9 프로파일링 기법 개요 — Sampling vs Instrumentation·PGO·LTO
- 10 CPU 파이프라인 분석 — 5-stage·Cortex-M·Cortex-A 비교
- 11 Pipeline Stall 분석 — Data·Structural·Control Hazard·Forwarding
- 12 Branch Prediction 분석 — Static·2-bit·BTB·BHT·Mispredict 비용
- 13 Speculative Execution 분석 — OoO·Reorder Buffer·Register Renaming
- 14 CPU Cache 기초 — L1·L2·L3·Set Associative·Replacement Policy
- 15 Cache Miss 3C Model 분석 — Compulsory·Capacity·Conflict
- 16 Cache Line 최적화 — Alignment·Prefetch·False Sharing 처리
- 17 메모리 대역폭 분석 — STREAM·Roofline·Bus Saturation 측정
- 18 SIMD·NEON 활용 — 128-bit Vector·Auto-Vectorization·SVE/SVE2
- 19 PMU·HPM 하드웨어 카운터 분석 — 정밀 성능 진단
- 20 임베디드 Bus Architecture — AHB·AXI·CHI 진화와 5-Channel
- 21 Bus Contention 진단 — Arbitration·QoS·Starvation 측정
- 22 DMA 성능 최적화 — Burst·Scatter-Gather·Chain·Cache 일관성
- 23 DMA vs CPU Copy 성능 비교 — Break-even·Setup Overhead 실측
- 24 Interrupt Latency 분석 — 진입·종료·Tail-Chaining·Late Arrival
- 25 Interrupt Storm 처리 — NAPI·Rate-Limit·Polling 전환
- 26 MMIO 접근 성능 — Cache Policy·Write-Combining·Volatile·Barrier
- 27 Peripheral Clock 분석 — PLL·Divider·Gating·DVFS
- 28 Power vs Performance 트레이드오프 — DVFS·Race-to-Idle·Big.LITTLE
- 29 Thermal Throttling 분석 — Junction Temp·Trip Point·냉각
- 30 CXL Interconnect 분석 — AI 시대 메모리 대역폭 확장
- 31 Concurrency 기초 — Concurrency vs Parallelism·Race·Memory Model
- 32 False Sharing 진단 — Cache Line Ping-Pong·Padding·측정
- 33 Lock Contention 분석 — Wait·Hold·Convoy·측정 기법
- 34 Spinlock 성능 분석 — Spin-Wait vs Context Switch·Ticket·MCS
- 35 Mutex 성능 분석 — Futex·Adaptive·Priority Inheritance
- 36 Reader-Writer Lock 성능 — Reader/Writer Priority·RCU·Seqlock
- 37 Lock-Free 자료구조 성능 — CAS·ABA·Hazard Pointer·Epoch Reclamation
- 38 Memory Ordering 분석 — Acquire·Release·Seq-Cst·ARM Relaxed Model
- 39 Cache Coherency 프로토콜 — MESI·MOESI·Snoop·Directory
- 40 SMP 성능 분석 — Per-Core·Affinity·Load Balance·Scalability
- 41 Linux perf 기초 — stat·record·report 활용
- 42 Linux perf 고급 — Raw Event·Tracepoint·perf script
- 43 ftrace 활용 — function·function_graph·latency tracer
- 44 eBPF·bpftrace 동적 트레이싱 — 커널 무수정 관측
- 45 Flamegraph 분석 — On-CPU·Off-CPU·Differential
- 46 ARM DS·Lauterbach 분석 — Hardware Trace 전문 도구
- 47 Bare-metal 프로파일링 — GPIO·DWT·SysTick·ITM 활용
- 48 NVIDIA Nsight Systems — GPU·NPU 포함 시스템 분석
- 49 모던 프로파일러 비교 — Tracy·Hotspot·uftrace·Coz
- 50 연속 프로파일링 — Parca·Pixie·Pyroscope·Tetragon
- 51 실전 사례 — ISR Latency 100µs Deadline Miss 추적
- 52 실전 사례 — Matrix Multiply가 예상의 10배 느린 이유
- 53 실전 사례 — 8-core가 4-core를 넘으면 throughput이 떨어지는 이유
- 54 실전 사례 — 카메라 1080p 60fps가 30fps로 떨어지는 이유
- 55 CXL.mem 지연·대역폭 실측 — Direct·Switch·Pooled 토폴로지 비교
- 56 CXL 성능 프로파일링 도구 — cxl-cli·DAMON·perf-mem 활용
- 57 실전 사례 — CXL.mem 추가로 LLM inference KV cache 처리량 회복
관련 글
실전 사례 — CXL.mem 추가로 LLM inference KV cache 처리량 회복
70B 모델 KV cache가 HBM 한계를 넘어 throughput이 무너졌을 때, CXL.mem 256 GB pool 추가로 회복한 실전 케이스.
같은 시리즈에서 이어 읽기
CXL 성능 프로파일링 도구 — cxl-cli·DAMON·perf-mem 활용
CXL.mem 환경 성능 도구 — cxl-cli 토폴로지·DAMON page activity·perf-mem로 보는 CXL 트래픽·numastat 통계.
같은 시리즈에서 이어 읽기
CXL.mem 지연·대역폭 실측 — Direct·Switch·Pooled 토폴로지 비교
CXL.mem 토폴로지별 실측 — Direct attach·Single switch·Multi-host pool의 지연·대역폭 비용 측정.
같은 시리즈에서 이어 읽기