이 내용 이전에 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)'



    Posted by 코요