LabHub

컴퓨터 구조 · CPU · 이론

명령어 하나가 62초 걸릴 수 있다 — 지연은 경로의 속성이다

LabHub 에서 이어서 보기

한 줄 요약

"이 명령어는 몇 사이클인가"라는 질문은 주변 상태를 고정하지 않으면 답이 없다. 지연은 명령어의 속성이 아니라 명령어와 시스템 상태의 조합에 대한 관측치다.

왜 이게 필요했나

최적화를 배우면 명령어 지연표부터 외운다. 덧셈 1사이클, 곱셈 3사이클, 나눗셈 20사이클. 이 표는 유용하지만, 그 숫자를 명령어의 고정된 성질로 믿는 순간 실제 성능을 설명하지 못하게 된다.

2026년에 공개된 Assembly Hall of Shame 이라는 순위표가 이 점을 극단적으로 보여 준다. 부제가 "CPU 성능의 바닥을 향한 경주"인데, 명령어 하나를 가장 느리게 만드는 경쟁의 기록이다. 최하위 nop 이 1사이클, 1위 fxrstor64 가 1,980억 사이클로 62초다. 같은 부류 안에서 2,000억 배 차이가 난다면, 그 부류를 하나의 숫자로 재고 있다는 전제부터 의심해야 한다.

어떻게 동작하나

그 순위표를 아래에서 위로 읽으면 현대 CPU 가 멈출 수 있는 지점의 목록이 된다. 세 구간으로 나뉜다.

첫째, 코어 안에서 마이크로코드가 끼어드는 지점. 하드웨어 고속 경로가 처리하지 못하는 입력을 주면 제어가 마이크로코드 루틴으로 넘어간다. fadd 는 정상 피연산자로는 몇 사이클이지만, 피연산자가 비정규 수(denormal)이면 677사이클이 걸린다. 명령어는 그대로이고 데이터만 바뀌었는데 두 자릿수 배율이 생긴다.

둘째, 코히런시와 플랫폼이 개입하는 지점. 원자 연산의 피연산자가 캐시 라인 경계에 걸치면(분할 잠금, split lock) 빠른 캐시 일관성 경로를 쓸 수 없어 외부 버스 잠금을 걸어야 한다. 865사이클이 걸리고 그동안 다른 코어까지 영향을 받는다. 캐시 전체를 비우는 wbinvd 는 160만 사이클인데, 이건 명령어가 복잡해서가 아니라 그 시점에 캐시에 들어 있던 더티 데이터를 전부 DRAM 까지 밀어내야 하기 때문이다.

셋째, 다이 밖으로 나가는 순간. 상위권은 전부 mov 다. 데이터를 옮기는 것 말고는 아무 일도 하지 않는 명령어가 1초 넘게 걸린다. 이유는 주소에 있다. DRAM 이 아니라 PCIe 패브릭 너머 장치 레지스터(MMIO)를 읽고 있기 때문이다. MMIO 읽기는 응답을 받아야 끝나는 논포스티드 트랜잭션이라 왕복 시간이 그대로 실행 시간이 된다.

1위의 전략이 결정적이다. 512바이트 상태를 가장 느린 MMIO 영역에서 복원하게 하면 23초가 나온다. 여기에 다른 코어들이 다른 MMIO 레지스터를 계속 두드리게 하면 62초가 된다. 즉 명령어 하나의 실행 시간이 다른 코어가 무엇을 하고 있는지에 따라 두 배 이상 달라졌다.

현장에서 만나는 모습

이 극단적인 놀이의 결론은 평범한 성능 작업에 그대로 적용된다.

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

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

실무에서 실제로 밟게 되는 절벽은 세 가지다. 값이 0으로 수렴하는 신호 처리 코드의 비정규 수, 정렬을 신경 쓰지 않은 원자 연산의 분할 잠금(리눅스 커널의 split_lock_detect 가 기본값 warn 으로 경고를 찍어 준다), 그리고 장치 레지스터를 루프 안에서 읽는 MMIO 폴링이다.

이어지는 퀴즈에서 확인할 것

"이 코드가 왜 어제는 빠르고 오늘은 느린가"라는 질문에 명령어 목록이 아니라 경로와 상태로 답할 수 있는지 확인한다.