Skip to content

필사 모드: 명령어 하나가 62초 걸릴 수 있다 — 지연은 명령어가 아니라 경로의 속성이다

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

들어가며 — 1사이클과 1,980억 사이클 사이

명령어 지연표는 최적화하는 사람의 기본 도구입니다. 곱셈이 3사이클, 나눗셈이 20사이클, 이런 식으로 외워 두고 코드를 짭니다.

2026년 8월 공개된 Assembly Hall of Shame은 그 반대 방향으로 갑니다. 부제가 "Racing to the bottom of CPU performance"입니다. 단일 명령어 하나를 가장 느리게 만드는 경쟁의 순위표입니다.

최하위 27위는 nop으로 1사이클입니다. 1위는 fxrstor64로 1,980억 사이클, 시간으로 62초입니다.

같은 부류의 물건 사이에 2,000억 배 차이가 난다면, 그 부류를 하나의 척도로 재고 있다는 전제부터 의심해야 합니다. 이 글은 그 순위표를 아래에서 위로 읽으면서 무엇이 그 차이를 만드는지, 그리고 그게 여러분의 코드와 벤치마크에 무엇을 뜻하는지 정리합니다.

규칙이 만든 것 — 무엇이 "명령어 하나"인가

이런 경쟁에서는 규칙이 곧 논지입니다. 저장소에 적힌 규칙은 이렇습니다.

  • 준비 과정에는 무엇을 써도 되지만, 점수를 매기는 것은 단일 명령어 하나뿐이다.
  • 트랩되거나 에뮬레이트되거나 가상화되는 명령어는 트랩까지만 재고 핸들러는 재지 않는다.
  • 명령어는 중단 가능하면 안 된다. rep movspause 같은 것은 실격이다.
  • 시간은 CPU 기본 클록 기준으로 정규화한다.
  • 모든 플랫폼은 공장 출하 설정이어야 하며 하드웨어 개조는 금지한다.

세 번째 규칙이 이 경쟁을 의미 있게 만듭니다. 중단 가능한 명령어를 허용하면 반복 접두사가 붙은 문자열 명령어로 아무 숫자나 만들 수 있습니다. 중단 불가를 요구하면 파이프라인이 그 명령어 하나에 실제로 묶여 있는 시간만 세게 됩니다.

두 번째 규칙도 마찬가지입니다. 트랩되는 명령어를 허용하면 측정 대상이 명령어가 아니라 운영체제 핸들러가 됩니다.

이 두 규칙 덕분에 순위표는 잡기가 아니라 마이크로아키텍처 관찰 기록이 됩니다.

첫 번째 구간 — 코어 안에서 마이크로코드가 끼어드는 지점

하위권은 전부 코어 내부에서 벌어지는 일입니다. 측정은 대부분 Intel Core i7-8559U에서 이루어졌습니다.

순위명령어사이클무엇이 일어나는가
27nop1아무 일도 없음
26nop1620data16 접두사를 일곱 개 붙인 긴 nop
25rdtsc49기준점
24idiv77128비트 피제수로 나눗셈기의 최장 경로를 태움
23enter112최대 중첩 깊이로 마이크로코드 디스플레이 순회 유발
22fldl133비정규 수 적재로 FP 마이크로코드 보조 진입
20fsin257지수 0x7ff의 특수값 처리 경로
17fadd677비정규 피연산자로 FP 마이크로코드 보조
15fdiv883비정규 제수
14cpuid1,248지연이 가장 큰 리프 선택

이 구간의 공통 패턴이 보입니다. 하드웨어 고속 경로가 처리하지 못하는 입력을 주면 제어가 마이크로코드로 넘어갑니다.

fadd 항목이 특히 교훈적입니다. 같은 명령어인데 피연산자가 비정규 수라는 이유만으로 677사이클이 걸립니다. 정상적인 부동소수점 덧셈은 몇 사이클입니다. 명령어는 그대로이고 데이터만 바뀌었는데 두 자릿수 배율이 생긴 것입니다.

enter 항목도 재미있습니다. 중첩 깊이를 최대인 31로 주면 디스플레이 포인터 30개를 적재하고 푸시하는 마이크로코드 경로를 타게 됩니다. 명령어 인코딩상 허용되지만 현대 컴파일러는 절대 생성하지 않는 형태입니다.

두 번째 구간 — 코히런시와 플랫폼이 개입하는 지점

중간 구간부터는 코어 하나로 끝나지 않습니다.

순위명령어사이클무엇이 일어나는가
21clflush165더티 라인 축출
19mfence326movnti 16개로 쓰기 결합 버퍼를 포화시킨 뒤 전부 배출
18mov cr3352TLB 전체 무효화
16split lock865캐시 라인을 걸친 lock xaddl로 외부 버스 잠금 유발
13rdrand5,579하드웨어 엔트로피 풀 고갈 후 회복 대기
12wrmsr34,304Zen의 MCG_CTL 기록. 다이 밖 유닛까지 동기화 추정
11out49,857NIC 레지스터 경계를 걸친 포트 쓰기로 TX DMA 정지
9wbinvd1,616,480전체 캐시 계층을 더티로 채운 뒤 DRAM 라이트백

16위 분할 잠금이 실무에서 가장 자주 만나는 항목입니다. 원자 연산의 피연산자가 캐시 라인 경계에 걸치면 CPU는 빠른 MESI 캐시 일관성 경로를 쓸 수 없고 외부 버스 잠금을 걸어야 합니다. 865사이클이 걸리고, 그 사이 다른 코어들도 영향을 받습니다.

9위 wbinvd가 160만 사이클인 것도 눈여겨볼 만합니다. 이건 명령어가 복잡해서가 아니라 캐시에 들어 있던 모든 더티 데이터를 DRAM까지 밀어내야 하기 때문입니다. 즉 이 명령어의 비용은 명령어가 아니라 그 시점의 캐시 상태가 결정합니다.

12위 wrmsr 항목의 추정도 인용할 만합니다. 저장소는 이것이 여러 하드웨어 유닛에 걸친 머신 체크 뱅크를 마이크로코드가 정지시키고 동기화하는 동작으로 보이며, 일부는 다이 밖에 있어 단순한 로컬 레지스터 쓰기가 아니라 패브릭 수준의 통신을 요구하는 것 같다고 씁니다. 저자 본인이 추정임을 밝히고 있으므로 그대로 옮깁니다.

세 번째 구간 — 다이 밖으로 나가는 순간

상위권은 성격이 완전히 다릅니다.

순위명령어사이클시간
8inl (ACPI PM 포트)12,524,4153.92밀리초
7movl (MMIO GPU 레지스터)443,937,696139밀리초
6movq (8바이트 MMIO)887,716,864278밀리초
5vmovdqu xmm (16바이트)1,774,555,776556밀리초
4vmovdqu ymm (32바이트)3,549,079,2961.111초
3vmovdqu ymm (비정렬 32바이트)4,453,212,2561.394초

여기 있는 것은 전부 mov입니다. 데이터를 옮기는 것 말고는 아무 일도 하지 않는 명령어가 1초 넘게 걸립니다.

이유는 주소에 있습니다. 이 주소들은 DRAM이 아니라 PCIe 패브릭 너머의 장치 레지스터입니다. 저자는 mmiotic이라는 별도 도구로 MMIO 공간을 뒤져 응답이 가장 느린 영역을 찾았습니다.

그리고 6위부터 3위까지의 증가 패턴이 이 구간의 원리를 그대로 드러냅니다. 접근 폭을 8바이트, 16바이트, 32바이트로 늘리면 시간이 대략 두 배씩 늘어납니다. 저장소 설명에 따르면 8바이트 MMIO 읽기 하나가 더블워드 레지스터 접근 두 번으로 분해되기 때문입니다. 32바이트면 여덟 번, 정렬을 어긋나게 하면 아홉 번입니다.

여기서 핵심은 이겁니다. MMIO 읽기는 논포스티드 트랜잭션입니다. 쓰기는 보내 놓고 잊어버릴 수 있지만 읽기는 응답을 받아야 합니다. 그래서 왕복 시간이 그대로 명령어 실행 시간이 됩니다. 저장소는 3위에 대해 비정렬 32바이트 MMIO 읽기가 "기술적으로 허용되지 않지만 어쨌든 동작한다"고 적어 두었습니다.

1위의 전략이 증명하는 것

이제 1위입니다.

; CPU 0 — 측정 대상 명령어
movl $0xfcc68830, %rsi
fxrstor64 %rsi

; CPU 1..N — 다른 고지연 위치를 두드리는 해머 루프
movl 0xfcc68858, %eax

fxrstor64는 512바이트짜리 FPU/MMX/XMM 상태를 메모리에서 복원하는 명령어입니다. 그 512바이트를 가장 느린 MMIO 영역에서 읽게 하면 위 3위 기법의 확장판이 됩니다. 이것만으로 74,584,168,512사이클, 23.35초가 나옵니다.

1위는 여기에 한 겹을 더합니다. 그 적재가 진행되는 동안 나머지 코어들이 다른 고지연 MMIO 레지스터를 4바이트 단위로 계속 두드립니다. PCIe 루트 콤플렉스와 엔드포인트가 논포스티드 트랜잭션으로 포화되고, CPU 0의 512바이트 적재는 그 뒤에 줄을 서야 합니다.

결과가 198,002,498,236사이클, 62초입니다. 즉 명령어 하나의 실행 시간이 다른 코어들이 무엇을 하고 있는지에 의해 두 배 이상 달라졌습니다.

이 실험이 증명하는 명제는 명확합니다. "이 명령어는 몇 사이클인가"라는 질문은 주변 상태를 고정하지 않으면 답이 없습니다. 표에 적힌 숫자는 명령어의 속성이 아니라 명령어와 시스템 상태의 조합에 대한 관측치입니다.

부수적으로 이 기법이 실용적 결과를 낳기도 했습니다. 저장소는 3위의 비정렬 ymm 적재가 System Management Mode의 근본 설계를 깨는 데 사용되었다고 적으며 별도 저장소를 링크합니다. 인터럽트를 임의로 길게 늘일 수 있으면 원자성을 전제한 설계가 깨집니다.

이 표가 여러분의 벤치마크에 대해 말하는 것

순위표는 극단적인 놀이처럼 보이지만, 결론은 평범한 성능 작업에 그대로 적용됩니다.

마이크로벤치마크는 명령어가 아니라 시스템 상태를 측정합니다. 같은 코드가 캐시가 따뜻할 때와 차가울 때, 다른 코어가 놀 때와 바쁠 때, 데이터가 정규 수일 때와 비정규 수일 때 전혀 다른 숫자를 냅니다. 벤치마크 결과를 인용할 때 조건을 함께 적어야 하는 이유가 이것입니다.

최악의 경우는 평균의 배수가 아닙니다. 위 표의 구간들은 연속적이지 않고 절벽으로 나뉘어 있습니다. 평균 지연이 아무리 좋아도 절벽 하나를 밟으면 그 요청 하나가 백 배 느려집니다. 꼬리 지연을 다룰 때는 평균을 개선하는 것보다 절벽을 찾아 없애는 편이 효과적입니다.

동일한 명령어 스트림도 다른 코어의 활동에 영향받습니다. 1위 사례가 극단이지만 원리는 같습니다. 공유 자원은 마지막 레벨 캐시, 메모리 컨트롤러, 인터커넥트, 그리고 I/O 패브릭까지 이어집니다. 단독 실행 벤치마크가 프로덕션 성능을 예측하지 못하는 이유입니다.

실제로 걸려 넘어질 세 가지

마지막으로 이 표에서 실무와 직접 닿는 항목 셋을 정리합니다.

비정규 수. 표의 22, 17, 15위가 전부 이것입니다. 신호 처리나 물리 시뮬레이션처럼 값이 0으로 서서히 수렴하는 코드에서 실제로 나타납니다. 증상은 "입력 데이터에 따라 같은 코드가 갑자기 느려짐"입니다. 대응 방법은 하드웨어와 언어에 따라 다르므로(x86의 flush-to-zero 및 denormals-are-zero 모드가 대표적입니다) 각자 플랫폼 문서를 확인해야 합니다. 여기서 중요한 것은 이 절벽의 존재를 알고 있어야 원인을 찾을 수 있다는 점입니다.

분할 잠금. 표의 16위입니다. 리눅스 커널은 이 현상을 감지하는 기능을 제공합니다. 커널 문서는 분할 잠금을 "피연산자가 두 캐시 라인에 걸친 모든 원자 연산"으로 정의하고, 버스 잠금은 "쓰기 저장 메모리에 대한 분할 잠금 접근이나 쓰기 저장이 아닌 메모리에 대한 모든 잠금 접근"으로 정의합니다. 부팅 파라미터 split_lock_detectoff, warn, fatal, ratelimit:N 값을 받고, 문서에 기재된 기본값은 warn입니다. 커널 로그에 관련 경고가 찍히고 있다면 그건 실제 성능 문제일 가능성이 높습니다.

MMIO를 메모리처럼 다루는 코드. 표의 상위권이 통째로 이 이야기입니다. 문법은 mov 하나지만 비용은 DRAM 접근의 백만 배일 수 있습니다. 드라이버나 임베디드 코드에서 장치 레지스터를 루프 안에서 읽고 있다면, 그 루프는 계산이 아니라 I/O 왕복의 반복입니다. 상태 폴링을 인터럽트로 바꾸거나, 여러 레지스터를 한 번에 읽거나, 읽기 대신 쓰기로 바꿀 수 있는지 검토할 가치가 있습니다.

저장소는 아직 x86 순위표만 채워져 있고 ARM과 RISC-V는 준비 중이라고 되어 있습니다. 다른 아키텍처의 목록이 채워지면 어떤 절벽이 x86 고유이고 어떤 것이 현대 SoC 일반의 성질인지 더 분명해질 것입니다.

참고 자료

저는 이 순위표의 어떤 항목도 직접 실행하지 않았습니다. 상당수가 시스템을 정지시키거나 장치를 불안정하게 만드는 조작이며, 저장소의 측정치는 특정 하드웨어(주로 Intel Core i7-8559U와 AMD Ryzen 7 5800H)에 대한 값입니다.

현재 단락 (1/80)

명령어 지연표는 최적화하는 사람의 기본 도구입니다. 곱셈이 3사이클, 나눗셈이 20사이클, 이런 식으로 외워 두고 코드를 짭니다.

작성 글자: 0원문 글자: 5,317작성 단락: 0/80