난 원래 로우레벨 최적화는 별로 좋아하지 않는다 -_-
로우레벨 최적화를 하면 코드를 알아보기 힘들어지게 되기 때문이고.. 알아볼 수 있게 만든답시고
주석을 덕지덕지 달거나, 혹은 함수 안에서 최적화 레벨에 따라 '알아보기 좋은 코드'와 '최적화한 코드'를 #ifdef 따위로 갈라놓아야 하는 불상사가 생기기도 한다.
그래서 로우레벨 최적화는 컴파일러에게 맡기자;는 사상이었는데, 지난주에 회사 엔진 개발팀에서 받아온 코드를 성능측정해보고 깜짝놀랐다. SSE 내장(intrisic) 함수를 쓴 벡터 정규화 코드가 컴파일러의 SSE2 최적화 코드보다 2배나(!) 빨랐던 것이다.
그래서, SSE 내장함수를 보기 좋게.. 코드에 넣는 방법을 모색해 봤는데, 요구조건은 다음 2가지이다.
- 코드가 의도하고자 하는 의미에 대한 명료하고 직관적인 표현이 들어갈 것.
- 하드웨어 의존적인 코드를 프로그래밍 단위별로 분할 할 수 있을 것.
1번 요구조건은 이 얘기이다. 코드에
_mm_store_ps( &x, _mm_add_ps( _mm_load_ps(&x), _mm_load_ps(&v.x) ) )
// (2)
x += rhs.x;
y += rhs.y;
z += rhs.z;
(1)과 (2)중에서 어느 쪽이 더 우아한 지는 자명하다. 일단 (1)번은 딱 보기에도 뭔소린지 못 알아먹겠다. -_- (2)번은 뭔 소린지는 중학교만 제대로 나온 사람이라면 다 알아먹을 수 있다. (2)번 완승. 문제는, (2)번이 (1)번만큼만 속도가 나와주면 좋은데, (1)번에 비해서 조홀라 느리다는 거다.
그래서 보통은 다음과 같이 한다. SSE 함수들은 VC에서 제공하는 헤더이므로, _WINDOWS_매크로 같은 걸로 감싸서 VC에서만 컴파일 되도록 만드는 거다.
vector3& operator+= ( const vector3& rhs )
{
#if !defined( _WINDOWS_ )
x += rhs.x;
y += rhs.y;
z += rhs.z;
#else
_mm_store_ps( &x, _mm_add_ps( _mm_load_ps(&x),
_mm_load_ps(&v.x) ) )
#endif
return *this;
}
근데 한 함수 안에서 저렇게 코드를 갈라 놓는 건 심히 보기 좋지 못하다. 이건 2번 요구조건과 걸리는데, 함수야말로 가장 기본적인 프로그래밍 단위인데, 그걸 저렇게 내부에서 갈라 놓는건 아름답다고는 할 수 없는 코드이다 -_-
그래서 궁여지책으로 생각해 낸 게 템플릿의 명시적 특수화를 이용한 구현이다. 나는 보통 벡터는 템플릿으로 구현해 놓기 때문에, SSE가 빠르게 처리해 줄 수 있는 float 타입의 벡터만 SSE 내장 함수로 특수화하면 되는 문제.
template <typename scalar_t> struct vector3
{
typedef scalar_t scalar_type;
scalar_type x, y, z;
public:
vector3() {}
vector3( scalar_type a, scalar_type b,
scalar_type c ) : x(a), y(b), z(c) {}
};
#if defined( _WINDOWS_ )|
#include <xmmintrin.h>
template<> struct vector3<float>
{
typedef float
scalar_type;
scalar_type x, y, z;
private: scalar_type _; // 128비트 정렬을 위한 숨은 멤버.
// 사실 #pragma pack이 있으므로 없어도 된다.
public:
vector3() {}
vector3( scalar_type a, scalar_type b,
scalar_type c )
{
_mm_store_ps( &x, _mm_set_ps( 0, c, b, a
) );
}
};
#endif
#pragma pack( pop )
뭐 이런 식. 윈도우일때만 SSE 내장 함수를 사용하고, 원래의 직관적 의미도 유지할 수 있는 방안인 거 같다.