← SEARCHABLE ARCHIVE
Assembly · 2026-08-15

x86-64 부동소수점과 SIMD 입문

XMM 스칼라 연산부터 packed SSE·AVX, 정렬과 기능 감지까지

INTERACTIVE READING MODE →

x86-64 부동소수점과 SIMD 입문

본 블로그 작성 글들은 강의를 목표로 제작되었습니다. 예제는 SSE2를 사용할 수 있는 x86-64 사용자 모드를 출발점으로 하며 AVX·AVX2·AVX-512 사용 가능 여부는 CPU와 운영체제 상태를 함께 확인해야 합니다.

x86-64의 부동소수점 코드를 읽을 때는 “XMM 레지스터가 보인다”는 사실만으로 vectorized 코드라고 결론 내리면 안 된다. addss는 XMM의 낮은 float 한 개만 더하고, addps는 float 네 개를 병렬로 더한다. 같은 레지스터 파일이 scalar와 packed 연산 모두에 쓰인다.

x87, SSE, AVX를 구분한다

  • x87: 80비트 확장 정밀도 stack register 모델의 오래된 부동소수점 경로
  • SSE/SSE2: XMM 레지스터, 2피연산자 인코딩 중심
  • AVX: YMM 확장과 VEX 기반 3피연산자 형태
  • AVX-512: ZMM, mask register, EVEX 기능 추가

현대 x86-64 C/C++ compiler는 일반 scalar float와 double에 SSE 계열을 주로 사용한다. x87이 보이면 32비트 호환 코드, 특수 long double 환경, 오래된 옵션을 의심할 수 있지만 대상 ABI를 확인해야 한다.

XMM은 128비트 저장 공간이다

128비트를 어떻게 나누는지는 명령이 정한다.

packed float32 : 4 lanes × 32 bit
packed float64 : 2 lanes × 64 bit
scalar float32 : low 32 bit 사용
scalar float64 : low 64 bit 사용
packed int32   : 4 lanes × 32 bit

레지스터 이름만 보고 타입을 알 수 없다. addss, addsd, addps, addpd, paddd 같은 opcode suffix를 본다.

scalar float과 double

; xmm0 = a, xmm1 = b
addss xmm0, xmm1     ; 낮은 float32 한 개
addsd xmm0, xmm1     ; 낮은 float64 한 개

SSE의 scalar 2피연산자 형태는 destination의 일부 상위 lane을 유지할 수 있다. AVX의 VEX 형태는 별도의 source를 가진다.

vaddss xmm0, xmm1, xmm2
vaddsd xmm0, xmm1, xmm2

scalar 결과가 xmm0에 반환되는 것은 ABI 계약이고, 산술 명령의 하드웨어 의미와 구분한다.

packed 연산은 lane별이다

movups xmm0, [rcx]
movups xmm1, [rdx]
addps  xmm0, xmm1
movups [r8], xmm0

float 네 개를 각각 더한다.

xmm0 = [a0, a1, a2, a3]
xmm1 = [b0, b1, b2, b3]
결과 = [a0+b0, a1+b1, a2+b2, a3+b3]

lane 사이 carry가 전달되는 큰 128비트 정수 덧셈이 아니다. integer packed 명령도 paddb, paddw, paddd, paddq처럼 lane 폭을 명시한다.

aligned와 unaligned load

movaps xmm0, [rcx]    ; aligned packed single
movups xmm0, [rcx]    ; unaligned packed single
movdqa xmm0, [rcx]    ; aligned integer data
movdqu xmm0, [rcx]    ; unaligned integer data

aligned 형태에 정렬되지 않은 주소를 주면 fault가 날 수 있다. modern CPU에서 정렬된 주소를 movups로 읽는 비용이 같거나 가까운 경우가 많더라도, cache line·page 경계와 대상 microarchitecture를 측정한다.

stack 또는 heap pointer가 16바이트 정렬이라는 일반 규칙만으로 모든 subobject가 정렬되지는 않는다. base에 더한 field offset도 확인한다.

산술 순서가 결과를 바꾼다

IEEE 754 부동소수점 덧셈은 결합법칙을 만족하지 않는다.

(a + b) + c  !=  a + (b + c)

vectorization과 reduction tree는 합산 순서를 바꾼다. scalar 기준 구현과 마지막 bit까지 같지 않을 수 있다. 허용 오차, NaN, infinity, signed zero와 rounding mode를 테스트 계약에 포함한다.

-ffast-math나 MSVC의 빠른 부동소수점 옵션은 compiler가 더 강한 수학 가정을 하게 할 수 있다. 옵션을 숨긴 채 결과 차이를 “SIMD 버그”라고 단정하지 않는다.

비교는 ordered와 unordered를 본다

NaN이 포함되면 일반 대소 비교가 unordered가 된다.

ucomiss xmm0, xmm1
jp unordered_case     ; PF로 NaN 관련 unordered 확인
ja above_case

ucomiss/ucomisd, comiss/comisd는 NaN 예외 동작의 세부가 다르다. 조건 분기 한 줄만 보지 말고 어떤 compare opcode가 플래그를 만들었는지 확인한다.

packed compare는 각 lane에 all-ones 또는 zero mask를 만들 수 있다. 그 mask는 andps, blend, movemask 등에 연결된다.

shuffle은 lane을 재배치한다

shufps xmm0, xmm1, imm8
pshufd xmm0, xmm1, imm8
unpcklps xmm0, xmm1

즉시값 bit가 어떤 source lane을 고르는지 instruction reference로 해석한다. shuffle이 많으면 AoS↔SoA 변환, transpose, horizontal reduction, broadcast의 단서가 된다.

dot product와 horizontal reduction

float 네 개의 내적은 lane 곱셈 뒤 합산이 필요하다.

movups xmm0, [rcx]
movups xmm1, [rdx]
mulps xmm0, xmm1
; 이후 shuffle/add로 4 lane을 하나로 reduction

SSE3의 haddps나 SSE4.1의 dpps를 사용할 수도 있지만 항상 최적인 것은 아니다. 대상 CPU의 throughput, dependency, 정확성 요구와 compiler 결과를 비교한다.

AVX의 3피연산자와 상위 상태

vaddps ymm0, ymm1, ymm2

두 source를 보존하며 destination을 따로 지정할 수 있다. YMM은 256비트로 float 8개를 처리한다. 하지만 AVX 명령과 legacy SSE 코드를 섞을 때 상위 YMM 상태 때문에 transition penalty가 있던 CPU가 있다. AVX 코드를 벗어나기 전에 vzeroupper가 보이는 이유다.

정확한 필요성과 비용은 CPU 세대에 따라 다르다. 무조건 삽입·삭제하지 않고 compiler convention과 측정을 따른다.

AVX를 지원한다고 AVX2도 지원하는 것은 아니다

instruction extension을 사용하려면 다음을 구분한다.

  1. CPU가 CPUID bit를 제공하는가?
  2. OS가 확장 register state 저장을 지원하는가?
  3. XGETBV로 필요한 XMM/YMM state가 활성화되어 있는가?
  4. binary가 실행될 모든 CPU에서 해당 기능을 보장하는가?

AVX2와 AVX-512는 별도 feature다. 지원하지 않는 명령을 실행하면 illegal instruction fault가 난다. 런타임 dispatch, 여러 binary target, compiler multiversioning을 사용한다.

Windows와 System V의 vector register 보존 차이

System V AMD64의 일반 규약에서는 XMM register가 caller-saved다. Microsoft x64에서는 xmm6~xmm15의 하위 128비트가 nonvolatile이다. Windows handwritten ASM이 이 register를 사용하면 저장·복원과 unwind metadata가 필요하다.

넓은 YMM/ZMM의 상위 부분 보존 규칙은 낮은 XMM 부분과 동일하다고 단순화하면 안 된다. 대상 Microsoft 문서와 사용 extension을 확인한다.

SIMD tail과 overread

길이가 vector 폭의 배수가 아니면 tail 처리가 필요하다. 마지막 1~3개 float를 scalar로 처리하거나 mask load를 사용한다.

“최대 15바이트 더 읽어도 같은 할당 안이니 괜찮다”는 최적화는 언어 객체 경계, 페이지 경계, sanitizer, 동시 접근 계약을 깨뜨릴 수 있다. overread가 실제 API 계약으로 보장되지 않으면 사용하지 않는다.

auto-vectorization이 실패하는 이유

  • pointer overlap을 배제할 수 없음
  • loop-carried dependency가 있음
  • 함수 호출이나 예외 가능 동작이 있음
  • 반복 횟수가 너무 작거나 알 수 없음
  • 비용 모델상 이득이 없음
  • strict FP 순서를 유지해야 함
  • target ISA 옵션이 부족함

먼저 compiler vectorization report와 Release disassembly를 확인한다. handwritten ASM 전에 데이터 layout, restrict에 해당하는 실제 계약, compiler intrinsic을 검토한다.

SIMD 검증 체크리스트

  1. scalar인가 packed인가, lane 폭은?
  2. 정렬 전제가 실제 모든 pointer+offset에서 성립하는가?
  3. 0개·vector 폭 미만·정확한 배수·나머지 입력을 테스트했는가?
  4. NaN, infinity, signed zero, 큰/작은 값과 허용 오차는?
  5. feature detection에 CPU와 OS state가 모두 포함되는가?
  6. ABI의 nonvolatile vector state를 보존하는가?
  7. compiler/intrinsic보다 빠른지 실제 병목에서 측정했는가?

정리

XMM 레지스터는 타입이 아니라 128비트 저장 공간이며 opcode가 scalar·packed, float·double·integer lane 해석을 정한다. SIMD는 여러 원소를 한 번에 처리하지만 alignment, tail, FP 재결합, feature detection과 ABI 보존이라는 추가 계약을 만든다.

vector 명령이 많다는 사실만으로 빠르다고 결론 내리지 않는다. 정확한 lane 의미를 복원하고 scalar 기준 구현과 경계값을 검증한 뒤 실제 workload에서 측정한다.

공식 참고 자료