dev.log2007. 9. 6. 02:31

벡터를 정규화하는 다음 두 코드를 보자.

void vector3::normalize()
{
    scalar_t len = scalar_t(1)/length();
    x *= len;
    y *= len;
    z *= len;
}

void vector3::normalize()
{
    scalar_t len = length();
    x /= len;
    y /= len;
    z /= len;
}

어느쪽이 빠를까? 언뜻보기에는 첫번째가 빨라보인다. 일반적으로 나눗셈은 곱셈보다 느리므로. 그런데 실제 50만번씩 정규화한 수행시간을 QueryPerformanceCounter로 재 보면,

47690907 : 44902704
45215687 : 44940973
45526767 : 45290300
46242218 : 44798457
44906158 : 46730200

임의로 5행을 뽑아봤는데, 놀랍게도, 대동소이하다. 왜 그럴까? 이 두 코드를 VC에서 기본 속도 최적화 옵션으로 컴파일하면 놀랍게도 다음과 같은 동일한 코드가 나온다.

 

; 78   :        scalar_t len = scalar_t(1)/length();

    fld DWORD PTR [ecx+8]
    fld DWORD PTR [ecx+4]
    fld DWORD PTR [ecx]
    fld ST(0)
    fmul    ST(0), ST(1)
    fld ST(2)
    fmul    ST(0), ST(3)

; 79   :        x *= len;
; 80   :        y *= len;
; 81   :        z *= len;

    faddp   ST(1), ST(0)
    fld ST(3)
    fmul    ST(0), ST(4)
    faddp   ST(1), ST(0)
    fsqrt
    fstp    ST(3)
    fstp    ST(0)
    fstp    ST(0)
    fdivr   DWORD PTR __real@3f800000
    fld ST(0)
    fmul    DWORD PTR [ecx]
    fstp    DWORD PTR [ecx]
    fld ST(0)
    fmul    DWORD PTR [ecx+4]
    fstp    DWORD PTR [ecx+4]
    fmul    DWORD PTR [ecx+8]
    fstp    DWORD PTR [ecx+8]


; 85   :   scalar_t len = length();

    fld DWORD PTR [ecx+8]
    fld DWORD PTR [ecx+4]
    fld DWORD PTR [ecx]
    fld ST(0)
    fmul ST(0), ST(1)
    fld ST(2)
    fmul ST(0), ST(3)

; 86   :   x /= len;
; 87   :   y /= len;
; 88   :   z /= len;

    faddp ST(1), ST(0)
    fld ST(3)
    fmul ST(0), ST(4)
    faddp ST(1), ST(0)
    fsqrt
    fstp ST(3)
    fstp ST(0)
    fstp ST(0)
    fdivr DWORD PTR __real@3f800000
    fld ST(0)
    fmul DWORD PTR [ecx]
    fstp DWORD PTR [ecx]
    fld ST(0)
    fmul DWORD PTR [ecx+4]
    fstp DWORD PTR [ecx+4]
    fmul DWORD PTR [ecx+8]
    fstp DWORD PTR [ecx+8]

코드가 같으니, 수행 시간도 사실상 동일할 수밖에. MS의 컴파일러가 제법 똑똑하게 최적화를 시켜 준다. 그런데 더 놀라운 결과는, SSE옵션을 켰을때다. SSE2 옵션을 켜고 컴파일 한 후 수행시간을 보면,

46711544 : 38123657
44900658 : 37913161

45154923 : 38187996

52286080 : 37779577

45221726 : 42707852

이번엔 오히려 나눗셈으로 계산을 한 쪽이 더 빠르다. 왜 그럴까? 컴파일된 결과를 보면 답이 나온다.

; _this$ = ecx
; 78   :        scalar_t len = scalar_t(1)/length();
    fld DWORD PTR [ecx+8]
    fld DWORD PTR [ecx+4]
    fld DWORD PTR [ecx]
    fld ST(0)
    fmul    ST(0), ST(1)
    fld ST(2)
    fmul    ST(0), ST(3)
; 79   :        x *= len;
; 80   :        y *= len;
; 81   :        z *= len;
    faddp   ST(1), ST(0)
    fld ST(3)
    fmul    ST(0), ST(4)
    faddp   ST(1), ST(0)
    fsqrt
    fstp    ST(3)
    fstp    ST(0)
    fstp    ST(0)
    fdivr   DWORD PTR __real@3f800000
    fld ST(0)
    fmul    DWORD PTR [ecx]
    fstp    DWORD PTR [ecx]
    fld ST(0)
    fmul    DWORD PTR [ecx+4]
    fstp    DWORD PTR [ecx+4]
    fmul    DWORD PTR [ecx+8]
    fstp    DWORD PTR [ecx+8]


; _this$ = ecx
; 84   :    {
    push    ecx
; 85   :        scalar_t len = length();
    fld DWORD PTR [ecx+8]
; 86   :        x /= len;
    movss   xmm0, DWORD PTR 
    fld DWORD PTR [ecx+4]
    fld DWORD PTR [ecx]
    fld ST(0)
    fmul    ST(0), ST(1)
    fld ST(2)
    fmul    ST(0), ST(3)
; 87   :        y /= len;
; 88   :        z /= len;
    faddp   ST(1), ST(0)
    fld ST(3)
    fmul    ST(0), ST(4)
    faddp   ST(1), ST(0)
    fsqrt
    fstp    ST(3)
    fstp    ST(0)
    fstp    ST(0)
    fstp    DWORD PTR _len$[esp+4]
    divss   xmm0, DWORD PTR _len$[esp+4]
    movaps  xmm1, xmm0
    mulss   xmm1, DWORD PTR [ecx]
    movss   DWORD PTR [ecx], xmm1
    movaps  xmm1, xmm0
    mulss   xmm1, DWORD PTR [ecx+4]
    mulss   xmm0, DWORD PTR [ecx+8]
    movss   DWORD PTR [ecx+4], xmm1
    movss   DWORD PTR [ecx+8], xmm0
; 89   :    }
    pop ecx

보면, 곱셈으로 계산을 한 쪽은 변화가 없고 나눗셈으로 계산을 한 쪽은 SSE명령이 생성되었다. 왜 그런지는 모른다 -_- 따라서 코드만 보고 짐작하지 말고, 직접 측정을 해봐야 한다는 결론. 컴파일러가 알아서 해주는 게 더 빠를지도 모르니까

Posted by uhm