메모리 주문 "획득"과 "소비"는 어떻게 다르며 언제 "소비"가 선호됩니까?
C ++ 11 표준은 대략 "순차적으로 일관된", "획득", "소비", "릴리스"및 "완화"인 메모리 순서 를 포함하는 메모리 모델 (1.7, 1.10)을 정의합니다 . 마찬가지로 대략적으로 프로그램은 인종이없는 경우에만 옳습니다. 모든 동작이 한 동작이 다른 동작 보다 먼저 발생 하는 순서로 배치 될 수있는 경우 발생합니다 . 작업 X가 작업 Y 이전에 발생 하는 방식 은 X 가 Y 이전에 (하나의 스레드 내에서 ) 시퀀스 되거나 X 가 Y 이전에 스레드 간 발생하는 것 입니다. 후자의 조건은 무엇보다도 다음과 같은 경우에 주어집니다.
- X는 과 동기화 Y , 또는
- X 는 Y 이전에 종속성 순서가 있습니다.
Synchronizing-with 는 X 가 일부 원자 변수에 대해 "release"순서가있는 원자 저장소이고 Y 가 동일한 변수에 대해 "acquire"순서가있는 원자로드 일 때 발생 합니다. 인 종속성-전에 주문이 유사한 상황이 발생 Y는 부하가 "소비"함께 주문 (적당한 메모리 액세스). 동기화와 동기화 의 개념은 스레드 내에서 순서 가 지정되는 작업간에 전 이적으로 발생-전 관계 를 확장 하지만, 이전 에 종속성이 지정 되는 것은 호출 전 시퀀스 의 엄격한 하위 집합을 통해서만 전 이적으로 확장됩니다.carry-dependency 는 거대한 규칙 세트를 따르며 특히 std::kill_dependency
.
그렇다면 "종속성 순서"개념의 목적은 무엇입니까? 더 단순한 순서 전 / 동기화와 비교하여 어떤 이점을 제공 합니까? 규칙이 더 엄격하기 때문에 더 효율적으로 구현할 수 있다고 가정합니다.
릴리스 / 획득에서 릴리스 / 소비로 전환하는 것이 정확하고 사소하지 않은 이점을 제공하는 프로그램의 예를 제공 할 수 있습니까? 그리고 언제 std::kill_dependency
개선을 제공할까요? 높은 수준의 주장은 좋지만 하드웨어 별 차이점에 대한 보너스 포인트입니다.
N2492 는 다음과 같은 근거 로 데이터 종속성 순서를 도입했습니다 .
현재 작업 초안 (N2461)이 일부 기존 하드웨어에서 가능한 확장 성을 지원하지 않는 두 가지 중요한 사용 사례가 있습니다.
- 거의 작성되지 않은 동시 데이터 구조에 대한 읽기 액세스
거의 작성되지 않은 동시 데이터 구조는 운영 체제 커널과 서버 스타일 애플리케이션 모두에서 매우 일반적입니다. 예를 들어 외부 상태 (예 : 라우팅 테이블), 소프트웨어 구성 (현재로드 된 모듈), 하드웨어 구성 (현재 사용중인 스토리지 장치) 및 보안 정책 (액세스 제어 권한, 방화벽 규칙)을 나타내는 데이터 구조가 있습니다. 10 억 대 1을 훨씬 초과하는 읽기-쓰기 비율은 매우 일반적입니다.
- 포인터 매개 게시를위한 게시-구독 의미 체계
스레드 간의 대부분의 통신은 포인터를 통해 이루어지며, 여기서 생산자는 소비자가 정보에 액세스 할 수있는 포인터를 게시합니다. 해당 데이터에 대한 액세스는 완전한 획득 의미없이 가능합니다.
이러한 경우 스레드 간 데이터 종속성 순서 지정을 사용하면 스레드 간 데이터 종속성 순서 지정 을 지원하는 시스템에서 크기 순서 속도가 빨라지고 확장 성이 비슷하게 향상되었습니다. 이러한 기계는 값 비싼 잠금 획득, 원자 적 명령 또는 달리 필요한 메모리 펜스를 피할 수 있기 때문에 이러한 속도 향상이 가능합니다.
내 강조
거기에 제시된 동기 부여 사용 사례는 rcu_dereference()
Linux 커널에서 가져온 것입니다.
Load-consume은 load-acquire와 매우 유사하지만, load-consume에 따라 데이터에 의존하는 식 평가에만 발생 전 관계를 유도한다는 점이 다릅니다. 표현식을 래핑하면 kill_dependency
더 이상로드 소비에서 종속성을 전달하지 않는 값이 생성됩니다.
주요 사용 사례는 작성자가 데이터 구조를 순차적으로 생성 한 다음 공유 포인터를 새 구조로 스윙하는 것입니다 ( release
또는 acq_rel
원자 사용). 리더는 load-consume을 사용하여 포인터를 읽고 역 참조를 통해 데이터 구조를 얻습니다. 역 참조는 데이터 종속성을 생성하므로 독자는 초기화 된 데이터를 볼 수 있습니다.
std::atomic<int *> foo {nullptr};
std::atomic<int> bar;
void thread1()
{
bar = 7;
int * x = new int {51};
foo.store(x, std::memory_order_release);
}
void thread2()
{
int *y = foo.load(std::memory_order_consume)
if (y)
{
assert(*y == 51); //succeeds
// assert(bar == 7); //undefined behavior - could race with the store to bar
// assert(kill_dependency(*y) + bar == 58) // undefined behavior (same reason)
assert(*y + bar == 58); // succeeds - evaluation of bar pulled into the dependency
}
}
부하 소비를 제공하는 데는 두 가지 이유가 있습니다. 주된 이유는 ARM 및 전력 부하가 소비되도록 보장되지만이를 획득으로 전환하려면 추가 펜싱이 필요하기 때문입니다. (x86에서는 모든로드가 획득되므로 소비는 순진한 컴파일에서 직접적인 성능 이점을 제공하지 않습니다.) 두 번째 이유는 컴파일러가 데이터 의존성없이 이후 작업을 소비 전까지 이동할 수 있기 때문에 획득을 위해 수행 할 수 없습니다. . (이러한 최적화를 활성화하는 것이이 모든 메모리 순서를 언어로 구축하는 큰 이유입니다.)
값을 래핑 kill_dependency
하면로드 소비 전에 이동할 값에 따라 달라지는 식을 계산할 수 있습니다. 예를 들어 값이 이전에 읽은 배열의 인덱스 일 때 유용합니다.
소비를 사용하면 더 이상 전 이적이지 않은 사전 발생 관계가 생성됩니다 (비순환이 보장되지만). 예를 들어 to bar
store는 foo에 대한 저장소 이전 에 발생하며 y
, 이는 bar
(주석 처리 된 assert에서) 읽기 전에 발생하는 의 역 참조 전에 발생 하지만 to 저장소 bar
는의 읽기 전에 발생하지 않습니다 bar
. 이로 인해 발생 전의 정의가 다소 복잡해 지지만 작동 방식을 상상할 수 있습니다 (시퀀싱 된 전으로 시작하여 원하는 수의 release-consume-dataDependency 또는 release-acquire-sequencedBefore 링크를 통해 전파).
Jeff Preshing은이 질문에 대한 훌륭한 블로그 게시물을 가지고 있습니다. 직접 추가 할 수는 없지만 소비와 획득에 대해 궁금해하는 사람은 자신의 게시물을 읽어야한다고 생각합니다.
http://preshing.com/20140709/the-purpose-of-memory_order_consume-in-cpp11/
그는 세 가지 다른 아키텍처에서 해당 벤치 마크 어셈블리 코드와 함께 특정 C ++ 예제를 보여줍니다. 와 비교할 때 memory_order_acquire
, memory_order_consume
잠재적으로 PowerPC에서 3 배 속도 향상, ARM에서 1.6 배 속도 향상, 어쨌든 강력한 일관성을 가진 x86에서 무시할 수있는 속도 향상을 제공합니다. 잡힌 점은 그가 작성했을 때 실제로 처리 된 GCC만이 획득과 다르게 의미론을 소비하며 아마도 버그 때문일 것입니다. 그럼에도 불구하고 컴파일러 작성자가이를 활용하는 방법을 알아낼 수 있다면 속도 향상이 가능함을 보여줍니다.
실제 답변이 아니고 적절한 답변에 대한 큰 현상금이 없다는 것을 의미하지는 않지만 부분적인 결과를 기록하고 싶습니다.
After staring at 1.10 for a while, and in particular the very helpful note in paragraph 11, I think this isn't actually so hard. The big difference between synchronizes-with (henceforth: s/w) and dependency-ordered-before (dob) is that a happens-before relationship can be established by concatenating sequenced-before (s/b) and s/w arbitrarily, but not so for dob. Note one of the definitions for inter-thread happens before:
A
synchronizes-withX
andX
is sequenced beforeB
But the analogous statement for is missing!A
is dependency-ordered before X
So with release/acquire (i.e. s/w) we can order arbitrary events:
A1 s/b B1 Thread 1
s/w
C1 s/b D1 Thread 2
But now consider an arbitrary sequence of events like this:
A2 s/b B2 Thread 1
dob
C2 s/b D2 Thread 2
In this sequenece, it is still true that A2
happens-before C2
(because A2
is s/b B2
and B2
inter-thread happens before C2
on account of dob; but we could argue that you can never actually tell!). However, it is not true that A2
happens-before D2
. The events A2
and D2
are not ordered with respect to one another, unless it actually holds that C2
carries dependency to D2
. This is a stricter requirement, and absent that requirement, A2
-to-D2
cannot be ordered "across" the release/consume pair.
In other words, a release/consume pair only propagates an ordering of actions which carry a dependency from one to the next. Everything that's not dependent is not ordered across the release/consume pair.
Furthermore, note that the ordering is restored if we append a final, stronger release/acquire pair:
A2 s/b B2 Th 1
dob
C2 s/b D2 Th 2
s/w
E2 s/b F2 Th 3
Now, by the quoted rule, D2
inter-thread happens before F2
, and therefore so do C2
and B2
, and so A2
happens-before F2
. But note that there is still no ordering between A2
and D2
— the ordering is only between A2
and later events.
In summary and in closing, dependency carrying is a strict subset of general sequencing, and release/consume pairs provide an ordering only among actions that carry dependency. As long as no stronger ordering is required (e.g. by passing through a release/acquire pair), there is theoretically a potential for additional optimization, since everything that is not in the dependency chain may be reordered freely.
Maybe here is an example that makes sense?
std::atomic<int> foo(0);
int x = 0;
void thread1()
{
x = 51;
foo.store(10, std::memory_order_release);
}
void thread2()
{
if (foo.load(std::memory_order_acquire) == 10)
{
assert(x == 51);
}
}
As written, the code is race-free and the assertion will hold, because the release/acquire pair orderes the store x = 51
before the load in the assertion. However, by changing "acquire" into "consume", this would no longer be true and the program would have a data race on x
, since x = 51
carries no dependency into the store to foo
. The optimization point is that this store can be reordered freely without concern to what foo
is doing, because there is no dependency.
'development' 카테고리의 다른 글
Python 사전 액세스 코드 최적화 (0) | 2020.12.09 |
---|---|
Xcode 작업 공간에서 프로젝트 간의 종속성을 어떻게 관리해야합니까? (0) | 2020.12.09 |
문제 : Bob의 판매 (0) | 2020.12.09 |
응용 프로그램 풀 ID를 사용하는 IIS 응용 프로그램에서 기본 토큰이 손실됩니까? (0) | 2020.12.09 |
300GB 메모리로 제한되는 64 비트 JVM? (0) | 2020.12.09 |