본문으로 건너뛰기
Embedded Performance Engineering · 37/57

Memory Ordering 분석 — Acquire·Release·Seq-Cst·ARM Relaxed Model

· Hawk · 7분 읽기

#한 줄 요약

“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를 지정해야 성능 이점을 얻습니다.

#6가지 Order의 시각화

relaxed:
A B C atomic operations — 순서 보장 0
acquire:
[load A] | B C ...
↑ 이후 access가 위로 못 감
release:
... B C | [store A]
↑ 이전 access가 아래로 못 감
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 명령

OperationOrder명령
Load relaxedLDR
Load acquireacquireLDAR (ARMv8)
Store relaxedSTR
Store releasereleaseSTLR (ARMv8)
Load seq_cstseq_cstLDAR
Store seq_cstseq_cstSTLR + barrier
CASvariesCASAL, CAS (ARMv8.1+)

ARMv8의 LDARSTLR은 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 설정
ISBpipeline flush, instruction refetchself-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 cycle

x86은 거의 모든 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 hit
Load acquire (LDAR) 5 cycle pipeline drain
Store relaxed (STR) 1 cycle store buffer
Store release (STLR) 5 cycle store buffer drain
CAS (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. 1Embedded Performance Engineering — 임베디드 성능 엔지니어링 시리즈 소개
  2. 2임베디드 성능 분석 방법론 — Measure → Analyze → Optimize 사이클
  3. 3성능 지표 정의 — Latency·Throughput·Utilization 분석
  4. 4성능 측정의 기본 — Wall-Clock·CPU Cycle·Instruction Count
  5. 5성능 데이터 통계적 분석 — Percentile·Histogram·평균의 함정
  6. 6실시간 성능 분석 — WCET·Jitter·Deadline Miss 측정
  7. 7임베디드 벤치마킹 기초 — 재현성·Warmup·노이즈 제거
  8. 8성능 모델링 — Amdahl·Gustafson·Roofline Model 적용
  9. 9프로파일링 기법 개요 — Sampling vs Instrumentation·PGO·LTO
  10. 10CPU 파이프라인 분석 — 5-stage·Cortex-M·Cortex-A 비교
  11. 11Pipeline Stall 분석 — Data·Structural·Control Hazard·Forwarding
  12. 12Branch Prediction 분석 — Static·2-bit·BTB·BHT·Mispredict 비용
  13. 13Speculative Execution 분석 — OoO·Reorder Buffer·Register Renaming
  14. 14CPU Cache 기초 — L1·L2·L3·Set Associative·Replacement Policy
  15. 15Cache Miss 3C Model 분석 — Compulsory·Capacity·Conflict
  16. 16Cache Line 최적화 — Alignment·Prefetch·False Sharing 처리
  17. 17메모리 대역폭 분석 — STREAM·Roofline·Bus Saturation 측정
  18. 18SIMD·NEON 활용 — 128-bit Vector·Auto-Vectorization·SVE/SVE2
  19. 19PMU·HPM 하드웨어 카운터 분석 — 정밀 성능 진단
  20. 20임베디드 Bus Architecture — AHB·AXI·CHI 진화와 5-Channel
  21. 21Bus Contention 진단 — Arbitration·QoS·Starvation 측정
  22. 22DMA 성능 최적화 — Burst·Scatter-Gather·Chain·Cache 일관성
  23. 23DMA vs CPU Copy 성능 비교 — Break-even·Setup Overhead 실측
  24. 24Interrupt Latency 분석 — 진입·종료·Tail-Chaining·Late Arrival
  25. 25Interrupt Storm 처리 — NAPI·Rate-Limit·Polling 전환
  26. 26MMIO 접근 성능 — Cache Policy·Write-Combining·Volatile·Barrier
  27. 27Peripheral Clock 분석 — PLL·Divider·Gating·DVFS
  28. 28Power vs Performance 트레이드오프 — DVFS·Race-to-Idle·Big.LITTLE
  29. 29Thermal Throttling 분석 — Junction Temp·Trip Point·냉각
  30. 30CXL Interconnect 분석 — AI 시대 메모리 대역폭 확장
  31. 31Concurrency 기초 — Concurrency vs Parallelism·Race·Memory Model
  32. 32False Sharing 진단 — Cache Line Ping-Pong·Padding·측정
  33. 33Lock Contention 분석 — Wait·Hold·Convoy·측정 기법
  34. 34Spinlock 성능 분석 — Spin-Wait vs Context Switch·Ticket·MCS
  35. 35Mutex 성능 분석 — Futex·Adaptive·Priority Inheritance
  36. 36Reader-Writer Lock 성능 — Reader/Writer Priority·RCU·Seqlock
  37. 37Lock-Free 자료구조 성능 — CAS·ABA·Hazard Pointer·Epoch Reclamation
  38. 38Memory Ordering 분석 — Acquire·Release·Seq-Cst·ARM Relaxed Model
  39. 39Cache Coherency 프로토콜 — MESI·MOESI·Snoop·Directory
  40. 40SMP 성능 분석 — Per-Core·Affinity·Load Balance·Scalability
  41. 41Linux perf 기초 — stat·record·report 활용
  42. 42Linux perf 고급 — Raw Event·Tracepoint·perf script
  43. 43ftrace 활용 — function·function_graph·latency tracer
  44. 44eBPF·bpftrace 동적 트레이싱 — 커널 무수정 관측
  45. 45Flamegraph 분석 — On-CPU·Off-CPU·Differential
  46. 46ARM DS·Lauterbach 분석 — Hardware Trace 전문 도구
  47. 47Bare-metal 프로파일링 — GPIO·DWT·SysTick·ITM 활용
  48. 48NVIDIA Nsight Systems — GPU·NPU 포함 시스템 분석
  49. 49모던 프로파일러 비교 — Tracy·Hotspot·uftrace·Coz
  50. 50연속 프로파일링 — Parca·Pixie·Pyroscope·Tetragon
  51. 51실전 사례 — ISR Latency 100µs Deadline Miss 추적
  52. 52실전 사례 — Matrix Multiply가 예상의 10배 느린 이유
  53. 53실전 사례 — 8-core가 4-core를 넘으면 throughput이 떨어지는 이유
  54. 54실전 사례 — 카메라 1080p 60fps가 30fps로 떨어지는 이유
  55. 55CXL.mem 지연·대역폭 실측 — Direct·Switch·Pooled 토폴로지 비교
  56. 56CXL 성능 프로파일링 도구 — cxl-cli·DAMON·perf-mem 활용
  57. 57실전 사례 — CXL.mem 추가로 LLM inference KV cache 처리량 회복