'C++'에 해당되는 글 35건

  1. 2009.01.28 통계자료 수집
  2. 2008.06.06 SSE의 오묘함 2
  3. 2008.02.25 유니코드 변환.
  4. 2008.02.05 xmmintrin.h 2
  5. 2008.02.01 DLL에서 STL map 익스포트하기
  6. 2007.12.12 템플릿과 매크로
  7. 2007.12.08 vfptr
  8. 2007.11.21 rebind의 중요성
  9. 2007.09.06 컴파일러 만세
  10. 2007.03.14 아놔 진짜 이거 참
dev.log2009. 1. 28. 23:38
ice 프로젝트를 시작하려다보니, 기초 수학 클래스의 성능을 측정해야만 했다. 그래서, 옛날 R모 게임을 만들던 시절에 만져본 언리얼엔진2의 스탯 수집방식을 좇아서 비슷하게 만들어 보기로 했다. 기본 방식은 수집할 스탯의 이름과 타입을 등록하여 인덱스를 지정한 후 수집할 구역의 시작과 끝을 지정하는 것. 여기에 시간 통계를 위한 API콜은 별도의 레이어로 분리하면 대략적인 틀이 나온다.
class syscore
{
public:
    static int64 clock()
    {
        int64 begin;
        QueryPerformanceCounter( (LARGE_INTEGER*)&begin );
        return begin;
    }
    static int64 clockpersec()
    {
        int64 freq;
        QueryPerformanceCounter( (LARGE_INTEGER*)&freq );
        return freq;
    }
};

class stat_collector
{
public:
    stat_collector() { _freq = (float)syscore::clockpersec(); }
    void flush();
    uint32 enroll( const string& name, uint type );
    void start( uint index );
    void stop( uint index );

    float mean_stat( uint index );
    float inst_stat( uint index );
    float total_stat( uint index );
    int   sample_count( uint index );

    class block_stat
    {
    public:
        block_stat( stat_collector& collector, uint index )
            :_collector( &collector ), _index( index )
        {
            _collector->start( _index );
        }
        ~block_stat()
        {
            _collector->stop( _index );
        }
    private:
        stat_collector* _collector;
        uint            _index;
    };

    enum stat_type
    {
        STAT_TIME,
        STAT_COUNT,
    };

private:

    struct stat_item
    {
        stat_item( const string& name, uint type )
            : _count(0), _name(name), _type(type)
        {}

        uint   _count;
        string _name;
        uint   _type;
    };
    std::vector<sint64> _prv_stats;
    std::vector<sint64> _cur_stats;
    std::vector<stat_item> _stat_info;
    float _freq;
};

void stat_collector::flush()
{
    //_prv_stats = _cur_stats;
}

uint32 stat_collector::enroll( const string& name, uint type )
{
    assert( _prv_stats.size() == _cur_stats.size() );
    assert( _prv_stats.size() == _names.size()  );
    uint32 index = _cur_stats.size();
    _prv_stats.push_back( 0 );
    _cur_stats.push_back( 0 );
    _stat_info.push_back( stat_item( name, type ) );

    return index;
}

void stat_collector::start( uint index )
{
    _prv_stats[index] = _cur_stats[index];

    switch ( _stat_info[index]._type )
    {
    case STAT_TIME:
        _cur_stats[index] -= syscore::clock();;
        break;
    case STAT_COUNT:
        break;
    default:
        assert( "unkown stat type!!!!!!!!!" && 0 );
    };
}

void stat_collector::stop( uint index )
{
    switch ( _stat_info[index]._type )
    {
    case STAT_TIME:
        _cur_stats[index] += syscore::clock();
        break;
    case STAT_COUNT:
        _cur_stats[index]++;
        break;
    default:
        assert( "unkown stat type!!!!!!!!!" && 0 );
    }

    _stat_info[index]._count++;
}

float stat_collector::mean_stat( uint index )
{
    switch ( _stat_info[index]._type )
    {
    case STAT_TIME:
        return _cur_stats[index]/(_freq * _stat_info[index]._count);
    case STAT_COUNT:
        return (float)_cur_stats[index]/_stat_info[index]._count;
    default:
        assert( "unkown stat type!!!!!!!!!" && 0 );
        return 0;
    }
}


float stat_collector::inst_stat( uint index )
{
    switch ( _stat_info[index]._type )
    {
    case STAT_TIME:
        return (_cur_stats[index]-_prv_stats[index])/_freq;
    case STAT_COUNT:
        return (float)_cur_stats[index]-_prv_stats[index];
    default:
        assert( "unkown stat type!!!!!!!!!" && 0 );
        return 0;
    }
}

네이밍 컨벤션은.. C/C++ 표준 위원회의 스타일을 따라가 봤다.
사용법은,
#define TEST_LIST() \
    TEST( l = c.length() )                   \
    TEST( c = vector_t::cross( a, b ) ) \
    TEST( l = vector_t::dot( b, c ) )      \

stat_collector _stats;

template <class vector_t>
void test_run( const char* filename, uint stat_index )
{
    static vector_t a, b, c;

    std::ofstream log( filename, ios::app );
#define TEST( expression ) #expression "\n"
    log << "=======================================" << endl;
    log << TEST_LIST();
    log << "---------------------------------------" << endl;
#undef TEST
    for ( int count = 0; count < 10; ++count )
    {
        vector_t::scalar_type x = (float)rand();
        vector_t::scalar_type y = (float)rand();
        vector_t::scalar_type z = (float)rand();
        volatile vector_t::scalar_type l = 0;
        a = vector_t( x, y, 0 );
        b = vector_t( -y, x, 0 );
        c = vector_t( x, y, z );

        _stats.start( stat_index );
        for ( int i = 0; i < 10000000; ++i )
        {
#define TEST( expression ) expression;
            TEST_LIST();
#undef TEST
        }
        _stats.stop( stat_index );
        log << _stats.inst_stat(stat_index) << std::endl;
    }
    log << "======== " << _stats.mean_stat(stat_index) << " sec/sample ==========" << std::endl;
}

int WINAPI WinMain( HINSTANCE , HINSTANCE, char* , int )
{
    uint imp_stat, exp_stat;
    imp_stat = _stats.enroll( _t("imp"), stat_collector::STAT_TIME );
    exp_stat = _stats.enroll( _t("exp"), stat_collector::STAT_TIME );
    test_run<vector3def>( "log_imp.txt", imp_stat );
    test_run<vector3sse>( "log_exp.txt", exp_stat );

    return 0;
}

이름과 타입을 등록하고, 측정할 구간을 start/end로 지정하면 끝. TEST_LIST는 테스트 대상이 어떤 코드였는지에 대한 로그를 남기기 위한 매크로.

부족한 부분은 나중에 수정하자.

Posted by uhm
dev.log2008. 6. 6. 09:11
요즘은 SSE 수학 클래스 만들기에 열을 올리고 있다. 아직 행렬 클래스는 (노느라) 바뻐서 못만들고는 있지만 벡터 클래스는 요모조모 손봐가면서 테스트중. 전에 쓴 대로, SSE는 확실히 성능향상에 도움은 된다. 그런데 그게 좀 오묘하다. 명령의 순서가 희한하게 걸리면 오히려 속도가 느려진다.

다음은 주어진 연산순서를 각각 1000만번씩 반복했을 때 걸린 시간을 10번씩 측정한 결과이다. (CPU - Core2Duo E6600, Memory - DDR2 PC2-5300 6GB, 32bit application on 64bit Windows)

연산순서 기본구현
SSE구현
cross->length->dot 0.307732
0.307663
0.307266
0.307756
0.308705
0.307553
0.307968
0.307418
0.307018
0.307521
0.173833
0.173594
0.17392
0.17435
0.173717
0.17394
0.173853
0.173623
0.173899
0.173708
cross->dot->length 0.308549
0.307164
0.307153
0.307491
0.307836
0.307346
0.307413
0.308006
0.307355
0.307362
0.366581
0.366742
0.366583
0.367308
0.366557
0.366436
0.36658
0.366613
0.36664
0.366343

첫번째 결과는 당연히 SSE가 빠르겠거니.. 하는 추정에 부합하는데, 두번째 결과를 보면 연산 순서만 바뀌었을 뿐인데 SSE구현쪽의 결과가 훨씬 더 느리다. 반면, 기본 구현은 연산 순서랑 관계없이 거의 일정한 속도를 보장해 준다. 내 생각으로는 SSE명령을 수행 후에 메모리에 연산 결과를 쓰면서 레지스터중 일부를 초기화하고 메모리에 억세스하는 비용이 크기 때문일거 같은데, 어셈코드를 봐도 사실 정확한 건 잘 모르겠다. (공부하자)

들쭉날쭉한 속도를 개선하기 위해 요모조모로 테스트해 보다가 내적의 문제점을 깨달았다. 내적은, SSE로 구현할 때 애로사항이 많다. SSE의 기본은 (사실은 SSE가 아니라 그 근간인 SIMD라고 해야 맞지만) 부동소수 4개를 각기 따로따로 별도의 데이터 패쓰를 거쳐서 처리한다는, 일종의 data parallelism 인데, 내적은 따로따로 처리되어야 하는 데이터 3개, 즉 xyz좌표를 필수적으로 한군데에서 처리하도록 해야 한다. SSE의 설계 이념과 매우 동떨어진 연산이 될 수 밖에 없다. 물론 SSE3에서는 패킹된 데이터4개를 하나로 더해주는 명령이 추가되었지만, 아직 SSE3를 지원하지 않는 CPU가 많이 사용되고 있으므로 적용하기는 약간 무리다. 따라서 지금으로썬 내적의 SSE 구현이 병목이므로 내적을 SSE를 쓰지 않은 일반 구현으로 바꾸는 것이 최선일듯.

SSE로 구현한 벡터클래스의 내적을 일반구현으로 바꿔서 테스트해봤다.다음이 그 결과다.
연산순서 기본구현 SSE구현 
cross->length->dot 0.31281
0.328362
0.326691
0.312755
0.312775
0.325598
0.318856
0.31096
0.325269
0.308923
0.163027
0.1661
0.163232
0.163836
0.163102
0.162626
0.163001
0.169913
0.163181
0.162604
cross->dot->length 0.307485
0.307058
0.306469
0.307107
0.308057
0.306862
0.308106
0.307052
0.307446
0.306855
0.161829
0.161945
0.162188
0.16885
0.163519
0.161771
0.161899
0.162052
0.16204
0.162045
결과가 아주 이쁘장하다. 흡족하다. 저정도면 어느 상황에서도 비교적 빠른 속도를 보장해주는 벡터클래스가 되지 않을까.

그래서 결과적으로 완성된 코드는 다음과 같다.
#if !defined( __VECTOR3_H__ )
#define __VECTOR3_H__

#include <cmath>


#pragma intrinsic( sqrt )
// Internal Combustion Engine
namespace ice
{
template <class scalar_t, bool use_implicit = true > struct vector3_t
{
    typedef scalar_t scalar_type;
    scalar_type x, y, z;

public:
    vector3_t() {}
    vector3_t( scalar_type a, scalar_type b, scalar_type c ) : x(a), y(b), z(c) {}
    vector3_t( const scalar_type* xyz ) : x(xyz[0]), y(xyz[1]), z(xyz[2]) {}

    scalar_type operator[] ( int i ) const { return *(&x + i); }
    scalar_type& operator[] ( int i ) { return *(&x +i); }

    vector3_t& operator+= ( const vector3_t& v )
    {
        x += v.x; y += v.y; z += v.z;
        return *this;
    }
    vector3_t operator+ ( const vector3_t& v ) const
    {
        return vector3( x+v.x, y+v.y, z+v.z );
    }
    vector3_t& operator-= ( const vector3_t& v )
    {
        x -= v.x; y -= v.y; z -= v.z;
        return *this;
    }
    vector3_t operator- ( const vector3_t& v ) const
    {
        return vector3( x-v.x, y-v.y, z-v.z );
    }
    vector3_t& operator*= ( scalar_type s )
    {
        x *= s; y *= s; z *= s;
        return *this;
    }
    vector3_t operator* ( scalar_type s ) const
    {
        return vector3( x*s, y*s, z*s );
    }
    vector3_t& operator/= ( scalar_type s )
    {
        x /= s; y /= s; z /= s;
        return *this;
    }
    vector3_t operator/ ( scalar_type s ) const
    {
        return vector3( x/s, y/s, z/s );
    }

    static scalar_type dot( const vector3_t& v1, const vector3_t& v2 )
    {
        return v1.x*v2.x + v1.y*v2.y + v1.z*v2.z;
    }
    static vector3_t cross( const vector3_t& v1, const vector3_t& v2 )
    {
        return vector3_t(  v1.y * v2.z - v1.z * v2.y,
                                v1.z * v2.x - v1.x * v2.z,
                                v1.x * v2.y - v1.y * v2.x );
    }

    scalar_type squared() const { return x*x + y*y + z*z; }
    scalar_type length() const { return std::sqrt(squared()); }
    void normalize()
    {
        *this /= length();
    }
    vector3_t unit() const
    {
        return *this/length();
    }
};
}

#if defined( _USE_SSE_ )
#pragma pack( push, 16 )

#include <intrin.h>
#define ALIGN_MMX __declspec(align(16))

namespace ice
{

template<> struct ALIGN_MMX vector3_t<float, false>
{
    typedef float scalar_type;
    scalar_type x, y, z;

    vector3_t() {}
    vector3_t( scalar_type _a, scalar_type _b, scalar_type _c )
    {
        _mm_store_ps( &x, _mm_set_ps( 0, _c, _b, _a ) );
    }
    vector3_t( const scalar_type* xyz )
    {
        _mm_store_ps( &x, _mm_load_ps( xyz ) );
    }
    vector3_t( const vector3_t& v )
    {
        _mm_store_ps( &x, _mm_set_ps( 0, v.z, v.y, v.x ) );
    }

    scalar_type operator[] ( int i ) const { return *(&x + i); }
    scalar_type& operator[] ( int i ) { return *(&x +i); }

    vector3_t& operator+= ( const vector3_t& v )
    {
        _mm_store_ps( &x, _mm_add_ps( _mm_load_ps(&x), _mm_load_ps(&v.x) ) );
        return *this;
    }
    vector3_t operator+ ( const vector3_t& v ) const
    {
        return vector3_t( _mm_add_ps( _mm_load_ps(&x), _mm_load_ps(&v.x) ) );
    }
    vector3_t& operator-= ( const vector3_t& v )
    {
        _mm_store_ps( &x, _mm_sub_ps( _mm_load_ps(&x), _mm_load_ps(&v.x) ) );
        return *this;
    }
    vector3_t operator- ( const vector3_t& v ) const
    {
        return vector3_t( _mm_sub_ps( _mm_load_ps(&x), _mm_load_ps(&v.x) ) );
    }
    vector3_t& operator*= ( scalar_type s )
    {
        _mm_store_ps( &x, _mm_mul_ps( _mm_load_ps(&x), _mm_set1_ps( s ) ) );
        return *this;
    }
    vector3_t operator* ( scalar_type s ) const
    {
        return vector3_t( _mm_mul_ps( _mm_load_ps(&x), _mm_set1_ps( s ) ) );
    }
    vector3_t& operator/= ( scalar_type s )
    {
        _mm_store_ps( &x, _mm_div_ps( _mm_load_ps(&x), _mm_set1_ps( s ) ) );
        return *this;
    }
    vector3_t operator/ ( scalar_type s ) const
    {
        return vector3_t( _mm_div_ps( _mm_load_ps(&x), _mm_set1_ps( s ) ) );
    }

    static scalar_type dot( const vector3_t& v1, const vector3_t& v2 )
    {
        return v1.x*v2.x + v1.y*v2.y + v1.z*v2.z;
    }
    static vector3_t cross( const vector3_t& v1, const vector3_t& v2 )
    {
        //v1.y * v2.z - v1.z * v2.y
        //v1.z * v2.x - v1.x * v2.z
        //v1.x * v2.y - v1.y * v2.x
        __m128 a = _mm_shuffle_ps( *(__m128*)&v1, *(__m128*)&v1,
_MM_SHUFFLE( 3, 1, 2, 0 ) );
        __m128 c = _mm_shuffle_ps( *(__m128*)&v2, *(__m128*)&v2,
_MM_SHUFFLE( 3, 2, 0, 1 ) );
        __m128 b = _mm_shuffle_ps( *(__m128*)&v1, *(__m128*)&v1,
_MM_SHUFFLE( 3, 2, 0, 1 ) );
        __m128 d = _mm_shuffle_ps( *(__m128*)&v2, *(__m128*)&v2,
_MM_SHUFFLE( 3, 1, 2, 0 ) );
        return vector3_t( _mm_sub_ps( _mm_mul_ps( a, c ), _mm_mul_ps(b,d) ) );
    }

    scalar_type squared() const { return dot(*this,*this); }
    scalar_type length()  const
    {
        return _mm_sqrt_ss( _mm_set_ss( squared() ) ).m128_f32[0];
    }
    void normalize()
    {
        _mm_store_ps( &x, _mm_div_ps( _mm_load_ps(&x),
_mm_sqrt_ps( _mm_set1_ps( squared() ) ) ) );
    }
    vector3_t unit() const
    {
        return vector3_t( _mm_div_ps( _mm_load_ps(&x),
_mm_sqrt_ps( _mm_set1_ps( squared() ) ) ) );
    }

private:
    vector3_t( __m128 v )
    {
        _mm_store_ps( &x, v );
    }
};

typedef vector3_t<float, false>  vector3;
}
#pragma pack ( pop )
#else

namespace ice
{
typedef vector3_t<float>  vector3;
}

#endif // #if defined( _USE_SSE_ )

#endif // #if defined( _VECTOR3_H_ )

Posted by uhm
dev.log2008. 2. 25. 21:00

처음 유니코드 문자열을 만날 때 흔히 하는 오해(혹은 실수)는.. char*를 wchar_t*로 바꿀 때 그냥 캐스팅하면 되리라는 것이다. (나도 그랬다)

 

그런데, char*로 표현되는 문자열은, MBCS - multi-byte character set - 문자열이고, wchar_t*로 표현되는 문자열은 WCS - wide character set - 문자열이다. 서로 코딩 스킴이 다른 문자열이다. 단순 치환만으로는 변환할 수 없다.

MBCS문자열 - 즉, MBS는 보통 ASCII와 각 국가의 코드 페이지별 OEM코드를 사용하는데, 이 코드 페이지마다 같은 코드에 다른 문자를 넣어 두게 된다. 따라서 MBS에서 한글(코드페이지 949) '가'의 코드포인트는 0xb0a1인데, 다른 코드페이지에서는 (이를테면 일본어) 그 국가의 다른 글자를 나타낼 수 있는 코드이다. 이는 문자를 표현할 때 국가마다 서로 다른 코드를 쓰기 때문인데, 이를 통합하기 위해서 유니코드 문자열을 쓰며, C에서는 유니코드 문자열을 위해 문자마다 2바이트를 쓰는 WCS를 쓴다.

MBS 코드페이지 949에서 한글 '가'의 코드는 0xb0a1라고 했다. 그런데 WCS에서 한글 '가'의 코드포인트는 0xac00이다. 그럼 WCS에서 0xb0a1에 해당하는 글자는? 인텔머신은 리틀 엔디언을 쓰므로, OEM코드 0xb0a1은 0xa1b0로 저장되며, 이 코드는 'ꆰ'이다. MBS의 내용물을 그대로 WCS 문자열 버퍼에 '복사'하는 것으로는 문자열 변환이 안된다는 말이다. 물론, 그 내용물이 영미권의 영문 알파벳만이라면, 제대로 이루어진다. 이들은 ASCII코드이며, 유니코드 체계에서도 ASCII영역(코드포인트 0~127까지)은 크게 변화되지 않은 채로 남아있기 때문에. 하지만 다른 국가의 언어는 절대 저렇게 해서 변환이 이루어지지 않는다. 한글은 물론이고, 알파벳을 쓰는 다른 국가의 - 독일, 프랑스, 스페인, 노르웨이, 핀란드 등등 - 언어도 마찬가지다.

 

그럼 한글 "가"를 한글 L"가"로 제대로 바꾸려면? 표준 문자열 라이브러리 함수가 있다. mbstowcs, wcstombs등의 함수가 MBS-WCS사이의 문자열 변환을 해준다.

 

Posted by uhm
dev.log2008. 2. 5. 10:50

난 원래 로우레벨 최적화는 별로 좋아하지 않는다 -_-

로우레벨 최적화를 하면 코드를 알아보기 힘들어지게 되기 때문이고.. 알아볼 수 있게 만든답시고

주석을 덕지덕지 달거나, 혹은 함수 안에서 최적화 레벨에 따라 '알아보기 좋은 코드'와 '최적화한 코드'를 #ifdef 따위로 갈라놓아야 하는 불상사가 생기기도 한다.

그래서 로우레벨 최적화는 컴파일러에게 맡기자;는 사상이었는데, 지난주에 회사 엔진 개발팀에서 받아온 코드를 성능측정해보고 깜짝놀랐다. SSE 내장(intrisic) 함수를 쓴 벡터 정규화 코드가 컴파일러의 SSE2 최적화 코드보다 2배나(!) 빨랐던 것이다.

그래서, SSE 내장함수를 보기 좋게.. 코드에 넣는 방법을 모색해 봤는데, 요구조건은 다음 2가지이다.

  1. 코드가 의도하고자 하는 의미에 대한 명료하고 직관적인 표현이 들어갈 것.
  2. 하드웨어 의존적인 코드를 프로그래밍 단위별로 분할 할 수 있을 것.

 1번 요구조건은 이 얘기이다. 코드에

// (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 내장 함수로 특수화하면 되는 문제.

#pragma pack( push, 16 )
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 내장 함수를 사용하고, 원래의 직관적 의미도 유지할 수 있는 방안인 거 같다.

Posted by uhm
dev.log2008. 2. 1. 04:51

DLL에서 익스포트할 클래스에서 STL 클래스 멤버를 쓰는 것은 바람직하지 않지만, 세상사란 그리 만만하지 않은 법. DLL에서 STL클래스를 익스포트 해야 할 경우가 가끔 생기는데, 사실 vector같은 건 별로 문제가 되지 않는다. 컨테이너 자체가 간단하고, 연관된 할당자도 별로 없기 때문에. 사실 map이나 set같은 복잡한 컨테이너가 진짜 문제다.

 

웹에서 찾아볼 수 있는, STL map을 DLL에서 익스포트하기 위한 매크로는 대개 다음과 같다

#define EXPORT_STL_MAP( dllmacro, mapkey, mapvalue ) \
  template struct dllmacro std::pair< mapkey,mapvalue >; \
  template class dllmacro std::allocator< \
    std::pair<const mapkey,mapvalue> >; \
  template struct dllmacro std::less< mapkey >; \
  template class dllmacro std::allocator< \
    std::_Tree_ptr<std::_Tmap_traits<mapkey,mapvalue,std::less<mapkey>, \
    std::allocator<std::pair<const mapkey,mapvalue> >,false> > >; \
  template class dllmacro std::allocator< \
    std::_Tree_nod<std::_Tmap_traits<mapkey,mapvalue,std::less<mapkey>, \
    std::allocator<std::pair<const mapkey,mapvalue> >,false> > >; \
  template class dllmacro std::_Tree_nod< \
    std::_Tmap_traits<mapkey,mapvalue,std::less<mapkey>, \
    std::allocator<std::pair<const mapkey,mapvalue> >,false> >; \
  template class dllmacro std::_Tree_ptr< \

    std::_Tmap_traits<mapkey,mapvalue,std::less<mapkey>, \
    std::allocator<std::pair<const mapkey,mapvalue> >,false> >; \
  template class dllmacro std::_Tree_val< \
    std::_Tmap_traits<mapkey,mapvalue,std::less<mapkey>, \
    std::allocator<std::pair<const mapkey,mapvalue> >,false> >; \
  template class dllmacro std::map< \
    mapkey, mapvalue, std::less< mapkey >, \
    std::allocator<std::pair<const mapkey,mapvalue> > >;

(이걸보고 주눅이 안들면 비정상. 안심해라)

 

근데 이 매크로를 써서 map을 익스포트하면, VC2003에서는 아무 탈 없이 컴파일 됐는데, VC2005에서는 요상하게 다음과 같은 에러를 뱉었다.

1>c:\dev\visualstudio8\vc\include\xtree(61) : warning C4251: 'std::_Tree_nod<_Traits>::_Alnod' : class 'std::allocator<_Ty>'에서는 class 'std::_Tree_nod<_Traits>'의 클라이언트에서 DLL 인터페이스를 사용하도록 지정해야 합니다.
1>        with
1>        [
1>            _Traits=std::_Tmap_traits<int,short,std::less<int>,std::allocator<std::pair<const int,short>>,false>
1>        ]
1>        and
1>        [
1>            _Ty=std::_Tree_nod<std::_Tmap_traits<int,short,std::less<int>,std::allocator<std::pair<const int,short>>,false>>::_Node
1>        ]
1>        and
1>        [
1>            _Traits=std::_Tmap_traits<int,short,std::less<int>,std::allocator<std::pair<const int,short>>,false>
1>        ]

(보통 STL이 뱉는 에러메시지는 암호같긴 하다)

 

이걸 해석하는 것도 만만치 않긴 하지만, 해석하자면 std::allocator< ... >::_Node 가 익스포트되도록 지정되지 않았다..는 내용. 따라서 다음 두 줄을 매크로 선언에 추가하면 된다. 위의 메시지는 _Node에 대한 것이고, _Node에 대한 할당자를 익스포트하도록 지정하면 또 다른 어딘가에서 _Node*에 대한 할당자도 익스포트해야 한다는 메시지가나온다. 그래서 두줄을 더 추가해야 했다.

 

  template class dllmacro std::allocator< \

    std::_Tree_nod<std::_Tmap_traits<mapkey,mapvalue,std::less<mapkey>, \
    std::allocator<std::pair<const mapkey,mapvalue> >,false> >::_Node >; \
  template class dllmacro std::allocator< \
    std::_Tree_nod<std::_Tmap_traits<mapkey,mapvalue,std::less<mapkey>, \
    std::allocator<std::pair<const mapkey,mapvalue> >,false> >::_Node* >; \

 2003에서는 멀쩡히 잘 돌아가던 매크로가 2005에서는 안되는 이유가 뭔지는 잘 모르겠지만 -_- 여튼 저렇게 하니까 됐다. 잊어먹지 말자.


Posted by uhm
dev.log2007. 12. 12. 21:37

오늘 받은 코드는 패킷을 다음과 같이 정의해서 쓰고 있다.

struct Packet

{

    unsigned type;

    unsigned size;

};

#define PACKET_DECLARE(_packet,_type)  _packet() \

    { type = _type; size = sizeof(_packet); }

 

struct PacketXXX : public Packet

{

    int BlahBlah;

    PACKET_DECLARE(PacketXXX, IDPKT_XXX)

};

 그런데 난 다음과 같은 방식을 더 선호한다.

template < class P, unsigned T >

struct Packet

{

    unsigned type;

    unsigned size;

    Packet() { type = T; size = sizeof(P) }

};

 

struct PacketXXX

    : public Packet < PacketXXX, IDPKT_XXX >

{

    int BlahBlah;

};

여러모로 템플릿은 매크로의 유용한 대체 수단이 된다. 그런데 뭐가 더 좋은지는 각자의 선호도에 달린 문제니까, 뭐라 평가는 못하겠지만, 대안이 있음을 알고 있다는 것은 더 좋은 일임에 분명하다.

 

Posted by uhm
dev.log2007. 12. 8. 01:54

vfptr는 중요하다. C++ 동적 타입 시스템의 근간을 이루기 때문이다.

아래는 대화기록. 까까군의 프라이버시에 대한 항의가 들어오면 바로 삭제됨.

 

까까  vfptr이 뭔가요;
엄     버추얼펑션테이블을 가리키는 포인터.
(어색한 침묵)
엄     버추얼 펑션 테이블이 뭔지 모른다거나;;?
까까  ..
까까  네 -ㅅ-;;
엄     버추얼 펑션은 아냐?
까까  음
까까  ..
까까  그거
까까  앞에 virtual 해서
까까  상속받아서 쓰는 물건 아닌가요
엄     그럼 그놈이 어떻게 구현될 거 같아?
까까  음
까까  ...
까까  글쎄요;

Posted by uhm
dev.log2007. 11. 21. 01:05

우리의 GS군이 어제 STL할당자를 만들다가.. rebind때문에 고생을 좀;; (이거 모르는 사람 은근히 많다)
STL할당자는.. 어떤 타입 T에 대해서 할당을 해주도록 되어 있다. 이를테면,

template < class T, class A = std::allocator<T> > class vector;

이런 식이다. 근데 이게 벡터 같은 놈에서는 별 상관 없는데, 리스트나 데크[각주:1], 맵, 셋등 에서는 약간 미묘하다. 이들이 할당하는 건 T가 아니기 때문이랄까. 리스트는 list_node<T> 따위, 맵에서는 tree_node<T> 따위 단위로 할당하기 때문이다. 이 글을 여기까지 읽는 사람이라면, 자료구조 수업시간에 링크드 리스트나 바이너리 서치트리 따위는 다 만들어 봤을 테니, 무슨 소리인지 알 거라고 본다.

따라서 리스트, 맵, 셋 등에서 필요한 할당은 T타입이 아니라, XXXX_node<T>타입에 대한 할당자이다. 어떤 할당자 A가 T를 할당하도록 만들어져 있는 상황에서, 컨테이너한테 A가 주어졌는데, 컨테이너가 필요한 것은 A가 할당하는 T가 아니라 N<T> 타입.

이걸 위해서 STL에는 할당자가 rebind라는 타입을 내부에 정의하도록 되어 있다. rebind는 다른게 아니라, A한테 "T말고 N<T>를 할당하려면 어떤 할당자를 쓰면 좋겠느냐-"라고 물어볼 수 있는 구석을 만들어 놓겠다는 것이다. 그럼 컨테이너에서는 T가 아닌 다른 타입을 (이를테면, N<T>) 할당 할 때는, A에서 정의한 rebind라는 녀석이 지정해 놓은 할당자를 새로 만들어서 그놈한테서 할당받으면 된다는 이야기.

 템플릿 A의 멤버 템플릿 rebind 구조체는 보통 다음과 같이 정의한다.

template < class _Other >
struct rebind
{
    typedef A<_Other> other;
};

컨테이너는 A가 주어지면, A::rebind<XXXX_node>::ohter 를 할당자로 사용해서 작업하면 된다.
이러면 '끗'.


  1. deque를 '디큐' 내지는 '데큐' 라고 읽는 사람도 은근히 많다 -_- 데크는 메모리의 '블럭'단위로 할당하여 이 포인터들을 한개의 테이블에다 저장해 놓는데, 이 테이블을 할당할 때에도 rebind가 쓰인다. [본문으로]
Posted by uhm
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
dev.log2007. 3. 14. 21:11

지금까지 했던 모든 삽질의 원흉은.. 언리얼 엔진의 appSleep은, 밀리초 단위가 아닌, 그냥 '초' 단위였다는 것이란 걸 어제 발견했다. (털썩)

2차 CBT까지 한 R모 게임을 플레이하면서 "30분 기다렸는데 캐릭터가 안나와요" 하신 분들, 죄송합니다. (꾸벅) "30분동안 해도 그림자만 보여요" 하신 분들, 역시 죄송합니다. (꾸벅) "캐릭터 머리가 계속 뻥 뚫려보여요" 하신 분들께도, 역시 죄송합니다 (꾸벅)

이제 단위가 밀리초단위가 아니라 그냥 '초'단위란 걸 알았으니, 아마 캐릭터가 나타나기까지 걸리는 시간이 1/1000로 단축될 겁니다.. (아마도) 다음부턴 잘 될 겁니다 ( __)

 

Posted by uhm