개요
XBOX One과 XBOX Series X|S 비교에서 다룬 기반 내용을 확장하여, 이 문서에서는 프로세서 코어 간에 공유되는 메모리에 대한 다양한 읽기/쓰기 작업과 관련된 성능 문제와 비용을 다룹니다. XBOX One 계열과 XBOX Series 콘솔 CPU의 아키텍처, 기능 및 성능에 대한 개괄이 필요하다면 위에서 언급한 문서를 먼저 살펴보시길 권장합니다. 소개 공유 데이터 데이터 공유의 비용 권장 사항 부록: 코드소개
XBOX One과 XBOX Series X|S 비교에서는 L1 캐시 미스와 관련된 비용을 설명합니다. L1 캐시 미스가 발생하는 가장 흔한 패턴 중 하나는 CPU 코어 간에 공유되는 주소에 쓰기를 수행하는 것입니다. 예를 들어 여러 스레드 간에 공유되는 데이터가 있습니다. 이 문서의 목적은 코어 간에 쓰기 가능한 데이터를 공유할 때 발생하는 패널티에 대한 상대적 수치를 제공하는 것입니다. 테스트에서 얻은 주요 결론은 코어와 스레드 간에 데이터를 공유하기보다 각 코어 내부로 읽기/쓰기 작업을 국한하는 것이 바람직하다는 것입니다. 여러 코어 간에 데이터를 공유하면 코드 처리가 빨라지기는커녕 느려집니다. XBOX One 계열은 Jaguar 프로세서를 사용하고 XBOX Series 콘솔은 Hercules 프로세서를 사용합니다. 두 프로세서 모두 MOESI protocol - Wikipedia을 사용하여 강한 메모리 모델과 캐시 일관성을 유지합니다. 각 캐시 라인은 다섯 가지 상태 중 하나에 있을 수 있습니다.
- Modified
- 이 프로세서만 캐시 라인의 유효한 복사본을 가지고 있으며 변경을 수행했습니다.
- 캐시 라인이 메모리와 일치하지 않습니다.
- Owned
- 이 프로세서만 유효한 복사본을 가지고 있으며 변경을 수행했습니다.
- 다른 프로세서는 읽기 전용 복사본을 가질 수 있습니다.
- Exclusive
- 이 프로세서만 캐시 라인의 복사본을 가지고 있습니다.
- 캐시 라인의 내용이 메모리와 일치합니다.
- Shared
- 이 프로세서는 캐시 라인의 여러 복사본 중 하나를 가지고 있습니다.
- 변경이 이루어졌다면 다른 프로세서가 Owned 상태로 가지고 있을 수 있습니다.
- Invalid
- 이 캐시 라인은 유효하지 않으며 접근 전에 가져와야 합니다.
공유 데이터
데이터 공유는 여러 가지 방식으로 일어날 수 있습니다. 좀 더 명확한 예로는 구조체의 참조 카운트와 같이 변수를 직접 공유하는 경우입니다. 예를 들어 함수 호출의 매개변수로 shared_ptr를 통해 각 스레드가 복사본을 만들면 데이터가 쉽게 수정될 수 있으며, 이 작업만으로도 리소스 경합이 발생합니다. 또 다른 종류의 데이터 공유인 거짓 공유(false sharing)는 감지하기가 더 어렵습니다. 거짓 공유는 두 메모리 주소가 동일한 캐시 라인으로 매핑될 때 발생합니다. Jaguar와 Hercules 프로세서 모두 L1 캐시 라인은 64바이트 크기이며 64바이트로 정렬되어 있으므로, 동일한 64바이트 블록 내의 두 정수는 공유되는 것으로 간주됩니다. 예를 들어 다음 두 데이터 구조를 살펴보세요.
데이터 공유의 비용
테스트 프로파일
테스트 프로파일은 다음 기준을 따랐습니다.- 테스트한 명령어는 원시 읽기(raw read), 원시 쓰기(raw write), 원자적 로드(atomic load), 원자적 저장(atomic store), 원자적 비교 후 저장(Compare and Store, CAS) 작업입니다. 각 명령어 집합에 사용된 코드는 부록: 코드에서 제공됩니다.
- 테스트는 모든 스레드가 공유하는 8바이트 메모리 위치(공유 캐시 라인) 또는 각 스레드가 고유한 8바이트 메모리 위치(고유 캐시 라인)를 사용하여 수행되었습니다.
- 4만 개의 작업이 타이트 루프에서 실행되었습니다.
- 옵티마이저는 루프 언롤링을 비활성화하도록 크기 최소화로 설정되었습니다.
- 총 100회 실행이 수행되었으며, 아래 표에는 모든 실행의 중앙값이 표시됩니다.
단일 코어 테스트
기준선을 잡기 위해 단일 코어에서 각 작업을 테스트하는 것부터 시작했습니다. | 작업 | XBOX One | XBOX One X | XBOX Series S - 3.4 GHz | XBOX Series X|S - 3.6 GHz | XBOX Series X - 3.8 GHz | |--------------|----------:|----------:|----------:|----------:|----------:| | Raw Read | 68.78 us | 52.34 us | 11.81 us | 11.16 us | 10.56 us | | Atomic Load | 68.78 us | 52.34 us | 11.81 us | 11.16 us | 10.56 us | | Raw Write | 114.61 us | 104.64 us | 11.81 us | 11.16 us | 10.56 us | | Atomic Store | 369.55 us | 278.99 us | 207.11 us | 195.61 us | 185.31 us | | Atomic CAS | 668.79 us | 509.02 us | 203.43 us | 192.13 us | 182.03 us | 첫 번째는 Jaguar 프로세서와 Hercules 프로세서 사이의 타이밍 차이입니다. Hercules에서 성능이 최대 4배 향상됩니다. 이는 더 높은 클럭 속도, 더 높은 대역폭, 그리고 동시에 진행 중인 더 많은 메모리 작업에 대한 지원의 조합에 기인합니다. 40,000회의 연속 읽기와 40,000회의 원자적 로드를 비교하면 시간 차이가 전혀 없습니다. 이는 CPU의 강한 메모리 모델과 MOESI 프로토콜 때문입니다. 컴파일러는 두 작업 모두에 대해 동일한 명령어를 생성할 수 있습니다. 이 테스트의 원시 쓰기 및 원자적 저장 작업은 메모리 값을 증가시키려고 시도합니다. 이 테스트에서 원시 쓰기와 원자적 저장 사이의 비용이 두 배로 증가합니다. 원자적 저장이 xchg 명령어를 사용하기 때문인데, 이 명령어는 암묵적으로 lock 접두어를 포함합니다. 이로 인해 결과가 캐시로 플러시될 때까지 프로세서가 정지될 가능성이 있는데, lock 접두어를 사용하는 명령어 간에는 작업 재정렬이 불가능하기 때문입니다. 원시 쓰기는 mov 명령어를 사용합니다. 이는 프로세서를 정지시키지 않으며, mov 명령어에 대해 작업 재정렬이 자유롭게 허용됩니다. 이제 원자적 비교 후 저장(CAS) 작업으로 넘어갑니다. 이 테스트의 작업은 원시 쓰기 및 원자적 저장 테스트와 유사하게 메모리의 값을 증가시키려고 시도합니다. 이 테스트에서는 원자적 저장 테스트와 비교 가능하도록 cmpxchg 명령어에 lock 접두어가 사용되었습니다. Jaguar 프로세서에서는 CAS 작업이 비교 때문에 앞서 언급한 xchg보다 비용이 더 많이 듭니다. 그러나 Hercules 프로세서에는 개선된 구현이 있어 비용이 원자적 저장과 비슷하게 유지됩니다.다중 코어 테스트
다중 코어 테스트는 모두 여러 코어 구성에서 두 개의 스레드를 사용했습니다.- 서로 다른 물리적 코어에 있지만 동일한 클러스터에 있는 두 스레드.
- 서로 다른 물리적 코어이면서 서로 다른 클러스터에 있는 두 스레드.
- SMT가 활성화된 경우, 단일 물리적 프로세서의 두 논리 코어에서 실행되는 두 스레드.
원시 읽기(Raw read) / 원자적 로드(Atomic Load)
이는 메모리 위치에서의 단순한 읽기입니다. 데이터를 변경하는 것이 없기 때문에 스레드 간 경합이 없으며, 각 스레드는 자신의 L1 캐시에 데이터의 자체 복사본을 가지고 있습니다. 그 결과 공유 데이터를 읽는 시간과 고유 데이터를 읽는 시간이 동일합니다. 강한 메모리 모델 덕분에 컴파일러는 원자적 로드에 대해 일반 읽기와 동일한 코드를 생성할 수 있습니다. 즉, 각 작업의 소요 시간이 동일합니다. | 테스트 | XBOX One | XBOX One X | XBOX Series X|S - SMT | XBOX Series X|S - no SMT | |--------------|----------:|----------:|----------:|----------:|----------:| | Single Core | 1.00 | 1.00 | 1.00 | 1.00 | | Same Physical Shared | N/A | N/A | 1.97 | N/A | | Same Cluster Shared | 1.00 | 1.00 | 1.00 | 1.00 | | Cross Cluster Shared | 1.00 | 1.00 | 1.00 | 1.00 | | Same Physical Unique | N/A | N/A | 1.97 | N/A | | Same Cluster Unique | 1.00 | 1.00 | 1.00 | 1.00 | | Cross Cluster Unique | 1.00 | 1.00 | 1.00 | 1.00 | 이 표에서 얻을 수 있는 두 가지 주요 시사점이 있습니다. 첫째, 여러 코어가 동일한 위치에서 읽기를 수행하더라도 메모리에서 읽기만 하는 경우 성능에 영향이 없다는 점입니다. 각 프로세서는 자신의 캐시에 유효한 복사본을 가지고 있으며, 여러 프로세서가 복사본을 가지므로 캐시 라인은 Shared 상태에 있습니다. 둘째, 두 스레드가 동일한 물리적 코어에서 실행되는 구성에서 시간이 두 배가 된다는 점입니다. SMT가 활성화되면 코어의 리소스가 두 스레드 사이에 공유됩니다. 이는 종종 단일 스레드가 사용 가능한 모든 리소스를 활용하지 못하므로 성능 향상으로 이어질 수 있습니다. 그러나 이 테스트는 Load/Store 유닛과 L1 캐시를 지배하는 매우 타이트한 루프입니다. 단일 스레드는 Load/Store 유닛의 모든 슬롯을 사용할 수 있으므로, 두 스레드는 라운드 로빈 방식으로 공유해야 합니다. 결과적으로 각 스레드가 두 배 오래 걸리지만, 코어에서 두 배의 작업이 수행됩니다. 전체적으로 코어는 같은 시간에 동일한 양의 작업을 수행합니다.원시 쓰기(Raw write)
코어 간에 공유되는 메모리에 대한 단순한 쓰기는 단일 코어가 비공유 메모리 위치에 쓰는 것보다 훨씬 느립니다. 쓰기 작업은 코어가 캐시 라인을 갱신하고 다른 코어의 복사본을 무효화하게 만듭니다. 이 특정 테스트는 공유 메모리의 값을 증가시키는 것입니다. 즉, 코어의 캐시에 유효한 복사본이 없으면 메모리 또는 해당 주소에 마지막으로 쓴 코어로부터 데이터를 요청해야 합니다. 이는 두 코어 간에 핑퐁 효과를 만들 수 있는데, 한 코어가 데이터를 갱신하면 두 번째 코어가 첫 번째 코어로부터 데이터를 읽어 갱신하고, 그러면 첫 번째 코어가 다시 갱신하기 전에 그 코어로부터 데이터를 읽어야 하는 식입니다. 이는 테스트 루프의 각 반복마다 앞뒤로 계속됩니다. 핑퐁 효과의 오버헤드는 이 표에서 확인할 수 있습니다. | 테스트 | XBOX One | XBOX One X | XBOX Series X|S - SMT | XBOX Series X|S - no SMT | |--------------|----------:|----------:|----------:|----------:|----------:| | Single Core | 1.00 | 1.00 | 1.00 | 1.00 | | Same Physical Shared | N/A | N/A | 10.69 | N/A | | Same Cluster Shared | 10.54 | 8.73 | 24.36 | 23.59 | | Cross Cluster Shared | 13.61 | 12.78 | 18.89 | 23.57 | | Same Physical Unique | N/A | N/A | 1.98 | N/A | | Same Cluster Unique | 1.05 | 0.93 | 1.03 | 1.03 | | Cross Cluster Unique | 1.01 | 0.84 | 1.04 | 1.03 | 이 표에서 몇 가지 시사점을 얻을 수 있습니다. 첫째, Jaguar 프로세서와 Hercules 프로세서 간의 성능에 대한 상대적 영향의 차이입니다. Hercules에서는 캐시 라인 공유 비용이 Jaguar보다 더 큽니다. 이 경우 Jaguar에서 다른 코어로부터 데이터를 요청하는 데 17 사이클이 드는 반면, Hercules에서 동일한 작업은 90 사이클이 듭니다. 이는 Hercules의 더 깊은 캐시, 특히 Jaguar에는 없는 L3 캐시의 존재 때문입니다. 둘째, 공유 메모리 위치와 고유 위치를 사용할 때의 차이입니다. 이 경우 작업은 현재 값의 증가로, 이는 읽기/수정/쓰기 작업입니다. 읽기 작업은 해당 메모리 위치에 가장 최근에 쓴 코어에 의해 처리되어야 합니다. 그 코어가 다른 코어라면 읽기 비용이 극적으로 높아집니다. 표에서 볼 수 있듯이 최대 25배까지 비쌀 수 있습니다. 셋째, SMT가 활성화된 상태에서 두 스레드가 단일 물리적 코어를 공유할 때의 상대적인 차이가 더 작다는 점입니다. 갱신된 캐시 라인을 다른 코어에서 가져올 필요가 없고 이미 로컬에 있기 때문입니다. 그러나 store-to-load 포워딩은 적용될 수 없습니다. 스레드 A가 값을 갱신하면 스레드 B가 값을 사용하기 전에 캐시로 플러시되어야 합니다. 넷째, 두 스레드가 동일한 물리적 코어에서 고유 메모리를 대상으로 실행되는 구성에서 시간이 두 배가 된다는 점입니다. SMT가 활성화되면 코어의 리소스가 두 스레드 사이에 공유됩니다. 이는 종종 단일 스레드가 사용 가능한 모든 리소스를 활용하지 못하므로 성능 향상으로 이어질 수 있습니다. 그러나 이 테스트는 Load/Store 유닛과 L1 캐시를 지배하는 매우 타이트한 루프입니다. 단일 스레드는 Load/Store 유닛의 모든 슬롯을 사용할 수 있으므로, 두 스레드는 라운드 로빈 방식으로 공유해야 합니다. 결과적으로 각 스레드가 두 배 오래 걸리지만, 코어에서 두 배의 작업이 수행됩니다. 전체적으로 코어는 같은 시간에 동일한 양의 작업을 수행합니다.원자적 저장(Atomic store)
원자적 저장은 xchg 명령어를 사용하며, 이 명령어는 암묵적으로 lock 플래그를 알립니다. xchg 명령어는 해당 메모리 주소를 읽은 다음 쓰기가 필요합니다. 이 동안 캐시 라인이 잠기며, 다른 코어가 해당 캐시 라인에 접근하지 못하도록 막습니다. 이는 xchg 명령어가 데이터를 캐시에 쓸 때까지 대기 중인 작업을 정지시킬 수 있습니다. 또 다른 비용은 코어가 xchg 명령어 전반에 걸쳐 작업을 재정렬할 수 없다는 점입니다. 마지막 비용은 다른 코어가 데이터를 수정한 경우입니다. 이 경우 해당 코어로부터 데이터를 가져와야 합니다. 다른 코어들은 공유 캐시 라인에서 작업할 때 이 전체 시퀀스 동안 대기해야 합니다. Raw Write 테스트와 마찬가지로 Atomic Store 테스트도 유사한 핑퐁 효과가 있을 수 있지만, 이 경우 더 두드러지며 심지어 단일 코어 테스트에도 영향을 미칩니다. 프로세서는 루프의 한 반복 이상으로 사변적 실행을 할 수 없습니다. 스레드 A가 xchg 명령어를 수행하는 동안 스레드 B는 접근을 기다립니다. 스레드 A가 완료되자마자 스레드 B는 계속 실행할 수 있으며, 스레드 A가 루프의 다음 반복에서 접근을 요청하기 전에 즉시 복사본을 요청합니다. 이 표는 단일 코어가 작업을 수행하는 것과 비교한 xchg 명령어의 상대적 비용을 보여줍니다. | 테스트 | XBOX One | XBOX One X | XBOX Series X|S - SMT | XBOX Series X|S - no SMT | |--------------|----------:|----------:|----------:|----------:|----------:| | Single Core | 1.00 | 1.00 | 1.00 | 1.00 | | Same Physical Shared | N/A | N/A | 2.38 | N/A | | Same Cluster Shared | 6.84 | 6.85 | 4.17 | 4.04 | | Cross Cluster Shared | 10.64 | 13.40 | 3.72 | 3.56 | | Same Physical Unique | N/A | N/A | 0.98 | N/A | | Same Cluster Unique | 1.00 | 1.00 | 1.00 | 1.00 | | Cross Cluster Unique | 1.01 | 1.00 | 1.00 | 1.00 | 이 표의 데이터에서 두 가지 시사점이 있습니다. 공유 주소를 사용할 때의 상대적 시간은 원시 쓰기에 비해 원자적 저장의 경우 그리 극단적이지 않습니다. 이 테스트의 비용 상당 부분이 이미 단일 코어 테스트에서 지불되었기 때문입니다. 그러나 Atomic Store의 전체 비용은 여전히 Hercules에서 원시 쓰기보다 2배에서 4배 더 비싸며, Jaguar에서는 최대 13배 더 비쌉니다. 둘째, Hercules는 lock 작업에 대해 개선된 구현을 가지고 있으며, 이 경우 동일 클러스터와 크로스 클러스터 테스트의 소요 시간이 대략 비슷합니다. Jaguar 프로세서는 데이터가 클러스터 간에 공유되고 lock 작업이 사용될 때 최대 두 배까지 비쌀 수 있습니다.원자적 비교 후 저장(Atomic Compare and Store, CAS)
이 테스트의 원자적 비교 후 저장(CAS) 작업은 lock 접두어와 함께 cmpxchg 명령어를 사용합니다. 즉, 작업이 수행되는 동안 캐시 라인이 잠기고 다른 프로세서가 접근할 수 없습니다. 이는 Atomic Store 테스트에서 xchg 명령어가 작동하는 방식과 동일합니다. 이 테스트에서 CAS 작업은 Atomic Store 작업과 동일한 연산을 수행합니다. 공유이든 고유이든 메모리의 값을 증가시키려고 시도합니다. 유일한 차이점은 CAS 작업이 지정된 세 번째 값과 같을 때만 메모리 위치에 쓴다는 것입니다. | 테스트 | XBOX One | XBOX One X | XBOX Series X|S - SMT | XBOX Series X|S - no SMT | |--------------|----------:|----------:|----------:|----------:|----------:| | Single Core | 1.00 | 1.00 | 1.00 | 1.00 | | Same Physical Shared | N/A | N/A | 2.91 | N/A | | Same Cluster Shared | 4.32 | 4.32 | 4.01 | 4.24 | | Cross Cluster Shared | 10.12 | 14.80 | 3.52 | 3.45 | | Same Physical Unique | N/A | N/A | 0.91 | N/A | | Same Cluster Unique | 1.00 | 1.00 | 1.00 | 1.00 | | Cross Cluster Unique | 1.00 | 1.00 | 1.00 | 1.00 | Atomic Store와 Atomic Compare and Store 테스트 모두 lock 접두어를 사용하기 때문에 상대적 비용은 동일하며 그 이유도 같습니다. 공유 주소를 사용하는 단일 스레드와 멀티스레드 작업 간의 상대적 시간이 극단적이지 않은 이유는 이 테스트 비용의 상당 부분이 이미 단일 코어 테스트에서 지불되었기 때문입니다. 그러나 CAS의 전체 비용은 데이터가 공유될 때 Hercules에서 여전히 2배에서 4배 더 비싸고, Jaguar에서는 최대 14배 더 비쌉니다. 둘째, Hercules는 lock 작업에 대해 개선된 구현을 가지고 있으며, 이 경우 동일 클러스터와 크로스 클러스터 테스트의 소요 시간이 대략 비슷합니다. Jaguar 프로세서는 데이터가 클러스터 간에 공유되고 lock 작업이 사용될 때 최대 두 배까지 비쌀 수 있습니다.권장 사항
코어 간에 쓰기 가능한 데이터를 공유하지 않도록 따를 수 있는 몇 가지 핵심 패턴이 있습니다.
- 데이터 구조: 각 스레드가 고유한 구조체에서 작업하도록 구조체의 배열(AoS)에서 배열의 구조체(SoA)로 전환합니다.
- 읽기 전용 데이터를 읽기/쓰기 데이터와 분리합니다.
- 패딩: 캐시 라인 크기(64바이트)의 배수만큼 구조체에 패딩을 추가할 수 있습니다. 이렇게 하면 각 스레드가 하나의 구조체에서 작업할 경우 거짓 공유가 제거됩니다.
- 참조 카운팅(shared_ptr, weak_ptr 등): 이들은 객체의 모든 인스턴스 간에 공유되는 단일 참조 카운트 변수를 가지고 있습니다.
- 데이터의 소유권을 명확히 하세요. 데이터가 다른 스레드로 처리를 위해 넘겨지면 이제 그 스레드가 데이터를 소유합니다. 해당 스레드가 데이터 처리를 끝낼 때까지 그 데이터에 대해 아무 것도 하지 않도록 합니다.
- 공유 작업 큐: 특히 작업이 작을 때 여러 스레드가 공유하는 하나의 작업 큐를 사용하지 않도록 합니다. 대신 각 스레드에 고유한 작업 큐를 두는 방법을 고려하세요. 스레드 간 부하를 균형 있게 유지하기 위해 작업 훔치기(work stealing) 알고리즘을 사용할 수 있습니다.
