Search
Duplicate
🥕

std::atomic은 느리고 lock-free가 아니다

간단소개
+ race condition은 왜 발생하는가
팔만코딩경 컨트리뷰터
ContributorNotionAccount
주제 / 분류
CS
C++
태그
Scrap
9 more properties
atomic operation을 lock-free라고 부르지만 사실 lock이 없는 건 아니다.
그리고 스핀락을 구현할 때, atomic operation을 계속 실행하도록 구현하는 사례를 많이 보는데. 이거 많이 느리다. 왜 그런지 알아두자.

애초에 Race Condition 문제가 왜 발생하는가?

Cache Coherence가 보장되는 cpu에서 atomic operation이 왜 필요할까. 하나의 operation은 여러 단계로 쪼개어질 수 있다. 예를 들어 i++ 이라는 코드는 다음과 같은 uop으로 쪼개어질 수 있다.
// i++ load eax, [i] add. eax, 1 store [i], eax
Assembly
캐시 일관성 프로토콜은 저기서 “store를 수행할 때” 만 다른 코어들을 잠근다. 그래서 load 실행을 막지 못하기 때문에 Race condition 이 존재한다.

1. Cache Lock

그렇다면 atomic opertion을 구현되려면, 위 세 단계 모두에 대해서 해당 캐시라인을 들고있는 모든 코어들의 캐시버스를 잠궈야 한다. 그렇게 되면 다른 코어들이 전부 방해를 받고 속도가 느려진다. (캐시 일관성 프로토콜도 마찬가지. 거기선 false sharing이란 이름으로 알려져 있음)
+ 위를 보면 알겠지만, 현재 x86의 atomic은 캐시 일관성의 MESI protocol을 사용해 주로 구현된다. MESI protocol이 불가능한 프로세서에서는 메모리 버스 전체를 잠궈버리는 식으로 구현된다.
그래서 atomic operation이 mutex같은 lock이 아니지만 lock을 사용하긴 한다. 물론 lock-free는 종류 구분을 위한거라 맞는 말.

2. Re-ordering limit

atomic operation은 out-of-order 프로세서에서 명령어 reorder가 제대로 수행되지 못한다. 그래서 파이프라이닝을 제대로 수행하지 못해 오는 성능저하가 있다.

3. Write-combined buffer flush (mfence)

std::atomic 은 연산이 끝난 후, 즉시 다른 코어에서 연산결과를 관측할 수 있어야 하기 때문에 mfence 명령을 동반한다. (or lock 접두) 쓰기가 reorder되거나 버퍼링 되면 결과를 곧바로 기록하지 않을 수 있기 때문이다.
이 mfence는 위에서 말한 reodering 을 제한하기도 하고, cpu에는 write-combined buffer라고 하는 버퍼가 있는데 이를 flush해야 한다.
(cpu에서 bus transaction을 열려면 꽤 많은 시간이 들고, 한 번 열리면 burst mode로 캐시라인 단위로 빠르게 전송할 수 있다. 그래서 write할 메모리를 캐시라인 단위로 모아놨다가 버스 개통시 한 번에 flush 한다.)
여기서 flush를 해야 한다면 버스를 마스터링 하는 시간 걸리고, write-combined buffer가 캐시라인 단위로 꽉 채워지지 않았다면 캐시라인 단위로 한번에 전송하지 못하고 워드단위로 하나하나 메모리에 써야한다.

Spin lock 구현할 때 atomic operation 막 돌리지 마라

위에서 서술했듯이 atomic operation을 수행할 때 마다 다른 코어들의 캐시가 계속 잠긴다.
while (atomic_try_lock() == false) { }
C++
이럴 땐, 한 번 atomic으로 try_lock을 검사하고 락을 획득할 수 없다면 x86같은 경우에 pause라는 yield 명령어가 있다. 20-500클럭 정도 대기하는데, 이걸 써서 대기하다가 풀리면 atomic으로 한 번 더 검사하고 그래도 여전히 획득 할 수 없다면 wait상태로 들어가거나 아까 과정을 반복한다.
(pause를 하지 않고 지속적으로 cmp하면 메모리 순서 위반으로 파이프라인을 flush해야돼서 느릴 수 있다는데 그건 잘 모르겠다. Wait for memory pipeline to become empty라고 한다. 그 외에도 기다리는 동안 하이퍼스레드 등으로 다른 작업을 수행하고 있을 수 있다)
while (true) { if (atomic_try_lock()) goto TASK; else __asm pause; // or processorYield() if (atomic_try_lock()) goto TASK; else _wait(); // syscall } TASK:
C++