Lock-free vs Mutex: 로봇 제어 시스템 IPC 성능 벤치마크
"In-Process 통신이 Inter-Process 통신보다 빠르다"는 직관은 틀렸습니다.
성능을 결정하는 것은 프로세스 경계가 아니라 동기화 메커니즘입니다.
이 글에서는 로봇 제어 시스템을 시뮬레이션하는 4단계 파이프라인에서 Mutex 기반과 Lock-free 기반 동기화의 성능을 비교합니다.
결론 먼저
| IPC 방식 | 최적화 전 (Mutex) | 최적화 후 (Lock-free) | 개선 |
|---|---|---|---|
| in_process | 78-103 µs | 0.74-0.82 µs | ~100배 |
| shared_memory | 0.7-0.8 µs | 0.75-0.78 µs | 변화 없음 |
| pipe | 33-106 µs | 68-87 µs | 변화 없음 |
Lock-free 적용 후 in_process와 shared_memory가 동일한 성능을 달성합니다.
테스트 환경
4단계 파이프라인 (로봇 제어 시뮬레이션)
Planner → IK Solver → EtherCAT Master → Mock Hardware
| 단계 | 처리 내용 |
|---|---|
| Planner | Cartesian 궤적 생성 |
| IK Solver | 관절 각도 계산 |
| EtherCAT Master | EtherCAT 프레임 생성 |
| Mock Hardware | 액추에이터 응답 시뮬레이션 |
테스트 조건
| 파라미터 | 값 |
|---|---|
| 주기 | 1ms, 2ms, 4ms, 10ms |
| 테스트 시간 | 10초/테스트 |
| 웜업 | 100 iterations |
| 자유도 | 6 DOF |
| 메시지 크기 | 고정 (동적 할당 없음) |
측정 메트릭
- Cycle Latency: 전체 파이프라인 왕복 시간
- Jitter: 레이턴시 변동
- Percentiles: P50, P95, P99, P99.9
- Deadline Misses: 주기 시간 초과 횟수
왜 Mutex가 느린가?
Mutex의 숨겨진 비용
| 연산 | 레이턴시 |
|---|---|
| 비경쟁 mutex lock | ~25-75 ns |
| 경쟁 mutex lock | ~1-15 µs (커널 futex 호출) |
| Condition variable notify | ~2-10 µs |
| 스레드 웨이크업 | ~10-50 µs (컨텍스트 스위칭) |
| 통신당 총합 | ~50-100 µs |
Mutex 통신 흐름
Thread A Kernel Thread B
│ │ │
├── mutex.lock() ►│ │
│ (futex syscall) │
│◄── acquired ────│ │
│ [critical section] │
├── cv.notify() ──►│── wakeup ─────►│
│ (futex syscall) (scheduling) │
│ │ │
└────────── Total ~50-100µs ────────┘
핵심 문제: 경쟁 상황에서 커널 개입(futex syscall)이 발생합니다.
Lock-free의 원리
Lock-free 비용
| 연산 | 레이턴시 |
|---|---|
atomic_store (release) | ~10-20 ns |
atomic_load (acquire) | ~10-20 ns |
std::this_thread::yield() | ~100-500 ns |
| 커널 개입 | 없음 |
| 통신당 총합 | ~0.7-0.8 µs |
Lock-free 통신 흐름
Thread A Thread B
│ │
├── atomic_store() ──────────────────►│
│ (single CPU instruction, ~10ns) │
│ ├── atomic_load()
│ │ (single CPU instruction)
│ │
└────────── Total ~0.7-0.8µs ─────────┘
핵심 차이: 커널 개입 없이 CPU 명령어만으로 동기화합니다.
x86 Memory Ordering
x86 아키텍처는 강한 메모리 모델을 가지므로:
memory_order_acquire/release에 추가 명령어가 필요 없음- 사실상 제로 오버헤드
상세 벤치마크 결과
최적화 후 (Lock-free)
| IPC 방식 | 주기 | Mean (µs) | P99 (µs) | Jitter (µs) | Misses |
|---|---|---|---|---|---|
| in_process | 1ms | 0.79 | 1.90 | 999.21 | 0 |
| in_process | 2ms | 0.74 | 1.34 | 1999.26 | 0 |
| in_process | 4ms | 0.76 | 1.41 | 3999.24 | 0 |
| in_process | 10ms | 0.82 | 2.30 | 9999.18 | 0 |
| shared_memory | 1ms | 0.78 | 1.29 | 999.22 | 0 |
| shared_memory | 2ms | 0.76 | 1.79 | 1999.24 | 0 |
| shared_memory | 4ms | 0.75 | 1.45 | 3999.25 | 0 |
| shared_memory | 10ms | 0.77 | 1.82 | 9999.23 | 0 |
| pipe | 1ms | 68.92 | 174.00 | 931.08 | 0 |
| pipe | 2ms | 86.62 | 179.07 | 1913.38 | 0 |
| pipe | 4ms | 79.47 | 173.84 | 3920.53 | 0 |
| pipe | 10ms | 82.06 | 174.79 | 9917.94 | 0 |
최종 성능 순위
- in_process (Lock-free): 0.74 µs
- shared_memory (Lock-free): 0.75 µs
- pipe (Kernel IPC): 68.92 µs (~90배 느림)
왜 Lock-free에서 In-Process = Shared Memory인가?
둘 다 동일한 Lock-free Atomic 패턴을 사용합니다:
- 동일한 spin-wait 메커니즘
- 유일한 차이: 메모리 위치 (힙 vs 공유 메모리)
메모리 위치는 성능에 영향을 주지 않습니다.
Lock-free 구현 체크리스트
- 캐시 라인 정렬 (False Sharing 방지)
alignas(64) std::atomic<size_t> writeIdx_; // 64바이트 정렬
alignas(64) std::atomic<size_t> readIdx_;
- 올바른 Memory Ordering (release/acquire 시맨틱스)
buffer_[writeIdx].store(data, std::memory_order_release);
auto data = buffer_[readIdx].load(std::memory_order_acquire);
- 2의 거듭제곱 버퍼 크기 (빠른 모듈로 연산)
constexpr size_t BUFFER_SIZE = 8192; // 2^13
size_t next = (current + 1) & (BUFFER_SIZE - 1); // % 대신 &
- 고정 크기 메시지 (동적 할당 없음)
흔한 구현 실수
| 실수 | 문제 |
|---|---|
| Non-atomic 연산 사용 | 데이터 레이스 |
memory_order_relaxed | 순서 보장 없음 |
| 캐시 라인 정렬 누락 | False Sharing |
아키텍처 선택: In-Process vs Inter-Process
| 기준 | In-Process (Lock-free) | Inter-Process |
|---|---|---|
| 레이턴시 | ~0.7 µs | ~50-200 µs |
| 결정성 | 매우 높음 | 커널 스케줄러 의존 |
| 장애 격리 | 없음 | 프로세스 수준 격리 |
| 장애 동작 | 전체 정지 (Fail-Stop) | 부분 운영 (위험) |
| 복구 방법 | 전체 재시작 | 개별 프로세스 재시작 |
| 메모리 보호 | 없음 (버그가 전체 손상) | 주소 공간 분리 |
| 디버깅 | 쉬움 | 분산 트레이싱 필요 |
로봇 제어에서 Fail-Stop이 중요한 이유
시나리오: IK Solver 프로세스 크래시
| 아키텍처 | 동작 | 결과 |
|---|---|---|
| Inter-Process | 다른 프로세스 계속 실행 | 모터가 오래된 명령으로 계속 동작 → 비제어 상태 |
| In-Process | 전체 프로세스 정지 | 모터 브레이크 자동 체결 → 안전 상태 |
"부분 장애가 완전 정지보다 위험합니다."
권장 아키텍처: 하이브리드
┌─────────────────────────────────────────────────────────┐
│ 실시간 제어 코어 (In-Process, Lock-free) │
│ Planner → IK → EtherCAT → Motor Driver │
│ • 1kHz+ 제어 루프 │
│ • 장애 시 Fail-Stop │
│ • Lock-free Atomic 동기화 │
├─────────────────────────────────────────────────────────┤
│ IPC (Shared Memory / Socket) - Non-realtime │
├─────────────────────────────────────────────────────────┤
│ [Camera] [UI Server] [Logging] [Monitoring] │
│ • 개별 프로세스 장애 허용 │
│ • 독립적 재시작 가능 │
│ • 실시간 코어에 영향 없음 │
└─────────────────────────────────────────────────────────┘
사용 사례별 권장사항
| 시나리오 | 권장 방식 | 이유 |
|---|---|---|
| 1kHz+ 실시간 제어 | In-Process + Lock-free | 최저 레이턴시, Fail-Stop |
| 다중 머신 분산 | Inter-Process + Socket | 네트워크 통신 필요 |
| 프로세스 격리 필요 | Inter-Process + SHM | 보안/안정성 |
| 빠른 프로토타이핑 | In-Process + Mutex | 구현 단순 |
| 프로덕션 로봇 | 하이브리드 | 실시간 코어 + 비실시간 서비스 |
동기화 메커니즘 선택 기준
| 요구사항 | 권장 |
|---|---|
| < 10µs 레이턴시 필요 | Lock-free Atomic (~0.7-1 µs, 높은 복잡도) |
| > 100µs 레이턴시 허용 | Mutex 기반 큐 (~50-100 µs, 단순 구현) |
핵심 정리
-
"성능은 동기화 메커니즘이 결정한다." 프로세스 경계가 아닙니다.
- Mutex 기반 in_process: 78-103 µs
- Lock-free shared_memory: 0.7-0.8 µs
-
Lock-free 적용 시 in_process와 shared_memory는 동일 성능입니다. 둘 다 ~0.7 µs.
-
Mutex의 숨겨진 비용: 경쟁 상황에서 커널 futex 호출로 ~50-100 µs 지연.
-
Lock-free의 핵심: 커널 개입 없이 CPU atomic 명령어만 사용.
-
로봇 제어에서 Fail-Stop이 중요: 부분 장애가 완전 정지보다 위험합니다.
-
하이브리드 아키텍처 권장: 실시간 코어는 In-Process + Lock-free, 비실시간 서비스는 Inter-Process.