이 내용 이전에 gdb 사용법, 버퍼 오버플로, 메모리 보호기법, 가변 크기 스택프레임에 대한 내용이 있었다.
그런데 글로 정리하려니 잘 안되서 그냥 생략하고 부동소수점을 정리하기로 했다.
부동소수점 아키텍처는 대략 아래와 같이 발전했다.
(1) MMX
MM 레지스터를 사용하며, 64비트다.
(2) SSE
128비트로 확장했다. XMM 레지스터가 추가됐다. %xmm0부터 %xmm15까지 있다.
MM 레지스터는 없다.
x86-64 코드를 실행할 수 있는 모든 프로세서들은 SSE2 이상을 지원한다.
(3) AVX
256비트로 확장했다. YMM 레지스터가 추가됐다. %ymm0부터 %ymm15까지 있다.
인텔 샌디브릿지 아키텍처부터 지원한다.
(4) AVX2
인스트럭션이 추가됐다.
인텔 하스웰 아키텍처부터 지원한다.
(5) AVX-512
512바이트로 확장했다. ZMM 레지스터가 추가됐다. %zmm0부터 %zmm31까지 있다.
인텔 스카이레이크 아키텍처부터 지원한다.
책은 AVX2에 기초해있으며, 이 글도 책을 따른다.
1. AVX 레지스터
256bit | 128bit | |
---|---|---|
%ymm0 | %xmm0 | 1st FP arg./Return value |
%ymm1 | %xmm1 | 2nd FP argument |
%ymm2 | %xmm2 | 3rd FP argument |
%ymm3 | %xmm3 | 4th FP argument |
%ymm4 | %xmm4 | 5th FP argument |
%ymm5 | %xmm5 | 6th FP argument |
%ymm6 | %xmm6 | 7th FP argument |
%ymm7 | %xmm7 | 8th FP argument |
%ymm8 | %xmm8 | Caller saved |
%ymm9 | %xmm9 | Caller saved |
%ymm10 | %xmm10 | Caller saved |
%ymm11 | %xmm11 | Caller saved |
%ymm12 | %xmm12 | Caller saved |
%ymm13 | %xmm13 | Caller saved |
%ymm14 | %xmm14 | Caller saved |
%ymm15 | %xmm15 | Caller saved |
AVX512 레지스터인 ZMM은 안넣었다.
2. 부동소수점 이동 및 변환 연산
이동 연산부터
Instruction | Source | Destination | Description |
---|---|---|---|
vmovss | M(32bit) | X | Move single precision |
vmovss | X | M(32bit) | Move single precision |
vmovsd | M(64bit) | X | Move double precision |
vmovsd | X | M(64bit) | Move double precision |
vmovaps | X | X | Move aligned, packed single precision |
vmovapd | X | X | Move aligned, packed double precision |
인스트럭션에서 문자 ‘a’는 aligned를 의미한다. 주소가 16바이트 정렬 요건을 만족하지 못하면 예외가 발생한다.
C 코드와 번역된 어셈블리 코드를 보자.
float float_mov(float v1, float *src, float *dst) {
float v2 = *src;
*dst = v1;
return v2;
}
아래는 “gcc -Og -S float_mov.c"의 결과.
float_mov:
movss (%rdi), %xmm1
movss %xmm0, (%rsi)
movaps %xmm1, %xmm0
ret
아래는 “gcc -mavx2 -Og -S float_mov.c"의 결과.
float_mov:
vmovss (%rdi), %xmm1 ; Read v2 from src
vmovss %xmm0, (%rsi) ; Write v1 to dst
vmovaps %xmm1, %xmm0
ret
이 글에서는 계속 -mavx2 명령줄 옵션으로 gcc를 실행할 것이다.
다음은 변환 연산.
Instruction | Source | Destination | Description |
---|---|---|---|
vcvttss2si | X/M(32bit) | R(32bit) | Convert with truncation single precision to integer |
vcvttsd2si | X/M(64bit) | R(32bit) | Convert with truncation double precision to integer |
vcvttss2siq | X/M(32bit) | R(64bit) | Convert with truncation single precision to quad word integer |
vcvttsd2siq | X/M(64bit) | R(64bit) | Convert with truncation double precision to quad word integer |
xmm 레지스터나 메모리에서 읽은 부동소수점 값을 정수 레지스터로 변환한다.
이때 값은 0의 방향으로 근사한다.
Instruction | Source 1 | Source 2 | Destination | Description |
---|---|---|---|---|
vcvtsi2ss | M(32bit)/R(32bit) | X | X | Convert integer to single precision |
vcvtsi2sd | M(32bit)/R(32bit) | X | X | Convert integer to double precision |
vcvtsi2ssq | M(64bit)/R(64bit) | X | X | Convert quad word integer to single precision |
vcvtsi2sdq | M(64bit)/R(64bit) | X | X | Convert quad word integer to double precision |
정수에서 부동소수점으로 변환하는 인스트럭션이다.
첫번째 Source에서 Destination의 자료형으로 변환한다. 두번째 Source는 결과의 하위 바이트에 영향을 주지 않는다.
두번째 Source는 무슨 역할일까? 난 모르겠다ㅜㅜ
이 시점에서 변환 인스트럭션이 어떻게 쓰이는지 확인하기 위해 아래 C 코드를 컴파일했다.
double fcvt(int i, float *fp, double *dp, long *lp) {
float f = *fp;
double d = *dp;
long l = *lp;
*lp = (long) d;
*fp = (float) i;
*dp = (double) l;
return (double) f;
}
fcvt:
vmovss (%rsi), %xmm0 ; Get f = *fp
movq (%rcx), %rax ; Get l = *lp
vcvttsd2siq (%rdx), %r8 ; Get d = *dp and convert to long
movq %r8, (%rcx) ; Store at lp
vxorps %xmm1, %xmm1, %xmm1
vcvtsi2ss %edi, %xmm1, %xmm1 ; Convert i to float
vmovss %xmm1, (%rsi) ; Store at fp
vxorpd %xmm1, %xmm1, %xmm1
vcvtsi2sdq %rax, %xmm1, %xmm1 ; Convert l to double
vmovsd %xmm1, (%rdx) ; Store at dp
vcvtss2sd %xmm0, %xmm0, %xmm0 ; Convert f to double
ret
vxorps 인스트럭션은 나중에 다룬다. 근데 왜 쓰는 걸까? 안해줘도 될 것 같은데…
단일 정밀도 값을 이중 정밀도 값으로 변환하는 인스트럭션 vcvtss2sd가 인상깊다. 아마 vcvtsd2ss도 있겠지.
3. 부동소수점 산술 연산
인스트럭션
Single | Double | Effect | Description |
---|---|---|---|
vaddss | vaddsd | D <- S2 + S1 | Floating-point add |
vsubss | vsubsd | D <- S2 - S1 | Floating-point subtract |
vmulss | vmulsd | D <- S2 * S1 | Floating-point multiply |
vdivss | vdivsd | D <- S2 / S1 | Floating-point divide |
vmaxss | vmaxsd | D <- max(S2, S1) | Floating-point maximum |
vminss | vminsd | D <- min(S2, S1) | Floating-point minimum |
sqrtss | sqrtsd | D <- sqrt(S1) | Floating-point square root |
예제
double funct(double a, float x, double b, int i) {
return a*x - b/i;
}
funct:
vcvtss2sd %xmm1, %xmm1, %xmm1 ; Convert x to double
vmulsd %xmm0, %xmm1, %xmm0 ; Multiply a by x
vxorpd %xmm1, %xmm1, %xmm1
vcvtsi2sd %edi, %xmm1, %xmm1 ; Convert i to double
vdivsd %xmm1, %xmm2, %xmm2 ; Compute b/i
vsubsd %xmm2, %xmm0, %xmm0 ; Subtract from a*x
ret
4. 부동소수점 상수
AVX 부동소수점 연산은 immediate value가 오퍼랜드에 올 수 없다.
컴파일러는 상수 값을 위해 저장공간을 할당한다.
다음 C코드를 통해 확인해보자.
double cel2fahr(double temp) {
return 1.8 * temp + 32.0;
}
cel2fahr:
.LFB0:
.cfi_startproc
vmulsd .LC0(%rip), %xmm0, %xmm0
vaddsd .LC1(%rip), %xmm0, %xmm0
ret
.cfi_endproc
.LFE0:
.size cel2fahr, .-cel2fahr
.section .rodata.cst8,"aM",@progbits,8
.align 8
.LC0:
.long 3435973837 ; Low-order 4 bytes of 1.8
.long 1073532108 ; High-order 4 bytes of 1.8
.align 8
.LC1:
.long 0 ; Low-order 4 bytes of 32.0
.long 1077936128 ; Low-order 4 bytes of 32.0
리틀 엔디안임을 감안하고 보면 1.8이 0x3FFCCCCCCCCCCCCD로 저장됐다는 것과, 32.0이 0x4040000000000000으로 저장됐다는 것을 알 수 있다.
5. 부동소수점 비트 연산
Single | Double | Effect | Description |
---|---|---|---|
vxorps | xorpd | D <- S2 ^ S1 | Bitwise EXCLUSIVE-OR |
vandps | andpd | D <- S2 & S1 | Bitwise EXCLUSIVE-OR |
예제. 아래는 math.c 파일 내용.
double zero(void) {
return 0.0;
}
double negative(double x) {
return -x;
}
#include <math.h>
double absolute(double x) {
return fabs(x);
}
gcc -mavx2 -Og -S math.c을 실행하면…
zero:
vxorpd %xmm0, %xmm0, %xmm0
ret
negative:
vxorpd .LC1(%rip), %xmm0, %xmm0
ret
absolute:
vandpd .LC2(%rip), %xmm0, %xmm0
ret
.LC1:
.long 0
.long -2147483648
.long 0
.long 0
.LC2:
.long 4294967295
.long 2147483647
.long 0
.long 0
부동소수점에 비트 연산이 쓰이는 걸 볼 수 있다. vxorp 인스트럭션은 0을 생성하는데 많이 애용되나 보다. xor 인스트럭션처럼.
6. 부동소수점 비교 연산
Instruction | Based on | Description |
---|---|---|
ucomiss S1, S2 | S2 - S1 | Compare single precision |
ucomisd S1, S2 | S2 - S1 | Compare double precision |
부동소수점 비교 인스트럭션도 CMP 인스트럭션처럼 조건 코드를 설정한다.
조건 코드는 다음과 같은 규칙을 따라 설정된다.
Ordering S2:S1 | CF | ZF | PF |
---|---|---|---|
Unordered | 1 | 1 | 1 |
S2 < S1 | 1 | 0 | 0 |
S2 = S1 | 0 | 1 | 0 |
S2 > S1 | 0 | 0 | 0 |
Unordered는 오퍼랜드 중 하나가 NaN일 때 발생하며, PF로 검출할 수 있다.
책에 있는 C 코드로 인스트럭션이 어떻게 사용되는지 보자.
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;
}
아래는 gcc -mavx2 -O2 -S find_range.c를 실행한 결과.
find_range:
vxorps %xmm1, %xmm1, %xmm1
vucomiss %xmm0, %xmm1 ; Compare 0:x
ja .L5
vucomiss %xmm1, %xmm0 ; Compare x:0
jp .L8
movl $1, %eax ; result = ZERO
je .L10
.L8:
xorl %eax, %eax
movl $3, %edx
vucomiss %xmm1, %xmm0 ; Compare x:0
seta %al
subl %eax, %edx
movl %edx, %eax ; result = (~CF & ~ZF) ? POS : OTHER
ret
.L5:
xorl %eax, %eax ; result = NEG
.L10:
rep ret
jp 인스트럭션을 주의깊게 보자.
7. 마무리
드디어 3장이 끝났다.
엣지 브라우저에서 글을 작성했는데 엄청 버벅거렸다. 그냥 크롬 써야겠다.
출처
‘Computer Systems A Programmer’s Perspective (3rd Edition)’