컴퓨터 시스템 : CSAPP 3장 정리 - 3.11 부동소수점 코드
📘 3.11 Floating-Point Code
부동소수점 연산은 일반적인 정수 연산과는 달리 복잡한 구조와 특별한 규칙을 따릅니다. 이 절의 도입부에서는 다음과 같은 핵심 요소들을 설명한다:
1. 부동소수점 아키텍처란?
부동소수점 아키텍처는 다음 네 가지 측면에서 프로그램이 부동소수점 데이터를 어떻게 다루는지를 규정한다:
- 저장 및 접근 방법: 대부분 레지스터를 사용해 저장하고 접근함.
- 작동하는 명령어 세트: 특정 명령어들이 부동소수점 데이터를 처리함.
- 함수 호출 시 인자 및 반환값 처리 규칙: 예를 들어, 부동소수점 인자는 %xmm0~%xmm7에 저장됨.
- 레지스터 저장 규칙: 어떤 레지스터가 caller-saved 또는 callee-saved인지 규정.
2. 역사적 배경
x86-64에서 부동소수점 연산은 SIMD (Single Instruction, Multiple Data) 구조에서 발전해왔다.
- MMX → SSE → AVX 순으로 확장됨
- 각각의 확장마다 레지스터 구조가 강화되었음:
- MM: 64비트
- XMM: 128비트
- YMM: 256비트
이러한 구조는 벡터 연산과 같은 고속 연산을 가능하게 했고, 현재의 AVX2 기반 부동소수점 연산 방식으로 이어졌다.
3. AVX2 기반 설명
이 책에서 설명하는 구조는 AVX2 (2013년 Core i7 Haswell부터 도입)를 기준으로 한다. 주요 특징은 다음과 같다:
- 레지스터 %xmm0 ~ %xmm15 및 %ymm0 ~ %ymm15 사용
- 부동소수점 스칼라 연산은 XMM의 하위 4바이트 (float) 또는 8바이트 (double)를 사용
- gcc는 -mavx2 옵션으로 AVX2 코드를 생성
4. 어셈블리 언어의 형식
- 이 책에서는 AT&T 포맷을 사용한다.
- 이는 인텔의 표준 포맷과 다르며, 피연산자의 순서가 반대이다.
- 예: addl %eax, %ebx → ebx = ebx + eax
3.11.1 부동소수점 이동 및 변환 연산
1. 이동(Move) 명령어
주요 명령어 요약 (그림 3.46 기준)
명령어 | 원본 <-> 대상 | 설명 |
vmovss | M32 ↔ X | 단정도(float) 이동 |
vmovsd | M64 ↔ X | 배정도(double) 이동 |
vmovaps | X → X | 단정도, 정렬된 레지스터 간 이동 |
vmovapd | X → X | 배정도, 정렬된 레지스터 간 이동 |
- M32/M64: 각각 32비트 또는 64비트 메모리 주소
- X: xmm 레지스터
이동 연산은 데이터 변환 없이 복사만 수행합니다. 메모리 ↔ 레지스터 간 이동은 스칼라 연산으로, 단일 데이터에만 작동한다.
⚠️ 정렬과 관련된 주의
- vmovss, vmovsd는 정렬에 민감하지 않음.
- vmovaps, vmovapd는 16바이트 정렬이 맞지 않으면 예외 발생.
2. 변환(Conversion) 명령어
2-1. 부동소수점 → 정수 변환 (그림 3.47 기준)
명령어 | 원본 - 대상 | 설명 |
vcvttss2si | X/M32 → R32 | 단정도 → 정수, 내림(truncation) |
vcvttsd2si | X/M64 → R32 | 배정도 → 정수 |
vcvttss2siq | X/M32 → R64 | 단정도 → 정수(64비트) |
vcvttsd2siq | X/M64 → R64 | 배정도 → 정수(64비트) |
이들 명령은 내림(round toward zero) 방식으로 변환한다. C 언어의 (int)x 캐스팅과 유사하다.
2-2. 정수 → 부동소수점 변환 (그림 3.48 기준)
이 명령어는 3-피연산자 형태를 취합니다:
명령어 | 소스1 | 소스2 | 대상 | 설명 |
vcvtsi2ss | M32/R32 | X | X | 정수 → 단정도 변환 |
vcvtsi2sd | M32/R32 | X | X | 정수 → 배정도 변환 |
vcvtsi2ssq | M64/R64 | X | X | 64비트 정수 → 단정도 변환 |
vcvtsi2sdq | M64/R64 | X | X | 64비트 정수 → 배정도 변환 |
- 소스 2는 상위 바이트를 유지하는 용도이며, 일반적으로 소스 2와 대상이 동일하다.
- 예: vcvtsi2sdq %rax, %xmm1, %xmm1은 %rax의 정수를 double로 바꿔 %xmm1에 저장.
예제: float float_mov(float v1, float *src, float *dst)
float float_mov(float v1, float *src, float *dst) {
float v2 = *src;
*dst = v1;
return v2;
}
컴파일 결과:
1 float_mov:
2 vmovaps %xmm0, %xmm1 ; v1을 %xmm1로 복사
3 vmovss (%rdi), %xmm0 ; *src → %xmm0
4 vmovss %xmm1, (%rsi) ; %xmm1 → *dst
5 ret ; v2 반환 (%xmm0)
- %xmm0은 함수 인자 v1 및 반환값으로 사용됨.
- vmovss는 단정도 이동이므로 4바이트 이동.
- %rdi는 src, %rsi는 dst 포인터를 담고 있음.
3.11.2 프로시저에서 부동소수점 코드
1. 함수 인자 전달 및 반환 규칙
부동소수점 인자는 레지스터 %xmm0부터 %xmm7까지 총 8개를 사용하여 전달된다:
- 인자의 순서에 따라 %xmm0부터 차례대로 할당
- 인자가 8개를 초과하면 스택을 사용하여 전달
- 반환값은 항상 %xmm0에 저장되어 반환됨
예시:
double f(double a, double b, double c, double d, double e, double f, double g, double h, double i);
이 경우 a - h 까지는 %xmm0 - %xmm7에 저장되고, i는 스택에 저장된다.
2. Caller-saved 규칙
- 모든 %xmm 레지스터는 caller-saved
- 즉, 함수가 호출되면 호출한 쪽에서 레지스터를 백업해야 하며, 호출된 함수는 마음대로 덮어써도 된다.
3. 다양한 인자 조합 처리 방식
함수가 포인터, 정수, 부동소수점을 혼합하여 인자로 받을 경우, 다음 규칙을 따른다:
- 포인터, 정수: 일반 범용 레지스터 (예: %rdi, %rsi, %rdx, ...)
- 부동소수점: %xmm0부터 차례대로
예제 1:
double f1(int x, double y, long z);
- x → %edi (정수)
- y → %xmm0 (double)
- z → %rsi (long)
예제 2:
double f2(double y, int x, long z);
→ 순서가 달라도 레지스터 배정은 타입 기준으로 결정되므로 동일함
예제 3:
double f3(float x, double *y, long *z);
- x → %xmm0 (float)
- y → %rdi (포인터)
- z → %rsi (포인터)
3.11.3 부동 소수점 산술연산
1. AVX2 부동소수점 산술 명령어
그림 3.49는 단일 명령으로 부동소수점 연산을 수행하는 스칼라 AVX2 명령어를 보여준다. 이들 명령은 다음과 같은 특징을 가진다:
- 피연산자: S1, S2 (소스), D (목적지)
- S1: 레지스터 또는 메모리
- S2, D: 반드시 XMM 레지스터
- 연산은 단정도(float) 또는 배정도(double)로 나뉨
- 결과는 목적지 레지스터 D에 저장
단정도 명령어 | 배정도 명령어 | 의미 |
vaddss | vaddsd | 덧셈 D ← S2 + S1 |
vsubss | vsubsd | 뺄셈 D ← S2 − S1 |
vmulss | vmulsd | 곱셈 D ← S2 × S1 |
vdivss | vdivsd | 나눗셈 D ← S2 / S1 |
vmaxss | vmaxsd | 최대값 D ← max(S2, S1) |
vminss | vminsd | 최소값 D ← min(S2, S1) |
sqrtss | sqrtsd | 제곱근 D ← √S1 |
2. 예제 분석: double funct(double a, float x, double b, int i)
double funct(double a, float x, double b, int i) {
return a * x - b / i;
}
레지스터 할당:
- a: %xmm0
- x: %xmm1
- b: %xmm2
- i: %edi
어셈블리:
1 funct:
2 vunpcklps %xmm1, %xmm1, %xmm1
3 vcvtps2pd %xmm1, %xmm1 ; x를 double로 변환
4 vmulsd %xmm0, %xmm1, %xmm0 ; a * x
5 vcvtsi2sd %edi, %xmm1, %xmm1 ; i를 double로 변환
6 vdivsd %xmm1, %xmm2, %xmm2 ; b / i
7 vsubsd %xmm2, %xmm0, %xmm0 ; (a*x) - (b/i)
8 ret
💡 주목할 점
- x는 float이므로 double로 변환 필요 (vcvtps2pd)
- i는 int이므로 vcvtsi2sd로 double 변환
- float ↔ double 간 변환, int ↔ float/double 간 변환이 많음
3.11.4 부동 소수점 상수의 정의 및 이용
🔍 핵심 개념
❗ AVX 명령어의 제약
- AVX 부동소수점 연산 명령어는 정수 연산처럼 즉시 상수 값을 사용할 수 없음
- 대신, 메모리에 상수를 저장하고 이를 로드하여 사용해야 함
예제: 섭씨를 화씨로 변환
C 코드
double cel2fahr(double temp) {
return 1.8 * temp + 32.0;
}
어셈블리 코드
1 cel2fahr:
2 vmulsd .LC2(%rip), %xmm0, %xmm0 ; temp × 1.8
3 vaddsd .LC3(%rip), %xmm0, %xmm0 ; + 32.0
4 ret
- %xmm0에 인자 temp가 들어 있고, 최종 결과도 %xmm0에 저장되어 반환됨
- .LC2, .LC3는 각각 1.8과 32.0을 저장한 메모리 주소
상수의 메모리 표현 (.long)
5 .LC2:
6 .long 3435973837 ; 하위 4바이트 (1.8)
7 .long 1073532108 ; 상위 4바이트 (1.8)
8 .LC3:
9 .long 0 ; 하위 4바이트 (32.0)
10 .long 1077936128 ; 상위 4바이트 (32.0)
- AVX는 리틀 엔디안이므로 낮은 주소에 하위 바이트가 먼저 저장됨
- .LC2:
- 0xCCCCCCCD (3435973837)
- 0x3FFCCCC (1073532108)
- 이 조합은 IEEE 754 표현으로 1.8을 나타냄
- .LC3의 두 값은 32.0을 나타냄
요점 정리
- AVX 명령어는 즉시 상수를 직접 사용할 수 없음
- 상수는 데이터 섹션에 저장되고, .LCx와 같은 레이블을 통해 참조
- .long으로 표현된 두 개의 32비트 정수는 64비트 double 형식 상수의 하위/상위 바이트
- 이는 부동소수점 데이터의 이진 표현에 대한 깊은 이해를 필요로 함
3.11.5 부동 소수점 코드에서 비트연산 사용하기
핵심 아이디어
AVX2 명령어 중 일부는 비트 단위 연산 (bitwise operation)을 지원하며, 이들은 부동소수점 값의 특정 비트를 직접 조작할 수 있는 도구가 된다.
📌 주의점
- 이러한 명령어는 packed data에 작동합니다 (즉, 128비트 전체에 대해 수행).
- 하지만 우리는 보통 하위 8바이트 (double) 혹은 하위 4바이트 (float)만 사용하므로, 나머지는 무시된다.
- 이러한 방식은 정교하고 효율적인 방식으로 부동소수점 값을 조작하는 데 사용될 수 있다.
주요 명령어 (그림 3.50)

명령어 | 의미 |
vxorps, xorpd | 비트 XOR (배타적 논리합) |
vandps, andpd | 비트 AND (논리곱) |
- xorpd: 두 레지스터의 내용을 XOR하여 새로운 레지스터에 저장
- andpd: 두 레지스터의 내용을 AND하여 저장
예제: 실용적인 활용
예제 A
vmovsd .LC1(%rip), %xmm1
vandpd %xmm1, %xmm0, %xmm0
- .LC1은 특정 마스크 비트를 담은 상수 (예: sign bit 제거)
- vandpd는 이 마스크를 적용하여 특정 비트만 남김 (예: 절댓값 계산)
예제 B
vxorpd %xmm0, %xmm0, %xmm0
- 자기 자신과 XOR하면 0이 됨
- xmm0을 0으로 초기화하는 간단하고 빠른 방법
예제 C
vmovsd .LC2(%rip), %xmm1
vxorpd %xmm1, %xmm0, %xmm0
- .LC2는 sign bit만 1인 값
- vxorpd는 sign bit만 반전 → 부호 반전 (예: -x)
요점 정리
- 정수 연산처럼, 부동소수점에서도 비트 연산이 간편한 초기화, 절댓값 계산, 부호 반전 등에 매우 유용하게 사용된다.
- 이는 함수 호출과 연산보다 훨씬 빠른 저수준 최적화 기술이다.
- 특히 컴파일러는 이러한 연산을 자동으로 생성하기도 한다.
3.11.6 부동 소수점 비교 연산
핵심 비교 명령어
명령어 | 설명 |
ucomiss | 단정도(float) 비교 |
ucomisd | 배정도(double) 비교 |
- 이 명령어들은 **S2 - S1**을 계산하여 비교 결과에 따라 조건 코드 (ZF, CF, PF)를 설정한다.
- AT&T 문법에서는 피연산자 순서가 반대입니다: ucomisd S1, S2는 실제로 S2 - S1을 의미한다.
조건 코드 설정
조건 | CF | ZF | PF |
Unordered (NaN 포함) | 1 | 1 | 1 |
S2 < S1 | 1 | 0 | 0 |
S2 = S1 | 0 | 1 | 0 |
S2 > S1 | 0 | 0 | 0 |
- PF (Parity Flag)는 하나라도 NaN이면 1로 설정됨
- ZF는 두 값이 같을 때, CF는 S2 < S1일 때 설정
- NaN 검출: jp (jump on parity) 명령어로 감지 가능
예제: 값의 범위 분류 함수
typedef enum {NEG, ZERO, POS, OTHER} range_t;
range_t find_range(float x) {
int result;
if (x < 0) result = NEG;
else if (x == 0) result = ZERO;
else if (x > 0) result = POS;
else result = OTHER;
return result;
}
어셈블리 분석
1 find_range:
2 vxorps %xmm1, %xmm1, %xmm1 ; %xmm1 = 0.0
3 vucomiss %xmm0, %xmm1 ; 비교: 0 - x
4 ja .L5 ; x < 0 이면 NEG
5 vucomiss %xmm1, %xmm0 ; 비교: x - 0
6 jp .L8 ; NaN이면 OTHER
7 movl $1, %eax ; result = ZERO
8 je .L3 ; x == 0 이면 종료
9 .L8: ; x > 0 혹은 NaN
10 vucomiss .LC0(%rip), %xmm0 ; 0.0과 비교
11 setbe %al ; NaN이면 1, 아니면 0
12 movzbl %al, %eax
13 addl $2, %eax ; result = POS 또는 OTHER
14 ret
15 .L5: movl $0, %eax ; result = NEG
16 .L3: rep; ret
흐름 설명
- x < 0 → NEG
- x == 0 → ZERO
- x > 0 → POS
- x == NaN → OTHER (jp 분기로 감지)
요점 정리
- ucomiss, ucomisd는 AVX2에서 스칼라 부동소수점 비교에 사용됨
- 비교 결과는 ZF, CF, PF 플래그로 표현되고, 조건부 분기 명령어 (je, ja, jp 등)와 함께 사용됨
- NaN 검출에는 PF (parity flag)를 사용하며, 이는 일반 정수 연산에서는 거의 쓰이지 않음
3.11.7 부동 소수점 코드에 대한 관찰
핵심 관찰 요약
유사성: 정수 코드와의 공통점
- AVX2 부동소수점 연산은 정수 연산처럼 레지스터 기반으로 수행된다.
- 부동소수점 연산도 XMM 레지스터를 사용하여 데이터를 전달하고, 결과를 반환한다.
- 함수 인자 전달 및 반환에서의 레지스터 사용 규칙이 명확하게 정의되어 있음
차이점: 다양한 데이터 형식과 명령어
- 부동소수점은 데이터 형식 변환, 비트 연산, 예외 처리(NaN, Inf 등), 정렬 조건 등을 고려해야 한다.
- AVX2 기반 부동소수점 명령어는 정수 연산보다 훨씬 다양하고 복잡한 형식을 가진다.
- 특히 스칼라 vs 벡터 모드, 단정도 vs 배정도, 비트 연산 포함 여부에 따라 명령어가 분기된다.
병렬화와 SIMD
- AVX2는 벡터화(parallelization) 기능을 통해 여러 데이터를 동시에 처리할 수 있음
- 예: YMM 레지스터는 256비트 → 4개의 double 혹은 8개의 float를 동시에 처리 가능
- 하지만 컴파일러가 자동으로 스칼라 → 벡터 코드 변환하는 데는 한계가 있음
- GCC는 벡터화 최적화를 위해 C 확장을 제공함 (#pragma, vector types 등)
결론
- AVX2 기반 부동소수점 연산은 정수 연산과 유사한 점도 많지만, 그 복잡성과 처리 방식에서 큰 차이가 있음
- 고성능 부동소수점 연산을 위해서는 컴파일러 최적화 외에도 프로그래머가 직접 벡터 연산을 설계하는 경우도 많음
- 부동소수점 코드의 이해는 C 추상화를 넘어서 기계 수준의 정확한 모델링을 가능케 해 줌