x86 에뮬레이션의 골칫거리
요약
본 기사는 ARM 아키텍처에서 x86 메모리 모델(x86-TSO)을 에뮬레이션하는 과정에서 발생하는 성능 및 정확성 문제를 심층 분석합니다. 특히, 강한 순서 제약을 가진 x86과 느슨한 순서를 허용하는 ARM의 차이점 때문에 여러 가지 기술적 난제들이 존재함을 설명합니다.
핵심 포인트
- x86-TSO는 강력한 메모리 순서 제약을 요구하며, 이는 ARM의 기본 모델과 큰 차이를 보입니다.
- FEAT_LRCPC와 같은 최신 기능은 x86 에뮬레이션에 도움을 주지만, 모든 접근 패턴에서 완벽하게 문제를 해결하지 못합니다.
- 비정렬 접근이나 캐시라인 경계를 넘는 원자적 연산 등에서는 여전히 성능 저하 및 정확성 문제가 발생할 수 있습니다.
ARM에서 x86 프로그램을 실행하려면 x86-TSO 메모리 모델 을 재현해야 하며, ARM의 느슨한 메모리 순서 규칙과의 차이가 성능과 정확성 모두에 부담을 줌
FEAT_LRCPC 는 일반적인 메모리 읽기 성능을 크게 개선하지만, 비정렬 접근과 쓰기까지 모두 해결하지는 못함
비정렬 메모리 접근 은 FEX가 메모리 배리어나 예외 처리 경로로 우회하게 만들며, 지원 하드웨어와 접근 경계에 따라 성능 차이가 매우 커짐
캐시라인 경계를 넘는 원자적 연산인 split-lock 은 성능뿐 아니라 정확성도 미해결 과제이며, FEX의 현재 구현에서는 일부 상황에서 데이터 찢어짐이 발생할 수 있음
GPU에 데이터를 전달하는 쓰기 결합 메모리 에서는 ARM의 TSO 에뮬레이션 쓰기 대역폭이 x86보다 최대 816배 낮았으며, 캐시 일관성을 지원하는 UMA 시스템과 달리 PCIe GPU 환경은 우회하기도 어려움
x86-TSO와 ARM 메모리 모델의 차이
메모리 모델 은 단일 스레드와 다중 스레드 환경에서 읽기와 쓰기가 서로 어떤 순서로 관찰되는지 규정함
x86-TSO 는 강한 순서 제약을 제공함
ARM의 약한 순서 모델 은 일반 읽기와 쓰기에 더 많은 재정렬 여지를 주어 하드웨어 최적화를 허용함
일관성 과 원자성 은 관련 있지만 서로 다른 속성임
일관성은 다른 프로세서가 메모리 접근을 어떤 순서로 관찰하는지와 관련됨
원자성은 한 접근의 중간 상태나 변경 전후 데이터가 섞인 값을 관찰하지 않도록 보장하는 성질임
ARMv7 이전 계열은 메모리 순서를 보장하기 위해 비용이 큰 메모리 배리어 를 사용했으며, 이후 load-acquire/store-release 명령어가 이 부담을 줄임
C++의 std::atomic
에서 memory_order_acquire
, memory_order_release
에 각각 대응함
ARM 용어상 이 명령어 자체를 원자적 연산으로 분류하지는 않으며, FEX도 과거에는 이를 atomic-load/atomic-store라고 불렀음
ARM의 RCsc(Release Consistency sequentially consistent) 모델은 acquire 읽기와 release 쓰기에 순서 제약과 barrier-ordered-before
의미론을 적용함
이전처럼 매번 별도 배리어 명령어를 사용하는 비용을 피할 수 있음
ARMv8.0의 기본 경로와 LRCPC의 개선
FEX는 ARMv8.0-a에서 모든 x86 읽기를 load-acquire , 쓰기를 store-release 로 변환함
x86과 사실상 같은 메모리 의미론을 얻지만, 정확히 맞는 중간 단계가 없어 필요한 수준보다 더 엄격한 순서를 강제함
원래 비교적 드물게 쓰이던 acquire/release 명령어가 실행 명령어의 대부분을 차지하면서 비용이 커짐
정렬된 일반 접근을 사용하는 마이크로벤치마크 에서도 CPU별 차이가 크게 나타남
일반 읽기/쓰기를 기준으로 같은 양의 작업을 비교했으며, 시스템의 최대 메모리 대역폭 측정이 목적은 아님
테스트한 CPU 5종 중 3종에서 acquire 읽기 성능이 크게 떨어졌고, AmpereOne의 release 쓰기는 특히 낮았음
Cortex-X4와 Cortex-X925는 이 명령어들의 성능이 좋았지만, Oryon-3는 상대적으로 우선순위를 낮춘 모습임
ARMv8.3부터 필수인 FEAT_LRCPC 는 RCpc(Release Consistency processor consistent) 모델과 새 읽기 명령어를 추가함
x86 에뮬레이션 요구에 맞춘 확장으로, 대부분의 테스트 플랫폼에서 LRCPC 읽기는 일반 읽기와 비슷한 성능을 보임
FEX는 지원을 감지하면 acquire 읽기를 LRCPC 읽기로 전환함
이 마이크로벤치마크에서는 문제가 거의 해결된 것처럼 보이지만, 다른 접근 패턴에는 여전히 예외가 있음
Apple의 하드웨어 TSO 모드와 확장 방식의 한계
Apple Silicon 은 하드웨어에 x86-TSO 모드를 넣어, 활성화 시 일반 ARM 읽기/쓰기 명령어의 동작을 x86 요구에 맞게 바꿈
Apple M1의 LRCPC 읽기는 acquire 읽기의 별칭이며, Apple의 x86 에뮬레이터는 대신 일반 읽기/쓰기를 사용함
FEX도 Asahi Linux 에서 이 기능을 감지하면 활성화함
스레드 전체에 적용되는 TSO 모드 에도 성능 비용은 있지만, 첫 마이크로벤치마크에서는 구분하기 어려웠음
함께 실행되는 ARM 네이티브 코드까지 불필요한 TSO 비용을 부담할 수 있으나, 에뮬레이션 중 실행되는 ARM 네이티브 코드의 비중은 0%에 가까움
FEX가 중요하게 보는 것은 소수의 ARM 접근 비용보다, 대부분을 차지하는 x86 접근 성능이 이상적인 수준에 크게 못 미치는 문제임
하드웨어 TSO 전환 기능 은 모든 메모리 접근 명령어에 원하는 동작을 적용한다는 점에서 FEX가 선호하는 해법임
FEAT_LRCPC 는 기본 범용 레지스터용 TSO 읽기를 추가함
FEAT_LRCPC2 는 TSO 읽기에 작은 즉시값 오프셋을 추가함
FEAT_LRCPC3 는 기본 벡터 및 스택 기반 TSO 읽기/쓰기를 추가함
세 차례 확장 후에도 남은 예외가 있으며, 추가 확장이 필요할 것으로 예상함
비정렬 접근과 FEX의 코드 패치
x86 애플리케이션과 게임은 메모리 정렬 을 지키지 않는 경우가 많음
일반 읽기/쓰기가 캐시라인 안에 머무르면 비정렬 상태에서도 원자성과 메모리 일관성을 보장함
일반 읽기/쓰기가 캐시라인을 넘으면 데이터 찢어짐이 가능하며, 이는 split-lock과는 다름
split-lock은 Linux 커널이 감지해 실행을 늦추기도 하며, 일부 게이머는 이를 피하려고 커널 옵션을 변경함
ARMv8.0의 acquire/release 접근은 자연 정렬(natural alignment) 을 요구함
8바이트 접근이라면 주소 오프셋이 0, 8, 16, 24처럼 접근 크기의 배수여야 함
이를 어기면 정렬 예외(alignment fault) 가 발생하며, 일반적으로는 충돌로 이어지지만 FEX는 별도로 처리함
FEX의 JIT는 예외가 발생할 수 있는 읽기/쓰기 주변에 패치 지점(patchpoint) 을 둠
명령어 앞이나 뒤에 NOP
를 배치하고 해당 접근을 추적함
예외 발생 시 acquire/release 명령어를 일반 읽기/쓰기로 바꾸고 DMB 데이터 메모리 배리어 로 감싼 뒤 실행을 재개함
결국 비정렬 접근에서는 이전 세대의 비용 큰 배리어 방식으로 되돌아감
일반 읽기/쓰기는 이 벤치마크에서 정렬 여부에 따른 차이가 작아 평균값을 사용했지만, TSO 에뮬레이션 경로 는 정렬 여부에 따라 큰 차이를 보임
CPU별 비정렬 읽기/쓰기 성능
AmpereOne 은 정렬과 비정렬 읽기 성능이 오차 범위 안에서 비슷했음
DMB 비용을 잘 처리하거나, 이미 다른 병목에 막혀 있을 가능성이 있음
release 쓰기 자체가 매우 느렸고, 비정렬 쓰기는 여기서 약 8.5% 더 느려짐
같은 테스트의 일반 쓰기는 약 28GB/s였으며, 소비자용 게임보다는 서버 작업에 맞춘 특성이 드러남
Cortex-X4 는 Qualcomm Snapdragon 8 Gen 3 의 코어로, 여러 휴대용 기기에서 사용됨
그래프가 복잡해지지 않도록 SoC에서 이 코어만 테스트했으며, 비교 대상 중 유일한 휴대전화용 SoC임
정렬 읽기는 약 11.5GB/s, 쓰기는 약 6.7GB/s를 기록함
비정렬 접근에서 DMB를 사용하면 읽기와 쓰기 모두 약 50%의 성능 손실이 발생함
DGX Spark 의 Cortex-X925 는 X4와 유사한 추세를 더 높은 성능 수준에서 보임
플랫폼 메모리 대역폭은 273GB/s로, 앞선 X4 플랫폼의 76.8GB/s보다 큼
비정렬 접근의 손실 경향도 비슷하지만 쓰기 성능은 다소 빠르게 회복하며, 더 빠른 메모리 덕분일 가능성이 있음
Oryon-3 는 Linux 지원이 진행 중인 최신 Qualcomm 코어로, 정렬된 LRCPC 읽기가 일반 읽기와 같은 성능을 냄
release 쓰기는 일반 쓰기 대역폭의 68%를 기록함
비정렬 읽기는 약 70%의 성능 손실이 발생하고, 쓰기는 정렬 시 성능의 약 43% 수준으로 내려감
그래도 비정렬 접근 성능이 비교한 Cortex 코어들의 정렬 접근보다 높았음
광고한 “Fully coherent 96KB 6-way L1 cache with 64B coherency granules”가 일반 비정렬 접근의 손실까지 없애지는 않음
Apple M1 은 TSO 모드에서 정렬과 비정렬 접근 성능이 거의 같았으며, 비정렬 쓰기 손실은 약 5% 수준임
하드웨어가 처리하므로 비정렬 접근에 DMB를 추가할 필요가 없음
TSO 모드 자체의 비용은 남아 있어 쓰기가 일반 쓰기 성능의 76% 수준이지만, 읽기는 거의 동일함
FEAT_LSE2가 해결하는 범위
테스트한 모든 ARM 플랫폼은 FEAT_LSE2 를 지원하며, acquire/LRCPC/release 읽기/쓰기와 읽기-수정-쓰기(RMW) 원자적 연산 의 정렬 제약을 완화함
단, 비정렬 접근이 16바이트 영역 안에 머무를 때만 적용됨
16바이트 경계를 넘으면 여전히 정렬 예외가 발생하므로, 캐시라인 전체에서 자유롭게 접근하는 x86 코드에는 제한적인 개선임
ARM의 단일 복사 원자성(single-copy atomicity) 은 자연 정렬된 읽기/쓰기에서 데이터 찢어짐을 방지함
FEAT_LSE2는 이 보장을 16바이트 영역 안의 비정렬 접근까지 확장함
x86은 캐시라인 전체에 걸쳐 이 보장을 제공하므로, 문제가 줄어들 뿐 완전히 없어지지는 않음
원자적 명령어의 대응과 성능 차이
ARMv8.1-a에는 x86의 원자적 RMW 연산 18종 대부분에 대응하는 명령어가 있음
LOCK DEC
, INC
, ADC
, ADD
, SBB
, SUB
, XADD
는 ldaddal
에 대응함
LOCK NOT
, XOR
는 ldeoral
, AND
는 ldclral
, OR
는 ldsetal
에 대응함
원문의 대응표에서 LOCK BTC
, BTR
, BTS
는 각각 ldclralb
, ldeoralb
, ldsetalb
로 연결됨
LOCK CMPXCHG
는 casal
, CMPXCHG8B
와 CMPXCHG16B
는 caspal
에 대응함
LOCK NEG
는 단순 대응이 어려운 예외이며, 실제 작업에서는 사용되지 않아 상세 분석에서 제외함
명령어 대응이 있어도 비정렬 원자적 접근 의 문제가 남음
비교 벤치마크는 캐시라인 내 주소 위치를 바꿔 가며 단일 원자적 명령어를 측정함
다른 원자적 연산도 대체로 비슷하게 동작하며, 가장 빠른 결과와 느린 결과의 차이는 약 1,000배여서 로그 눈금을 사용함
x86 Zen 은 캐시라인 안에 접근이 완전히 포함되면 정렬 여부와 관계없이 1.44ns의 지연시간을 보임
수십 년간 지원한 원자적 캐시라인 덕분이며, 게임도 이 동작에 크게 의존함
64바이트 경계를 넘는 split-lock은 약 660ns로, 약 458배 느려짐
느리더라도 원자성과 일관성을 유지하며 데이터가 찢어지지 않음
동작 원리는 Chips and Cheese의 split-lock 분석 에서 더 자세히 다룸
ARM은 자연 정렬 상태에서도 가장 빠른 플랫폼의 지연시간이 x86의 약 3배 였음
게임 성능에 영향을 주지만 보통 직접적인 병목은 아니어서 전체 영향을 정확히 측정하기 어려움
대부분의 ARM 플랫폼에서 16바이트 경계와 64바이트 경계를 넘는 접근은 비슷한 느린 경로로 처리됨
Oryon-3의 원자적 캐시라인과 Steam Frame의 커널 패치
FEX는 지원 범위를 넘는 원자적 접근을 일반 읽기/쓰기처럼 DMB로 패치할 수 없어, 실행할 때마다 정렬 예외 를 처리함
커널, 사용자 공간 신호 처리기, 커널, 원래 코드로 이동하는 비용이 매번 발생함
초당 수천 번 실행되면 이 전환 비용이 빠르게 누적됨
Oryon-3 는 원자적 접근이 64바이트 캐시라인 안에 있으면 자연 정렬과 같은 성능을 냄
x86 에뮬레이션의 성능과 정확성 문제 일부를 해결함
64바이트 경계를 넘는 split-lock은 지원하지 않아 FEX의 에뮬레이션 경로로 돌아감
Apple M1 은 하드웨어 TSO 모드가 있어도 캐시라인 전체의 비정렬 원자적 연산을 지원하지 않음
16바이트 경계를 넘으면 다른 플랫폼과 같은 문제가 발생함
Valve Steam Frame 의 Cortex-X4 는 비정렬 원자적 연산에서 약 209ns를 기록해, Cortex-X925의 1,060ns보다 약 5배 빨랐음
FEX 개발자가 만든 커널 패치 를 적용해 사용자 공간으로 왕복하지 않고 커널에서 처리함
다른 플랫폼도 이 패치를 적용하면 FEX가 자동으로 활용함
이러한 성능 개선과 별개로, 현재 FEX는 split-lock을 가능한 범위에서 에뮬레이션하지만 일부 경우 데이터 찢어짐 이 발생할 수 있음
Oryon-3의 원자적 캐시라인 지원도 캐시라인 경계를 넘는 정확성 문제까지 해결하지는 못함
split-lock을 소프트웨어만으로 정확히 처리하기 어려운 이유
전역 뮤텍스 는 그 뮤텍스에 참여하는 split-lock끼리만 직렬화하므로 충분하지 않음
FEX는 경계 양쪽을 두 개의 64비트 비교-교환(CAS)으로 처리해야 함
정렬된 원자적 연산은 뮤텍스에 참여하지 않으므로, 한쪽 데이터를 동시에 수정할 수 있음
첫 CAS가 실패하면 재시도할 수 있지만, 두 번째 CAS가 실패하면 이미 일부 변경이 반영되어 데이터가 찢어짐
이런 접근은 실제 잠금 없는 연결 리스트 구현에도 존재하며, 손상이나 충돌 여부는 게스트 알고리듬에 달려 있음
커널이 메모리를 공유하는 모든 프로세스와 스레드 를 추적하고, split-lock 동안 전부 멈추는 방법은 성능상 현실적이지 않음
게임과 애플리케이션은 초당 수천 회 이상 split-lock을 실행할 수 있음
매번 전체 실행을 중단하는 비용은 네이티브 x86보다도 훨씬 커짐
정확한 에뮬레이션에는 어떤 형태로든 하드웨어 지원 이 필요함
ARM의 Transactional Memory Extension 은 여러 연산을 트랜잭션으로 묶어 원자적으로 커밋하고, 실패하면 재시도하는 해법이 될 수 있었음
하지만 이 확장은 출하된 적 없이 공식 폐기됨
x86의 유사 확장도 많은 문제로 여러 플랫폼에서 비활성화된 전례가 있음
FEX가 제안하는 경계 횡단 CASP
FEX는 128비트 CASP 의 두 64비트 절반이 원자적 접근 영역의 경계 양쪽에 정확히 걸칠 때, 정렬 예외 없이 연산을 시도하도록 허용하는 방안을 제안함
x86의 비정렬 원자적 연산은 최대 64비트이므로, 필요한 데이터를 이 단일 연산 범위에 포함할 수 있음
모든 원자적 명령어에 x86식 split-lock을 추가하자는 제안은 아님
핵심은 안전한 실패와 재시도 임
반드시 찢어짐 없이 성공해야 하는 x86 원자적 연산과 달리, 제안한 CASP는 실패를 반환한 뒤 다시 시도할 수 있음
CAS는 읽은 최신 데이터를 반환하므로, FEX가 이를 바탕으로 재시도할 수 있음
두 캐시라인 중 하나를 다른 코어가 먼저 확보하면 실패하도록 하고, 하드웨어는 결국 연산이 완료되는 전진 보장을 제공해야 함
완료 보장에 수천 사이클이 걸려도 x86 split-lock과 비슷한 수준이 될 수 있음
이는 하드웨어 설계 제안 이며 구현된 기능은 아님
FEX는 ARM의 LL/SC 구조와 기존 전진 보장 지원을 활용할 수 있는 해법으로 보고 있음
GPU용 비캐시 메모리가 필요한 이유
여기서 비캐시 메모리 는 Vulkan의 VK_MEMORY_HOST_CACHED_BIT
가 없는 메모리를 뜻함
GPU 메모리, 특히 PCIe 너머 메모리는 비캐시일 때 대개 호스트 일관성 속성도 얻지만 항상 그런 것은 아님
CPU 매핑은 주로 캐시 가능한 쓰기 되돌림(WB) 과 비캐시 쓰기 결합(WC) 으로 나뉨
강한 비캐시 매핑은 사용자 공간에서 사실상 쓰이지 않아 비교에서 제외함
게임은 보통 스테이징 버퍼 에 캐시 메모리를, GPU로 직접 데이터를 전달할 때 비캐시 메모리를 사용함
UMA 시스템은 캐시 가능하고 일관성이 있으며 GPU에서도 접근 가능한 메모리를 제공할 수 있음
PCIe GPU는 이를 보장할 수 없어 스테이징 버퍼의 비동기 복사나 비캐시 메모리를 통한 전송이 필요함
일부 엔진은 UMA 전용 경로 없이 항상 비캐시 방식을 쓰며, 비캐시 버퍼 지원을 노출하지 않으면 작동하지 않는 엔진도 있음
쓰기 결합 메모리에서 발생하는 성능 절벽
Steam Frame과 Snapdragon X2 Elite 는 캐시 메모리 벤치마크에서 초당 수십 GB를 기록하며, 메모리 대역폭이 큰 Oryon-3가 더 높은 성능을 냄
WC 메모리에서도 ARM의 일반 쓰기 는 캐시 메모리와 비슷한 성능을 보임
쓰기 결합 버퍼 가 캐시라인 분량의 데이터를 잠시 모았다가 한 번에 전송하기 때문임
Zen 4의 WC 쓰기는 캐시 메모리 성능에 못 미쳤지만, PCIe 전송을 고려하면 충분할 가능성이 있음
WC 읽기 는 모든 테스트 플랫폼에서 매우 느림
비캐시 의미론을 유지하려면 매 접근마다 시스템 메모리까지 읽어야 함
ARM의 일반 읽기는 Zen보다 빨랐지만, LRCPC 읽기는 더 느렸음
가장 심각한 문제는 TSO 에뮬레이션 쓰기 로, Zen 대비 대역폭이 최대 816배 낮았음
FEAT_LRCPC 계열 은 WC 메모리에 x86-TSO 의미론으로 쓰는 문제를 해결하지 못함
FEX는 ARMv8.0-a 때부터 메모리 종류에 관계없이 같은 store-release
경로를 사용함
현재 우회책은 문제가 될 때 TSO 에뮬레이션을 선택적으로 끄는 것임
PCIe GPU 환경에서는 UMA보다 불리한 경험이 이어지며, 이를 해결할 추가 확장이 필요함
UMA의 우회책과 남은 과제
CPU와 GPU가 캐시 일관성 을 공유하는 UMA 플랫폼은 그래픽 드라이버가 항상 캐시 버퍼를 사용하도록 해 문제를 피할 수 있음
NVIDIA Tegra는 이미 이 방식을 사용함
Snapdragon은 적어도 Adreno 600 계열부터 이를 지원하며, 여러 Mali 플랫폼에서도 가능함
Adreno Turnip 패치 는 지원 플랫폼에서 FEX 실행 시 비캐시 메모리를 사용하지 않도록 함
Asahi 사용자는 하드웨어 TSO 모드 덕분에 실제 사용에서 이 문제를 겪지 않지만, PCIe GPU 연결은 별개의 문제임
ARM 플랫폼의 Radeon GPU는 WC 메모리를 숨기고 WB로 제공하는 특이한 동작도 있음
ARMv8.0을 최소 사양으로 삼던 시점 이후 하드웨어 지원은 크게 개선됐지만, 아키텍처 차원의 예외 상황 은 아직 남아 있음
여러 공급업체가 문제의 일부를 해결하며 성능과 호환성을 높이고 있음
장기적인 목표는 ARM으로 이식되지 않을 x86 게임도 계속 실행해, 하드웨어 플랫폼이 달라져도 PC 게임 생태계의 유산을 유지하는 것임
댓글과 토론
AI 자동 생성 콘텐츠
본 콘텐츠는 GeekNews의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기