CS:APP - 기계어, 부동소수점

이 내용 이전에 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 레지스터

256bit128bit
%ymm0%xmm01st FP arg./Return value
%ymm1%xmm12nd FP argument
%ymm2%xmm23rd FP argument
%ymm3%xmm34th FP argument
%ymm4%xmm45th FP argument
%ymm5%xmm56th FP argument
%ymm6%xmm67th FP argument
%ymm7%xmm78th FP argument
%ymm8%xmm8Caller saved
%ymm9%xmm9Caller saved
%ymm10%xmm10Caller saved
%ymm11%xmm11Caller saved
%ymm12%xmm12Caller saved
%ymm13%xmm13Caller saved
%ymm14%xmm14Caller saved
%ymm15%xmm15Caller saved

AVX512 레지스터인 ZMM은 안넣었다.

2. 부동소수점 이동 및 변환 연산

이동 연산부터

InstructionSourceDestinationDescription
vmovssM(32bit)XMove single precision
vmovssXM(32bit)Move single precision
vmovsdM(64bit)XMove double precision
vmovsdXM(64bit)Move double precision
vmovapsXXMove aligned, packed single precision
vmovapdXXMove 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를 실행할 것이다.

다음은 변환 연산.

InstructionSourceDestinationDescription
vcvttss2siX/M(32bit)R(32bit)Convert with truncation single precision to integer
vcvttsd2siX/M(64bit)R(32bit)Convert with truncation double precision to integer
vcvttss2siqX/M(32bit)R(64bit)Convert with truncation single precision to quad word integer
vcvttsd2siqX/M(64bit)R(64bit)Convert with truncation double precision to quad word integer

xmm 레지스터나 메모리에서 읽은 부동소수점 값을 정수 레지스터로 변환한다.

이때 값은 0의 방향으로 근사한다.

InstructionSource 1Source 2DestinationDescription
vcvtsi2ssM(32bit)/R(32bit)XXConvert integer to single precision
vcvtsi2sdM(32bit)/R(32bit)XXConvert integer to double precision
vcvtsi2ssqM(64bit)/R(64bit)XXConvert quad word integer to single precision
vcvtsi2sdqM(64bit)/R(64bit)XXConvert 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. 부동소수점 산술 연산

인스트럭션

SingleDoubleEffectDescription
vaddssvaddsdD <- S2 + S1Floating-point add
vsubssvsubsdD <- S2 - S1Floating-point subtract
vmulssvmulsdD <- S2 * S1Floating-point multiply
vdivssvdivsdD <- S2 / S1Floating-point divide
vmaxssvmaxsdD <- max(S2, S1)Floating-point maximum
vminssvminsdD <- min(S2, S1)Floating-point minimum
sqrtsssqrtsdD <- 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. 부동소수점 비트 연산

SingleDoubleEffectDescription
vxorpsxorpdD <- S2 ^ S1Bitwise EXCLUSIVE-OR
vandpsandpdD <- S2 & S1Bitwise 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. 부동소수점 비교 연산

InstructionBased onDescription
ucomiss S1, S2S2 - S1Compare single precision
ucomisd S1, S2S2 - S1Compare double precision

부동소수점 비교 인스트럭션도 CMP 인스트럭션처럼 조건 코드를 설정한다.

조건 코드는 다음과 같은 규칙을 따라 설정된다.

Ordering S2:S1CFZFPF
Unordered111
S2 < S1100
S2 = S1010
S2 > S1000

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)’