x86에서 레지스터를 0으로 만드는 데 왜 mov 대신 xor eax, eax를 쓰는지, 그리고 이것이 코드 크기와 CPU 성능에 어떤 이점을 주는지 설명한다.
나가 쓴 글이고, LLM이 교정해 주었다.
자세한 내용은 맨 끝에.
어셈블리 관련 내 발표 중 하나에서, 나는 일반적인 x86 리눅스 데스크톱에서 가장 많이 실행되는 명령어 20개의 목록을 보여준다(슬라이드 링크). mov, add, lea, sub, jmp, call 같은 익숙한 녀석들이 전부 등장하지만, 의외의 침입자가 하나 있다. 바로 xor — “eXclusive OR”이다. 6502를 가지고 놀던 시절, exclusive OR가 코드에 등장한다는 건 거의 확실하게 암호화 루틴이거나, 스프라이트 루틴 같은 걸 찾아냈다는 뜻이었다. 그런데 리눅스 머신이 그냥 자기 할 일만 하고 있는데, 그런 명령어가 그렇게 많이 실행된다는 건 꽤 놀라운 일이다.
그렇게까지 놀라운 일도 아니다. 컴파일러가 레지스터를 0으로 만들 때 xor을 내보내는 걸 아주 좋아한다는 걸 떠올리면 말이다.
우리는 어떤 값을 자기 자신과 exclusive-OR 하면 0이 된다는 걸 알고 있다. 그런데 왜 컴파일러가 굳이 이런 시퀀스를 내보내는 걸까? 그냥 뽐내는 걸까?
위 예제에서 나는 -O2로 컴파일했고, CPU가 실제로 보는 기계어를 볼 수 있도록 Compiler Explorer의 “Compile to binary object”를 켜 두었다. 특히 이 코드는 다음과 같다:
asm31 c0 xor eax, eax c3 ret
GCC의 최적화 수준을 -O1로 낮추면 이렇게 바뀐 걸 볼 수 있다:
asmb8 00 00 00 00 mov eax, 0x0 c3 ret
EAX 레지스터를 0으로 만들겠다는 의도가 훨씬 분명하게 드러나는 mov eax, 0은 5바이트를 차지하는 반면, exclusive OR 버전은 2바이트면 된다. 조금 더 난해한 명령어를 쓰는 대신, 레지스터를 0으로 만들어야 할 때마다 3바이트를 아낄 수 있다. 레지스터를 0으로 만드는 건 꽤 흔한 연산이니, 이 차이는 커진다. 바이트를 절약하면 프로그램이 더 작아지고, 명령어 캐시를 더 효율적으로 쓸 수 있다.
여기서 더 좋아진다! 이건 정말 매우 흔한 연산이기 때문에, x86 CPU는 파이프라인의 아주 앞 단계에서 이 “zeroing idiom(0으로 만드는 관용 패턴)”을 인지하고 특별히 최적화할 수 있다. out-of-order 추적 시스템은 eax(또는 0으로 만드는 대상 레지스터)의 값이 예전 eax 값에 의존하지 않는다는 걸 안다. 그래서 의존성이 없는, 새로 0으로 된 레지스터 리네이머 슬롯을 할당할 수 있다. 그리고 그렇게 하고 나면, 실행 큐에서 이 연산을 아예 제거해 버린다 — 즉, 이 xor은 실행 사이클을 0개 사용한다 사실상 CPU 차원에서 최적화로 사라져 버리는 것이다!
아마 이런 생각이 들 수도 있다. 왜 xor rax, rax(64비트 버전)는 거의 보이지 않고, long을 반환할 때도 xor eax, eax만 보일까?
이 경우, 결과로 나오는 64비트 long 전체를 담기 위해선 rax가 필요하다. 그런데 eax에 쓰기를 하면 멋진 효과가 있다. 다른 부분 레지스터 쓰기와 달리, eax처럼 e 레지스터에 쓰기를 하면 아키텍처가 상위 32비트를 공짜로 0으로 만들어 준다. 그래서 xor eax, eax는 64비트 전체를 0으로 만든다.
흥미롭게도, “확장” 번호 레지스터들(r8 같은)을 0으로 만들 때도 GCC는 여전히 d (더블 폭, 즉 32비트) 변종을 사용한다:
(역자 주: 예시 코드에서) xor r8d, r8d(32비트 변종)을 쓰는데, 사실 REX 프리픽스(여기서는 45)가 붙어 있기 때문에 전체 폭을 대상으로 하는 xor r8, r8을 써도 바이트 수는 똑같다. 아마 컴파일러 쪽 구현을 단순하게 만드는 이점이 있기 때문일 것이다. Clang도 이렇게 한다.
xor eax, eax는 코드 공간도 아끼고 실행 시간도 줄여 준다! 고마워요, 컴파일러들!
이 글과 함께 올라간 영상도 참고해 보자.
이 글은 **Advent of Compiler Optimisations 2025**의 1일 차 글이다. 이 시리즈는 25일 동안, 컴파일러가 코드를 어떻게 변형하는지 살펴본다.
이 글은 인간(Matt Godbolt)이 썼으며, LLM과 인간이 함께 검토 및 교정을 했다.
Patreon이나 GitHub 후원을 통해, 혹은 Compiler Explorer Shop에서 CE 관련 상품을 구입하여 Compiler Explorer를 후원할 수 있다.
게시 시각: 약 13시간 전.