이것은 하나의 컴파일러가 일부 대상 시스템에서 원하는 것을 수행하는 코드를 생성하더라도 C ++이 정의되지 않은 동작을 일으키는 데이터 레이스로 정의하는 것입니다. std::atomic
신뢰할 수있는 결과 를 위해 사용해야 하지만 memory_order_relaxed
재정렬에 신경 쓰지 않으면 사용할 수 있습니다 . 를 사용하는 예제 코드 및 asm 출력에 대해서는 아래를 참조하십시오 fetch_add
.
그러나 먼저 질문의 어셈블리 언어 부분은 다음과 같습니다.
num ++는 하나의 명령어 ( add dword [num], 1
)이므로이 경우 num ++이 원자 적이라고 결론 내릴 수 있습니까?
메모리 저장소 명령 (순수 저장소 제외)은 여러 내부 단계에서 발생하는 읽기-수정-쓰기 작업입니다 . 아키텍처 레지스터는 수정되지 않지만 CPU는 ALU를 통해 데이터를 전송하는 동안 내부적으로 데이터를 보유해야합니다 . 실제 레지스터 파일은 가장 간단한 CPU조차도 데이터 저장 장치의 일부에 불과하며 래치는 한 스테이지의 출력을 다른 스테이지의 입력 등으로 유지합니다.
다른 CPU의 메모리 작업은로드와 저장 사이에서 전체적으로 볼 수 있습니다. 즉 add dword [num], 1
, 루프에서 실행 되는 두 개의 스레드 는 서로의 저장소를 밟습니다. ( 좋은 다이어그램은 @Margaret의 답변 을 참조하십시오 ). 두 스레드 각각에서 40k 씩 증가한 후 카운터는 실제 멀티 코어 x86 하드웨어에서 ~ 60k (80k 아님) 만 증가했을 수 있습니다.
불가분의 의미로 그리스어 단어에서 "원자"는 관찰자가 조작을 별도의 단계로 볼 수 없음을 의미 합니다. 모든 비트에 대해 동시에 물리적 / 전기적으로 동시에 발생하는 것은로드 또는 저장에서이를 달성하는 한 가지 방법 일 뿐이지 만 ALU 작업에서는 불가능합니다. x86의 Atomicity에 대한 답변에서 순수한로드와 순수한 상점에 대해 더 자세히 설명 했지만이 답변은 읽기 수정 쓰기에 중점을 둡니다.
lock
프리픽스는 시스템의 모든 가능한 관찰자에 대한 전체 동작 원자 있도록 많은 읽기 - 수정 - 쓰기 (메모리 목적지)의 지시에 적용될 수있다 (다른 코어와 DMA 장치하지 오실로스코프는 CPU 핀에 매여). 그것이 존재하는 이유입니다. ( 이 Q & A 도 참조하십시오 ).
원자도 마찬가지 lock add dword [num], 1
입니다 . 이 명령어를 실행하는 CPU 코어는로드가 캐시에서 데이터를 읽을 때부터 저장소가 결과를 다시 캐시에 커밋 할 때까지 캐시 라인을 개인 L1 캐시에서 수정 된 상태로 고정합니다. 이를 통해 MESI 캐시 일관성 프로토콜 (또는 멀티 코어 AMD / MESIF가 사용하는 MOESI / MESIF 버전의 규칙)에 따라 시스템의 다른 캐시에로드 할 때마다 캐시 라인의 사본이 저장되는 것을 방지합니다. 각각 Intel CPU). 따라서 다른 코어의 작업은 이전이 아닌 이전 또는 이후에 발생하는 것으로 보입니다.
lock
접두어가 없으면 다른 코어가 캐시 라인의 소유권을 가져 와서로드 후, 스토어 전에이를 수정할 수 있으므로로드와 스토어 사이에 다른 스토어가 전체적으로 표시 될 수 있습니다. 다른 몇 가지 대답은이 문제를 lock
해결하고 동일한 캐시 라인의 사본이 충돌 하지 않는다고 주장합니다 . 코 히어 런트 캐시가있는 시스템에서는 이런 일이 발생할 수 없습니다.
( lock
ed 명령이 두 캐시 라인에 걸쳐있는 메모리에서 작동 하는 경우 , 객체의 두 부분에 대한 변경 사항이 모든 관찰자에게 전파 될 때 원자 적으로 유지되도록 관찰하는 데 더 많은 작업이 필요하므로 관찰자가 눈물을 볼 수 없습니다. 데이터가 메모리에 도달 할 때까지 전체 메모리 버스를 잠 가야합니다. 원자 변수를 잘못 정렬하지 마십시오!)
있습니다 lock
접두사는 (같은 전체 메모리 장벽에 명령을집니다 MFENCE 모든 런타임 재정렬 따라서 순차적 일관성을 제공 중지). Jeff Preshing의 우수한 블로그 게시물을 참조하십시오 . 그의 다른 게시물도 모두 우수하며 x86 및 기타 하드웨어 세부 정보에서 C ++ 규칙에 이르기까지 잠금없는 프로그래밍 에 대한 많은 좋은 점을 명확하게 설명합니다 .
단일 프로세서 시스템 또는 단일 스레드 프로세스 에서 단일 RMW 명령어는 실제로 접두사 가 없는 원자 lock
입니다. 다른 코드가 공유 변수에 액세스하는 유일한 방법은 CPU가 컨텍스트 전환을 수행하는 것입니다. 이는 명령 도중에 발생할 수 없습니다. 따라서 일반 dec dword [num]
스레드는 단일 스레드 프로그램과 해당 신호 처리기 또는 단일 코어 시스템에서 실행되는 다중 스레드 프로그램에서 동기화 할 수 있습니다. 보기 다른 질문에 대한 내 대답 하반기 , 그리고 좀 더 자세하게 설명 그 아래 주석을.
C ++로 돌아 가기 :
num++
컴파일러에게 단일 읽기-수정-쓰기 구현으로 컴파일해야한다고 알려주지 않고 사용 하는 것은 가짜입니다 .
;; Valid compiler output for num++
mov eax, [num]
inc eax
mov [num], eax
num
나중에 값을 사용하면 컴파일러가 증가 후에도 레지스터에 그대로 유지됩니다. 따라서 num++
자체 컴파일 방법을 확인하더라도 주변 코드를 변경하면 영향을 줄 수 있습니다.
값이 나중에 필요하지 않은 경우 ( inc dword [num]
바람직하고, 현대의 x86 CPU가 세 개의 별도의 지침을 사용하여 효율적으로 적어도 메모리 대상 RMW 명령을 실행 재미있는 사실 :. gcc -O3 -m32 -mtune=i586
실제로이 방출됩니다 , (펜티엄) P5의 슈퍼 스칼라 파이프 라인 didn를하기 때문에 P6 이상의 마이크로 아키텍처에서와 같이 복잡한 명령어를 여러 개의 간단한 마이크로 연산으로 디코딩하지 않습니다. 자세한 정보는 Agner Fog의 명령어 표 / 마이크로 아키텍처 안내서 를 참조하십시오.x86 많은 유용한 링크에 대한 태그 위키 (PDF로 무료로 제공되는 Intel의 x86 ISA 설명서 포함).
대상 메모리 모델 (x86)을 C ++ 메모리 모델과 혼동하지 마십시오
컴파일 시간 재정렬 이 허용 됩니다. std :: atomic으로 얻는 것의 다른 부분은 컴파일 타임 재정렬을 제어하여num++
다른 작업 후에 만 전 세계적으로 볼 수있도록합니다.
전형적인 예 : 다른 스레드가 볼 수 있도록 일부 데이터를 버퍼에 저장 한 다음 플래그를 설정합니다. x86은 무료로로드 / 릴리스 저장소를 가져 오지만 여전히 컴파일러를 사용하여 순서를 바꾸지 않도록 지시해야 flag.store(1, std::memory_order_release);
합니다.
이 코드가 다른 스레드와 동기화 될 것으로 예상 할 수 있습니다.
// flag is just a plain int global, not std::atomic<int>.
flag--; // This isn't a real lock, but pretend it's somehow meaningful.
modify_a_data_structure(&foo); // doesn't look at flag, and the compilers knows this. (Assume it can see the function def). Otherwise the usual don't-break-single-threaded-code rules come into play!
flag++;
그러나 그렇지 않습니다. 컴파일러는 flag++
함수 호출을 가로 질러 자유롭게 이동할 수 있습니다 (함수가 함수를 인라인하거나 보지 않는 것을 알고있는 경우 flag
). 그런 다음 flag
조차도 아니기 때문에 수정 사항을 완전히 최적화 할 수 있습니다 volatile
. (그리고 더, C는 ++ volatile
표준을위한 유용한 대체 :: 원자. 표준 : 컴파일러는 메모리에 그 값을 가질 수 있도록 않는 원자의 정보는 다음의 제품에 비동기 적으로 유사한 수정할 수 있습니다하지 않습니다 volatile
,하지만 그것보다 훨씬 더있다. 또한, volatile std::atomic<int> foo
한다가 아니라 std::atomic<int> foo
@Richard Hodges와 논의한 바와 동일 )
정의되지 않은 동작으로 비 원자 변수에 대한 데이터 레이스 정의는 컴파일러가 여전히 루프에서로드를로드 및 싱크하고 여러 스레드가 참조 할 수있는 메모리에 대한 다른 많은 최적화를 허용하는 것입니다. ( UB가 컴파일러 최적화를 활성화하는 방법에 대한 자세한 내용은 이 LLVM 블로그 를 참조하십시오 .)
앞에서 언급했듯이 x86 lock
접두사 는 전체 메모리 장벽이므로 num.fetch_add(1, std::memory_order_relaxed);
x86에서 동일한 num++
기본값을 사용하면 (기본값은 순차 일관성) ARM과 같은 다른 아키텍처에서는 훨씬 더 효율적일 수 있습니다. x86에서도 완화는 더 많은 컴파일 타임 재정렬을 허용합니다.
이것이 std::atomic
전역 변수 에서 작동하는 몇 가지 함수에 대해 GCC가 x86에서 실제로 수행하는 작업 입니다.
Godbolt 컴파일러 탐색기 에서 멋진 형식의 소스 + 어셈블리 언어 코드를 참조하십시오 . ARM, MIPS 및 PowerPC를 포함한 다른 대상 아키텍처를 선택하여 해당 대상의 원자에서 어떤 종류의 어셈블리 언어 코드를 얻을 수 있는지 확인할 수 있습니다.
#include <atomic>
std::atomic<int> num;
void inc_relaxed() {
num.fetch_add(1, std::memory_order_relaxed);
}
int load_num() { return num; } // Even seq_cst loads are free on x86
void store_num(int val){ num = val; }
void store_num_release(int val){
num.store(val, std::memory_order_release);
}
// Can the compiler collapse multiple atomic operations into one? No, it can't.
# g++ 6.2 -O3, targeting x86-64 System V calling convention. (First argument in edi/rdi)
inc_relaxed():
lock add DWORD PTR num[rip], 1 #### Even relaxed RMWs need a lock. There's no way to request just a single-instruction RMW with no lock, for synchronizing between a program and signal handler for example. :/ There is atomic_signal_fence for ordering, but nothing for RMW.
ret
inc_seq_cst():
lock add DWORD PTR num[rip], 1
ret
load_num():
mov eax, DWORD PTR num[rip]
ret
store_num(int):
mov DWORD PTR num[rip], edi
mfence ##### seq_cst stores need an mfence
ret
store_num_release(int):
mov DWORD PTR num[rip], edi
ret ##### Release and weaker doesn't.
store_num_relaxed(int):
mov DWORD PTR num[rip], edi
ret
순차 일관성 저장 후에 MFENCE (완전한 장벽)가 필요한지 확인하십시오. x86은 일반적으로 강력하게 정렬되지만 StoreLoad 재정렬은 허용됩니다. 파이프 라인 된 비 순차적 CPU에서 좋은 성능을 위해서는 저장소 버퍼가 필요합니다. 법에 적발 된 Jeff Preshing의 메모리 재정렬 은 실제 하드웨어에서 재정렬이 일어나는 것을 보여주는 실제 코드와 함께 MFENCE를 사용 하지 않은 결과 를 보여줍니다.
Re : std :: atomic num++; num-=2;
연산을 하나의 num--;
명령 으로 병합 하는 컴파일러 에 대한 @Richard Hodges의 답변에 대한 의견 토론 :
동일한 주제에 대한 별도의 Q & A : 컴파일러가 중복 std :: atomic 쓰기를 병합하지 않는 이유는 무엇입니까? 내 답변은 아래에 쓴 내용을 많이 정리합니다.
현재 컴파일러는 실제로 이것을하지 않지만 허용되지 않기 때문에 아닙니다. C ++ WG21 / P0062R1 : 컴파일러는 언제 원자를 최적화해야합니까? 많은 프로그래머들이 컴파일러가 "놀라운"최적화를하지 않을 것이라는 기대와 프로그래머가 제어 할 수있는 표준을 설명합니다. N4455 는이를 포함하여 최적화 할 수있는 많은 예를 설명합니다. 인라인 및 상수 전파는 원래 소스에 명백한 중복 원자 연산이 없었더라도 ( fetch_or(0)
단지 load()
의미를 획득하고 해제 하는) 것으로 전환 할 수있는 것과 같은 것을 도입 할 수 있다고 지적합니다.
컴파일러가 그것을하지 않는 진짜 이유는 (아직) : (1) 컴파일러가 (잘못 실수하지 않고) 안전하게 할 수있는 복잡한 코드를 작성한 사람은 없으며, (2) 잠재적 으로 최소한 의 원칙을 위반하는 것입니다. 놀람 . 잠금이없는 코드는 처음부터 올바르게 작성하기에 충분하지 않습니다. 따라서 원자력 무기를 사용하는 데 부담이되지 마십시오. 싸지 않고 많이 최적화하지도 않습니다. std::shared_ptr<T>
비록 원자 버전이 아니기 때문에 ( 여기서 대답 중 하나가shared_ptr_unsynchronized<T>
gcc 를 정의하는 쉬운 방법을 제공 하지만) 중복 원자 연산을 피하는 것이 항상 쉬운 것은 아닙니다 .
num++; num-=2;
마치 마치 컴파일로 돌아 가기 num--
: is가 아닌 한 컴파일러 는 이 작업을 수행 할 수 있습니다. 재정렬이 가능한 경우, as-if 규칙을 사용하면 컴파일러는 컴파일 타임에 항상 그런 식으로 발생 하도록 결정할 수 있습니다. 관찰자가 중간 값 ( 결과)을 볼 수 있다고 보장하는 것은 없습니다 .num
volatile std::atomic<int>
num++
즉, 이러한 작업 사이에 아무것도 보이지 않는 순서가 소스의 순서 요구 사항과 호환되는 경우 (대상 아키텍처가 아닌 추상 기계의 C ++ 규칙에 따라) 컴파일러 lock dec dword [num]
가 lock inc dword [num]
/ 대신 단일을 방출 할 수 있습니다 lock sub dword [num], 2
.
num++; num--
는 다른 스레드와 동기화 관계가 여전히 있기 때문에 사라질 수 없으며, num
이 스레드에서 다른 작업의 순서를 변경할 수없는 획득로드 및 릴리스 저장소입니다. x86의 경우 lock add dword [num], 0
(즉 num += 0
) 대신 MFENCE로 컴파일 할 수 있습니다 .
PR0062 에서 논의한 바와 같이 , 컴파일 타임에 인접하지 않은 아톰 ops 를보다 적극적으로 병합하는 것은 좋지 않을 수 있습니다 (예 : 진행 카운터는 매번 반복하는 대신 마지막에 한 번만 업데이트 됨). shared_ptr
컴파일러가 shared_ptr
임시의 전체 수명 동안 다른 객체가 존재 함을 증명할 수있는 경우 a의 사본 이 생성 및 소멸 될 때 원자 inc / ref의 카운트가 계산됩니다 .)
num++; num--
하나의 스레드가 즉시 잠금을 해제하고 다시 잠금을 설정하면 병합 조차도 잠금 구현의 공정성을 손상시킬 수 있습니다. 실제로 asm에서 릴리스되지 않으면 하드웨어 중재 메커니즘조차도 다른 스레드가 그 시점에서 잠금을 잡을 수있는 기회를주지 않습니다.
현재 gcc6.2 및 clang3.9를 사용하면 가장 명확하게 최적화 가능한 경우 lock
에도 별도의 작업을 수행 할 수 있습니다 memory_order_relaxed
. ( Godbolt 컴파일러 탐색기 이므로 최신 버전이 다른지 확인할 수 있습니다.)
void multiple_ops_relaxed(std::atomic<unsigned int>& num) {
num.fetch_add( 1, std::memory_order_relaxed);
num.fetch_add(-1, std::memory_order_relaxed);
num.fetch_add( 6, std::memory_order_relaxed);
num.fetch_add(-5, std::memory_order_relaxed);
//num.fetch_add(-1, std::memory_order_relaxed);
}
multiple_ops_relaxed(std::atomic<unsigned int>&):
lock add DWORD PTR [rdi], 1
lock sub DWORD PTR [rdi], 1
lock add DWORD PTR [rdi], 6
lock sub DWORD PTR [rdi], 5
ret
add
원자 라고 했어요 ?