dev.log2009/11/15 01:13
몇년동안이나 책장에 덩그러니 꽂혀만 있던 D&EC++을 드디어 다 읽었다. 대략적인 느낌은 왜 지금의 C++이 이모냥밖에 못되었나에 대한 변명..이랄까... 이런 느낌인데, 뭐, (스타게이트 아틀란티스의) 닥터 로드니의 말을 따오자면 "완벽한 세상에서는" 이모냥밖에 안되지 않았겠지. 하지만 우리 세상은 완벽하지 않잖아? 안될거야 아마. -_-a

책을 보면, 각각의 언어 스펙에 대한 변천사가 개략적으로 기술되는데, 일부는 현재의 솔루션이 확실히 진보했다는 느낌이 들고, 일부는 호환성이나 기술적, 관습적 한계 때문에 쉽고 우아하고 효율적인 솔루션을 포기했다는 면이 안타깝다는 느낌.

현재의 상태라서 더 낫다고 생각하는 한가지 예로는 비야네가 '상속을 통한 제약조건'이라고 이름붙인 항목에서 기술한 것이다. 이는 템플릿이 어떤 종류의 매개변수로 스페셜라이제이션 될 수 있는가를 명시할 필요성이 있지 않겠느냐는 논의에서 비롯된다. 템플릿 매개변수가 만족해야 하는 조건을 명시할 필요성은 나도 느끼는 바이고, C++0x의 concept 개념이 매우 마음에 들었지만, 현재 C++0x 표준에서는 떨어졌다고 하는 걸 보면 만족스러운 해결책은 아직도 요원하다. 그런데 이러한 논의는 C++의 템플릿 명세를 처음 만들면서도 했다는 점이다. 비야네의 동료들이 제안한 방안은 템플릿 선언시 매개변수 선언부에서 특정한 클래스에서 상속받은 타입들로만 스페셜라이제이션할 수 있음을 명시하면 어떻겠느냐는 것이다.
template <class T>
class Comparable
{
    bool operator==(const T&, const T&);
};

template <class T : Comparable>
class XXX
{
  // .....
};
일견 타당해 보이기도 하지만, 비야네의 생각으로는 근본적으로 'T가 비교가능해야 한다'고 명시하는 대신에 'T가 Comparable에서 상속받아야 한다'라고 명시하는 것은 틀린 개념이라고 봤다고 한다. 여기엔 나 역시 동의한다. C++의 템플릿 매개변수가 저런 식으로 제약조건을 명시해야 했다면 활용도가 크게 떨어졌을 듯.

C++에서 빠져서 아쉬운 것중의 하나는, 비야네가 생각만 하고 있었다는 include 키워드(프리프로세서 명령이  아닌!).
#include 전처리기 명령은 매우 무식한 방법으로 작동하여, 스코프 룰을 완전히 무시하고, 순서의존성이 매우 크다. 윈도우에서 프로그래밍해본 사람이라면 std::max 템플릿을 쓰기 위해서는 windows.h 헤더에서 선언된 max 매크로를 요리조리 피해가야 했던 경험이 있을 것 같다. 비야네는 include라는 키워드를 도입하여 유일성을 자동으로 보장해주며,  include로 포함되는 헤더에서 선언된 매크로는 해당 헤더파일 안에서만 동작하고, 거꾸로 include로 포함된 헤더에서 선언되지 않은 매크로는 해당 헤더에서 동작하지 않도록 만들고 싶었다고 한다. 대략적으로 자바의 import와 비슷하게 동작할 수 있도록 만들고 싶었던 모냥. 이는 나도 적극 찬성인데, C++에 들어가지 못한 것은 매우 안타깝다.

책 전체에 걸쳐서 이건 이렇게 하려고 했었는데 이런이런 문제가 발생해서 결국 이렇게 되고야 말았다... 이런걸 구구절절하게 써놔서..... 난 무척 재미있었다. 아! 비야네도 불완전한 세상에 살수밖에 없는 엔지니어였구나. 흑_흑

비야네도 알고 있는, 인류에게 가장 도움이 되는 조언.

저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by uhm
dev.log2009/10/27 04:15
TDD는 코딩 전에 테스트를 정의하는 방법론이다. TDD가 말하는 바는, 작성하는 코드가 어떻게 동작하여야 하는지를 정의하고, 이 정의에 부합하도록 동작하는지, 그리고 이 정의에 부합하지 않는 쪽으로 동작하지 않는지를 검증하는 코드를 먼저 짜야 한다는 것이다. 내가 지금까지 코딩해왔던 방식이랑 비교하자면, 난 일단 특정 모듈의 행동양식을 정의하고, 사용처에서 사용하고자 하는 방식을 적어놓고 컴파일해본다. 컴파일 에러가 모두 사라지고 구현하면 동작하는 코드가 나오는 방식.
의식의 흐름 기법으로 정리해 보면.
  1. "파일에서 뭘 하나 읽어와야겠군"
    int XXX::open_file( const string& filename )
    {
    }
  2. "그럼 스트림을 추상화해야겠군"
    int XXX::open_file( const string& filename )
    {
        stream file( filename );
        file.read( this->buffer, this->size );
    }
  3. "컴파일 에러로군"
    class stream
    {
    public:
        stream( const string& name );
        int read( void* buffer, size_t size );
    };

    int XXX::open_file( const string& name )
    {
        stream file( name );
        return file.read( this->buffer, this->size );
    }
  4. "구현을 해야겠지?"
    class stream
    {
    public:
        stream( const string& name );
        int read( void* buffer, size_t size );
    };

    int XXX::open_file( const string& name )
    {
        stream file( name );
        return file.read( this->buffer, this->size );
    }

    stream::stream( const string& name )
    {
        // ...................
    }
    int stream::read( void* buffer, size_t size )
    {
        // ...................
    }
  5. "파일이 제대로 열리나?"
    class stream
    {
    public:
        stream( const string& name );
        int read( void* buffer, size_t size );
    };

    int XXX::open_file( const string& name )
    {
        stream file( name );
        return file.read( this->buffer, this->size );
    }

    stream::stream( const string& name )
    {
        // ...................
    }
    int stream::read( void* buffer, size_t size )
    {
        // ...................
    }

    int main()
    {
        // ...................
        XXX x;
        int result = x.open_file( ",,,,,,,,,,,,,,,," );
        assert( result != ERROR );
        assert( memcmp( x.buffer, dummy ) == 0 );
    }
  6. "버그 잡자"
내가 코딩하는 방식과 TDD에서 제시하는 방식의 차이는, 바라는 동작을 정의하는 부분이 워킹 코드이냐 테스트용 코드이냐 하는 점이다. 지금 내 방식의 단점은, 코드가 바뀌고 나면 동작의 일관성이 깨져도 그걸 체크할 방법이 바뀐 코드와 함께 유실돼 버린다는 것. TDD의 유용성이 느껴지는 부분이다.

TDD의 방법론은 실천 지침이 명확하다는 점에서 유용한 듯 하다. "코드보다 테스트코드 먼저". 이 얼마나 간단 명료한 지침인가. 내 생각으로는, 이 간단한 지침에 숨겨진 효용은 대충 이런 것 같다. 테스트를 작성하기 위해서는 내가 쓰고자 하는 코드가 어떤 식으로 동작해야 하는지를 정의해야 한다. 동작을 정의하기 위해서는 설계를 머리 속에 담고 있어야 하고, 이는 결국 코딩하기 전에 생각하라-는 오래된 금언을 실천하기 위한 매우 유용한 방법론인 것이다.
생각해 보면, 코딩하기 전에 생각하는 것이 습관화된 사람에게 TDD는 별달리 임팩트를 주진 않을 것 같다. TDD의 사용자에게 작용하는 궁극적이고 장기적인 영향은 코딩하기 전에 생각하는 습관을 들이게 하는 거라는 점에서 그렇다. (TDD의 다른 장점은 프로젝트에 작용하는 장점이지, 사람에게 작용하는 장점이 아니다) 하지만 그럼에도 장점은 있는데, 일단 남들에게 가르치기 쉽고, 또한 각 단계가 매우 짧고 보상이 정확하기 때문에 마치 게임을 하는 것처럼 코딩을 할 수 있다는 거다. "만렙까지 알아서 레벨 올리세요"와 "남쪽골짜기에서 토끼를 잡으면 레벨이 하나 올라요"의 차이랄까. 나에게 TDD란, 프로그래밍이란 게임의 규칙이란 느낌. 한 단계씩 레벨 올려서 만렙이 되면 프로그램이 완성되는 게임.

저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by uhm
dev.log2009/09/07 02:39
대략적으로, VC++에서 구조체 멤버의 배치와 관련된 개념은 2가지. #pragma pack, 혹은 컴파일러의 /Zp 옵션으로 지정하는 패킹과, __declspec(align)으로 지정하는 정렬이 있다.

원칙적으로는 SSE명령이나 캐쉬에 적합성을 높이기 위한 메모리 배치는 __declspec(align)으로 하는 것이 맞다. 그런데 문제는 __declspec(align) 속성을 가진 벡터 객체, 혹은 이를 포함하는 클래스의 객체가 STL 컨테이너에 들어갈 수 없다는 것이다. 이는 다음 두가지 사실에서 기인한다.
1. VC++컴파일러는 __declspec(align) 속성을 가진 객체를 함수의 밸류타입 인자로 넘길 수 없게 구현돼 있다.
2. STL컨테이너들의 기본생성자는 초기화 과정에서 resize메소드를 호출하는데, 표준에 의하면 resize 메소드는 밸류 타입으로 인자를 받도록 되어 있다.
그러한 고로, 주소 정렬이 포함돼 있는 클래스의 객체는 STL컨테이너에 넣을 수가 없다는 당혹스러운 결과에 직면하게 된다.

전에 회사에서 쓰던 엔진은 vector3 클래스의 선언부를 #pragma pack(16)으로 감싸놓고 할당자만 재정의해서 쓰고 있었다. 요 벡터 클래스가 심심하면 뻗을 때가 있었는데, 대개의 경우는 객체의 주소가 16바이트 정렬이 안된 채로 생성이 되어 SSE 내장 함수에서 뻗어버리는 것이었다. 이때의 경험으로 미루어 안될거 같다는 생각을 하면서도, 집에서 놀던 차에 잉여력을 활용하여 둘 간의 차이점에 대해서 삽질을 좀 해봤다.

사용한 선언은,
#pragma pack( push, 16 )
struct test_pack
{
    float x;
    float y;
    float z;
};
#pragma pack(pop)

struct __declspec(align(16)) test_align
{
    float x;
    float y;
    float z;
};
이렇게 해놓고서 배열을 선언해서 비교해 보았다.
    FILE* out = fopen( "align_pack.txt", "w");
#define LOG_OFFSET(tag,member) fprintf( out, "offset of "  #tag "::" #member " = %d\n", offsetof( tag, member ) );
#define LOG_SIZE(tag) fprintf( out, "size of "  #tag " = %d\n", sizeof( tag ) );
#define LOG_ADDR(pointer) fprintf( out, "address of "  #pointer " = 0x%x\n", pointer );
    LOG_OFFSET( test_pack, x );
    LOG_OFFSET( test_pack, y );
    LOG_OFFSET( test_pack, z );
    LOG_SIZE( test_pack );

    test_pack tp[3];
    LOG_ADDR( &tp[0] );
    LOG_ADDR( &tp[1] );
    LOG_ADDR( &tp[2] );

    LOG_OFFSET( test_align, x );
    LOG_OFFSET( test_align, y );
    LOG_OFFSET( test_align, z );
    LOG_SIZE( test_align );

    test_align ta[3];
    LOG_ADDR( &ta[0] );
    LOG_ADDR( &ta[1] );
    LOG_ADDR( &ta[2] );

    fclose( out );
결과는,
offset of test_pack::x = 0
offset of test_pack::y = 4
offset of test_pack::z = 8
size of test_pack = 12
address of &tp[0] = 0x12fecc
address of &tp[1] = 0x12fed8
address of &tp[2] = 0x12fee4
offset of test_align::x = 0
offset of test_align::y = 4
offset of test_align::z = 8
size of test_align = 16
address of &ta[0] = 0x12fe90
address of &ta[1] = 0x12fea0
address of &ta[2] = 0x12feb0


패킹으로 선언된 test_pack의 경우는 주소 자체가 16바이트 정렬이 안되기 때문에, 패딩 바이트를 주더라도 SSE나 캐쉬라인에 맞추기 위한 목적으로는 쓰일 수 없다는 것이 분명해졌다. 구조체 멤버를 다른 타입으로 (char, short 등) 몇가지 더 추가하면서 알아낸 바로는, #pragma pack으로 지정하는 패킹은 멤버들의 구조체 내에서의 옵셋에만 영향을 주고, 구조체 자체의 배치에는 영향을 끼치지 않는다는 것.

vector3 클래스를 생성 영역에 따라 분리하는 것도 방법. 힙에서 생성할 객체들은 할당자에 의해 정렬이 되는 클래스로 분리하고, 스택이나 정적 데이터 영역에 생성할 객체들은 __declspec(align)으로 분리한다는 것인데... 이럴 경우에도 STL의 메소드가 내부적으로 사용하는 임시 객체가 정렬이 안이루어지면 뻗을 수밖에 없다.

이제 vector3 클래스를 SSE를 활용하면서 컨테이너에 넣기 위한 방법은 1가지밖에 없다는 결론이 도출된다. 바로 사용하고자 하는 모든 컨테이너 클래스의 메소드를 밸류 타입으로 인자를 받지 않도록 상수 참조의 형태로 바꿔서 재정의한다는 것. 대부분의 STL컨테이너를 커스터마이징해야 한다는 얘기. 이건 삽질도 보통 삽질이 아닌데.....


저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by uhm
TAG Align, C++, SSE, STL, 정렬
dev.log2009/07/03 17:17
행렬 클래스를 다음과 같이 구현해보려고 했었다.
struct matrix
{
    vector4 m[4];
    matrix( scalar m11, scalar m21, scalar m31, scalar m41,
               scalar m12, scalar m22, scalar m32, scalar m42,
               scalar m13, scalar m23, scalar m33, scalar m43,
               scalar m14, scalar m24, scalar m34, scalar m44 )
        : m[0]( m11, m21, m31, m41 ),
          m[1]( m12, m22, m32, m42 ),
          m[2]( m13, m23, m33, m43 ),
          m[3]( m14, m24, m34, m44 )
    {
    }
};

결론적으로 말하자면, 위와 같은 구문은 허용되지 않는다. 저런 식으로 접근해 보려고 1시간쯤 삽질했는데, 결과적으로는 안되는 거였다. 잠깐 생각해 보니 알 수 있었는데, 배열의 초기화는 {}로 묶인 배열의 초기화 목록으로만 가능하기 때문이다. 저런 구문이 작동 가능하려면 전역 배열도 다음과 같이 초기화할 수 있어야 한다.
[header]
extern int array[4];

[source]
int array[0](0);
int array[1](1);
int array[2](2);
int array[3](3);
배열의 이름/크기 선언과 별도로 배열의 내용물을 각기 따로 초기화할 수 있는 방법이 있다면 위의 구문이나 아래의 구문이나 내용면에서는 차이가 없는데, 아래와 같은 선언이 당연히 안될 거라는 것에 생각이 미치자 위의 것도 당연히 안된다는 걸 깨달았다. 기본 생성자에서 아무것도 안하게 하면 생성자 바디에서 초기화해도 오버헤드는 없으므로 상관은 없지만, 그래도 초기화는 초기화 리스트에서 해주는 것을 선호하는 지라, 약간 불만.

그리고 또 하나의 삽질은, 행렬의 SSE구현에서 다음과 같은 생성자를 만들려고 한 것이었다.
struct matrix
{
    matrix( __m128 v1, __m128 v2, __m128 v3, __m128 v4 )
    {
        //............
    }
};
__m128 구조체는 VC에서 선언한 SSE 내장함수용 16바이트 정렬 구조체인데, 위 생성자를 컴파일하면 2719 에러가 난다.
http://msdn.microsoft.com/ko-kr/library/373ak2y1.aspx
그냥 안되는 거였슴. 줵.

저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by uhm
dev.log2009/06/02 17:21
누구나 이와같은 실수를 할 수 있다. 이런 실수의 근원은 루프의 현재 값과 루프 카운터가 일치하지 않는다는 점. 이를테면, 현재 값을 배열로 dataset[i] 등으로 루프카운터를 직접 사용했다면 문제의 소지가 전혀 없었을 것이다. 하지만 루프카운터와 현재 값이 따로 변함으로써 버그가 된 케이스. 따라서 루프를 돌면 현재 값이 직접적으로 변하도록 루프를 구성하는 것이 버그를 미연에 방지할 수 있다.
for ( LIST* cur = header; cur != 0; cur = cur->next )
{
     some_function( *cur );
     // .........................
}

만약 현재 인덱스가 필요하다면 별도의 카운터를 두는 것이 안전.
int index = 0;
for ( LIST* cur = header; cur != 0; cur = cur->next, ++index )
{
     some_function( *cur );
     // .........................
}

사실 거의 모든 C/C++ 학습서가 for 루프의 루프카운터를 정수형 변수로 쓰는 예제들만 소개하고 있기 때문에 위와 같은 형태는 초심자에게는 익숙하지 않을 수도 있다. 하지만 루프 카운터는 어떠한 타입의 변수라도 상관이 없으므로, 루프의 불변값과 종료조건을 대표할 수 있는 것으로 정하는 것이 바람직하다.

위와 같은 일반적인 루프카운터의 개념을 캡슐화한 것이 이터레이터(iterator)라는 추상개념. C++ STL의 근간이 되는 개념이다. (이터레이터는 개념이라, C++0x에서는 iterator가 concept 로 선언된다.)
for( list::iterator cur = data.begin(); cur != data.end(); ++cur )
{
     some_function( *cur );
     // ...............
}

중요한 것은, 루프의 현재 값과 종료조건을 언어의 문법적 레벨에서 결합시켜 놔야 안전한 루프가 된다는 점.

저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by uhm
dev.log2009/04/09 12:59
라이브러리, 특히 C++ 라이브러리 배포 시에 인터페이스'만' 배포하는 것은 상당히 귀찮은 일이다. 나같은 경우는 클래스에 nested type을 무척 많이 쓰고, 일부는 구현에만 사용하는 타입이기 때문에 nested type이 외부로 나가면 내부 구현을 까발리는 것과 다를 바가 없다. 물론 private 권한으로 숨겨놓을 수도 있지만, 뻔히 헤더 소스에 보이는 걸 숨겨놨다고 하기엔 옹색한 것도 사실이다. 그래서 만들고 있는 파일 스트림 라이브러리는 다음과 같이 하기로 했다.

배포용 헤더. 바이너리 라이브러리만 배포할 때는 요놈만 쓴다.
class stream
{
public:
    static stream* create( const string& strm_path );
    static stream* create();
    virtual ~stream();

    virtual int open( const string& strm_path ) =0;
    virtual int close() = 0;
    virtual int read( void* buffer, size_t size ) = 0;
    virtual int write( void* buffer, size_t size ) = 0;
    virtual size_t tell() = 0;
    virtual int seek( size_t offset ) = 0;

protected:
    stream() {}
    stream( const stream& ) {}
};
기본 생성자와 기본복사생성자를 protected로 막아놓고 Create로 쓰는 이유는, 어디까지나 인터페이스이기 때문. 구현 레벨에서 다형성 타입으로 생성하도록 한다.


구현용 헤더
class stream_impl
{
    virtual int open( const string& strm_path ) =0;
    virtual int close() = 0;
    virtual int read( void* buffer, size_t size ) = 0;
    virtual int write( void* buffer, size_t size ) = 0;
    virtual size_t tell() = 0;
    virtual int seek( size_t offset ) = 0;
};

class stream_file : public stream_impl
{
public:
    //...
protected:
    fstream _file;
};

class archive;
class archive_item;
class stream_pack : public stream_impl
{
public:
    //...
protected:
    archive* _pack;
    archive_item* _item;
};

class stream_class : public stream
{
public:
    stream_class();
    stream_class( stream_impl* impl );
    //...
protected:
    stream_impl* _impl;
};

class archive
{
    int open_pack( const string& path );
    int close_pack();

    archive_item* open( const string& path );
    int close( archive_item* item );
    int read( .. );
    int write( .. );
    int seek( .. );
    int tell( .. );
};
stream_class와 stream_impl을 분리한 이유는, 동일한 stream 객체를 서로 다른 종류의 스트림으로 다시 열 수 있도록 하기 위해서.

구현용 소스는 대략 다음과 같은 모양새가 된다.
stream* stream::create( const string& strm_path )
{
    stream_class* strm = new stream_class;
    strm->open( strm_path );
    return strm;
}

int stream::open( const string& strm_path )
{
    if ( _impl )
    {
        delete _impl;
        _impl = 0;
    }

    if ( search_package( strm_path ) )
        _impl = new stream_pack;
    else if ( search_file( strm_path ) )
        _impl = new stream_file;
    else
        return 0;

    return _impl->open( strm_path );
}

약간 귀찮긴 하지만 '싫어하는 사람'에게 라이브러리를 배포해야 할 때에는 유용할지도.. 생각해보면 위와 같은 모양새를 가리키는 디자인 패턴의 이름도 있을 거 같긴 한데, 찾아보기 귀찮다. ( ..)

저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by uhm
dev.log2009/03/03 18:17
회사에는 goto를 좋아하는 사람이 한명 있다. goto를 그냥 남발하는 게 아니다. 진심으로 goto를 좋아해서 쓴다 -_-a 이런 종류의 인간이 제일 난감한데, 이런 식이다. '남들이 다 쓰지 말라고 하는 거지만 난 잘 쓸 자신이 있다'는 식이다. 남들과 달리 잘 쓸 수 있으므로 쓰겠다는 입장. 그래서 모든 로직의 핵심을 goto로 구현해 놓고 자랑스러워한다. '이 로직은 goto로 완벽하게 짜봤지요' ㄷㄷㄷ

bool func()
{
    bool succ = false;
    mutex.lock();
    if ( do_something() == false )
       goto exit;

    if ( do_another() == false )
       goto exit;

    if ( do_blabla() == false )
       goto exit;

    succ = true;
exit:
    mutex.unlock();
    return succ;
}


사실 난 되도록이면 코드는 자기가 짜고 싶은 대로 짜면 된다;는 입장이라, 이 인간이 goto를 쓰건 말건 걍 놔 뒀었다. 그리고 위와 같은 코드는 일반적으로 용인되는 goto의 쓰임과 크게 어긋나지도 않기 때문에. 문제는 이런 부분.

void* get()
{
retry:
    mutex.lock();  

    if ( !available )
    {
        if ( try_alloc() == false )
            goto must_alloc
    }

    void* result = do_postprocess();
    mutex.unlock();
    goto done;   

must_alloc:
    mutex.unlock();

    if ( alloc()  )
        goto retry;
    return 0;

done:
    return result;
}

대충 이런식. 이 코드를 내가 디버깅해야 할 비상사태가 오면서 문제가 불거지기 전까지는 문제의 심각성을 몰랐다. -_- 뒤로갔다 앞으로 갔다 하는 goto의 흐름을 쫓다가 디버깅시간의 2/3가 흘러간 걸 보고서 고쳐놔야 겠다는 생각이 들었다.

사실 앞의 코드는 너무 쉽다. 그냥 true/false를 바로 리턴하도록 바꾸고 뮤텍스는 로컬 객체로 래핑해서 생성자/소멸자에서 lock/unlock을 부르게 하면 되니까. 뒤에 꺼는 좀 까다롭다. 분명히 루프이긴 한데, 루프의 종료조건이 뭔지 알기가 매우 힘들다 -_- 종료조건이라 하더라도 거기에서도 분기가 두갈래다. 깔끔한 코드를 만들기가 어렵다. 나름 도전의식이 생기는 코드랄까.

저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by uhm
dev.log2009/02/23 00:09
C++ 0x 표준안이 올해 확정된다는 소식을 들었다. Design & Evolution of C++에서 봤던 변천 과정에 한가지가 더해진다는 느낌. D&EC++은.. 읽다가 말기도 했고, 오래전이라 기억이 가물가물하기도 해서 한번 뒤져봤다.

1979 : C with Classes
Bjarne Stroustrup께서(이하 B.S.) 시뮬라에서 힌트를 얻어 C의 개량판을 구상하기 시작하던 시절이다. 시뮬라는 본격 객체 지향 언어였지만, CwC에는 그냥 ADT(abstract data type) 개념만을 집어넣고 C에서는 거의 하지 않았던 타입 체크를 강화하는 쪽으로 구현. 이때는 C 컴파일 단계 전에 파싱하여 C소스코드를 생성하는 전처리기로 구현했었다. D&EC++에 따르면, 당시의 CwC는 다음 기능을 갖고 있었다.

  • classes
  • inheritance
  • access control (private/public)
  • cons/des (new/delete)
  • monitor (call/return)
  • friend classes
  • type check (undeclared function, void args, conversion)
  • inline func
  • default args
  • assignment overloading

대부분은 아직까지 유지되고 있지만 변경되거나, 없어진 기능들도 있다. 이를테면, call/return 멤버 메소드를 이용한 - call에서 락을 걸고, return에서 락을 해제하는, 일종의 콜백개념 - 동기화 구현은 B.S.자신을 제외한 그 누구도 쓰지 않았기에 없애는 쪽으로 결정되었다고 한다 -_-a 그리고 당시에는 생성자와 소멸자 메소드가 new/delete 라는 이름을 갖고 있었다. 그리고 연산자 오버로딩은 = 연산자만 지원.

1982 : C++ (Cfront)
C++이란 이름을 처음 갖고 발표된 C++ release 1.0의 시기. 하지만 여전히 C의 전처리기로 구현되었으며, 그 전처리기의 이름은 Cfront. Cfront의 기능은 위의 것에 더하여 다음이 추가.

  • virtual functions
  • function/operator overloading
  • reference
  • const
  • new operator
  • type check ( func args, ... list)
  • line comment
  • scope resolution

이때는 가상함수를 통한 동적 다형성을 지원하면서 객체지향 언어의 면모를 갖추게 된다. 또한 타입 체크가 점점 강화되는 쪽으로 가는데, 그것은 초기 C++의 설계 정책중 하나가 C와의 하위호환성 보장이었기 때문에, C코드를 컴파일할 수 없는 언어 스펙은 보급이 곤란하기 때문이었다고나. 충분한 수의 C++유저가 확보되지 않은 상태에서는 점진성이 타당한 선택이었을 듯. 그외에는 사소한 차이점들. 지금과 같은 개념의 new연산자가 도입되면서, 기존의 new/delete대신 지금의 형태를 가진 생성자/소멸자가 도입되었다. 또한 이때의 오버로딩에는 overload라는 키워드가 필요했다. 1984년에는 The C++ Programming Language가 출간되기도.

1985 : C++ R2.0
사람들의 관심이 서서히 증가하면서 달라진 기능의 C++ release 2.0을 발표. 이 시기는 B.S.가 만든 Cfront 프리프로세서를 대체할 (진짜) 컴파일러가 여기저기서 만들어지기 시작한 때이다.

  • multiple inheritance
  • type-safe linkage
  • overloading resolution
  • init/assign definition in member-wise basis
  • dynamic initialization
  • new overloading
  • abstract class
  • static member func
  • const member func
  • protected members
  • member to pointer (->) overloading
  • pointers to members
  • libraries

B.S의 말에 따르면 R2.0으로의 변화는 기능의 추가라기보다는 제약의 해제라고 한다. 단1개의 베이스클래스에서 여러개의 베이스클래스를 허용한다든가, 그전까지 오버로딩 불가능했던 연산자들에 대한 허용이라거나; 하는 것들. 또한 지금의 형태를 가진 기본적 라이브러리가 구현되기 시작한 시기이기도 하다. 물론, 이때는 템플릿이 없었으므로 주로 stream IO나 문자열 라이브러리들부터 구현되었다.
   
1988 : C++ARM
Annotated Reference Manual이 발표되면서 지금의 C++의 모습이 거의 갖추어진다. 다음의 변화가 추가되었다.

  • template
  • exception
  • nested class
  • pre/post ++ overloading
  • local statics
  • volatile
  • STL

이 시기의 C++이 가장 많은 사람들에게 알려진 C++의 형태이다. <iostream.h> 헤더를 사용하던 형태가 바로 이때의 C++. 표준화가 이루어지기 직전이다. 이때의 C++ 문법들은 거의 지금 그대로 남아있다. 가장 중요한 변화는 템플릿의 도입인데, 이로 인해서 C++이 지원하는 프로그래밍 패러다임이 하나 더 늘어나게 되었다. 템플릿과 함께 STL도 도입.
   
1998 : C++98
표준화가 이루어진 C++의 스펙이다. ISO/IEC 14882로 검색해 보면 표준에 대한 설명을 찾아 볼 수 있다. 주요 변경사항은 다음과 같다.

  • runtime type information
  • namespace
  • casting operators
  • bool type
  • explicit template instantiation
  • explicit template args in template function calls

표준화 작업이 거의 10년이나 지속됐기에, ARM형태의 C++이 널리 퍼진 다음에 발표된 표준은 확산이 매우 더뎠던 걸로 기억한다. MSVC도 최근에 와서야 C++98 스펙을 거의 다 지원하게 되었으니 말 다한 셈. 이때부터는 표준라이브러리는 모두 std네임스페이스에 속하도록 정해졌으며 기존 ARM형태의 라이브러리와 혼동을 피하기 위해 표준헤더도 .h확장자를 떼는 쪽으로 결정.

   
2003: C++03
1998년 표준 문서에서 발견된 모순이나 오류들을 수정한 문서이다. 그다지 크게 달라지지는 않았으나 스펙이 명료한 표현으로 대체되었다. 따라서 현재 C++ 표준은 ISO/IEC 14882:2003 이다. 주목할만한 점이라면 vector의 메모리 연속성에 대한 보장이 표준에 의해 명시되었다는 점 정도.

   
2005 : C++TR1
C++ 표준 라이브러리의 확장을 위한 첫번째 technical report. C++유저들 사이에 널리 쓰이거나 요구되는 라이브러리를 정리한 공식 표준으로 ISO/IEC TR19768로 검색하면 관련 내용이 나온다.

  • tuple
  • array containner
  • unordered_map
  • regular expression
  • reference wrapper
  • polymorph wrapper
  • smart pointer
  • func binder
  • return type wrapper
  • random number lib

면면을 살펴보면 기존의 Boost 라이브러리에 있던 개념들이 다수 포함되었다. Boost 자체가 C++표준화 위원회의 임원들이 다수 포진하여 만든 라이브러리이니 당연한 결과일지도 -_-a 전체적으로는 요즘 기조에 맞게 템플릿의 활용도를 높이는 쪽으로 가고 있다.


2009 : C++0x
ISO/IEC14882:2003을 대체할 차기 표준안. 올해 확정되는 바로 그 C++ 스펙이다. 엄청나게 많은 점이 달라진다.

  • template >> priority
  • const expression
  • sizeof member without object
  • peer constructor
  • unrestricted friend
  • extern template instantiation
  • r-value reference
  • long long int
  • null type
  • enum class
  • attributes
  • explicit conversion
  • unrestricted union
  • static_assert
  • control of implicit member
  • initializer constructor
  • inheriting constructor
  • return type abduction
  • template typedef
  • string literals (encoding, raw)
  • user-defined literals
  • thread_local

사실 여기까지는 사소한 변화이다. C++ 유저들의 스타일을 근본적으로 바꿀 변화는 따로 있다.

  • type inference
  • vari arg template
  • concept
  • ranged for
  • lambda func

각 항목의 변화가 덩치가 너무 크므로 여기선 생략. 여튼 이로 인해 표준 라이브러리에 다음 구성요소가 추가된다.

  • tuple
  • range
  • unordered_map
  • regular expression
  • smart pointer
  • random
  • reference wrapper
  • polymorph wrapper
  • general function binder
  • threading (thread, mutex, condition, atomics)
  • type_traits
  • return type wrapper

라이브러리 구성요소는 TR1의 스펙이 거의 다 포함되는 쪽이며, 오히려 컨셉트나 가변 템플릿 매개변수 같은 언어 자체의 변경사항 덕분에 더 명료해진 편이라는 인상.

이런저런 것들이 바뀌었지만 대체적으로 기존 표준에 적합한 코드는 거의 다 제대로 동작한다. 오히려 더 간편한 표현을 가능하게 하는 요소가 생겼으므로 (람다나, 초기화목록 생성자, 범위기반 for 같은) 더 유연한 언어가 되었다고 봐야 맞을 듯.
하지만, 안그래도 '메저키스트의 언어'라는 평을 듣는 C++에 이런 것들을 추가하면 메저키스트 정도로는 끝나지 않을 것 같기도 하다. 뭐, 사용자도 사용자지만, C++98이 제대로 구현되는데 거의 10년이나 걸렸음을 감안하면, C++0x를 제대로 구현한 컴파일러는 2019년쯤에나 나올거 같다.

저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by uhm
dev.log2009/02/17 00:00
행렬을 구현하는 데에 있어서 가장 어려운 것은, 바로 메모리상의 배열 우선순위(major ordering)를 정하는 것이다. 단위 배열로 행벡터를 쓸 것인가 열벡터를 쓸 것인가를 결정하는 것인데, 행벡터는 행렬에 있어서 같은 행번호를 가진 원소가 서로 인접하도록 메모리 상에 배치하는 것이고, 열벡터는 같은 열 번호를 가진 원소가 서로 인접하도록 메모리 상에 배치하는 것. 어차피 둘 중의 하나 밖에 선택할 수 없기 때문에 심플한 문제이기는 한데, 좀 미묘한 문제가 걸려 있다.

우선, 행우선(row major) 배치는, 다음을 보자.


이와 같은 행렬이 행 우선 배열에서는 메모리 상에 선형으로 다음과 같이 배치된다.

 a00 a01 a02 a03 a10 a11 a12 a13 a20 a21 a22 a23 a30 a31 a32 a33

이는 C/C++의 2차원 배열 선언과 동일한 배치이다.

float a[4][4]

위와같은 선언은 메모리를 다음과 같이 배치한다.

a[0][0] a[0][1] a[0][2] a[0][3] a[1][0] a[1][1] a[1][2] a[1][3] a[2][0] a[2][1] a[2][2] a[2][3]
... a[3][0] a[3][1] a[3][2] a[3][3]

따라서, 배열의 첨자가 그냥 순서대로 [행번호][열번호]가 된다는 편리함이 있다. 행렬의 이름이 '행', '열' 순서이므로 이름과 잘 일치한다. 또한 C/C++의 기본 배열과 개념상 일치해 보인다는 장점이 있고, Direct3D에서 사용하는 방식이므로 별다른 변환 없이 Direct3D에서 사용이 가능하다. 유혹적이다.

반면, 열우선(column major) 배치는 행렬을 메모리에 배열하는 방법이 반대다.

 a00 a10 a20 a30 a01 a11 a21 a31 a02 a12 a22 a32 a03 a13 a23 a33

이를 2차원 배열로 표현한다면 [열번호][행번호]의 순서로 첨자를 부여해야 한다는 점이 C/C++의 표준과 불일치한다는 인상을 준다. 하지만 어차피 가로/세로의 선호는 사람에 따라 달라지는 것이므로 열이 앞이냐 행이 앞이냐 하는 문제는 표준에 정의되어 있지 않은 문제이다. 메모리를 가로 방향으로 나열해서 그렇지, 다음과 같이 나열하면 자연스러워 보인다.

 a00
 a10
 a20
 a30
 a01
 a11
 a21
 a31
 a02
 a12
 a22
 a32
 a03
 a13
 a23
 a33

이는 전적으로 메모리가 위아래로 뻗어있느냐 양옆으로 뻗어있느냐로 보는 것의 차이인데, 사실 어느 쪽을 택해도 무방하다. 물론 행렬을 표현한 2차원 배열에서 첨자의 순서가 '행','열' 순서가 아닌 점은 약간 불편할 것 같긴 하다. 하지만 흔히 쓰는 수학적인 (벡터를 세로로 적고 행렬의 뒤에 곱하는) 표현과 일치한다는 장점이 있고, OpenGL에서 사용하는 오더링이므로 OpenGL에서 별다른 변환 없이 사용이 가능하다.

ice 프로젝트의 요구조건중 하나가 OpenGL과 Direct3D의 동시 지원이었기 때문에, 가능한 방법은 3가지다
1. 행 우선 배치로 구현하고, OpenGL에서는 일일이 transpose하여 로드한다.
2. 열 우선 배치로 구현하고, Direct3D에서는 일일이 transpose하여 로드한다.
3. 둘 다 구현한다.
사실, 둘 다 구현하는게 바람직하긴 한데, 문제는 그렇게 할 경우에 클래스가 2개 생기므로 이름 공간이 더럽혀지는 결과가 초래되는 점이 아름답지 못하다. row_matrix와 col_matrix가 공존하는 구조를 용납할 수는 없는 법.

그래서 전에 벡터 클래스를 만들 때 SSE 스페셜라이제이션을 템플릿 매개변수로 컨트롤했던 것을 약간 이용하여, 역시 템플릿 매개변수로 major ordering을 결정하는 방법을 취하기로 했다.
enum SPECIALIZE_POLICY
{
    SPECIALIZE_DEFAULT,
    SPECIALIZE_SSE,
};

enum MAJOR_ORDER
{
    ROW_MAJOR,
    COLUMN_MAJOR,
};

// OpenGL용 열우선 배열을 기본으로 구현
template <class scalar_t, MAJOR_ORDER order = COLUMN_MAJOR, SPECIALIZE_POLICY policy = SPECIALIZE_DEFAULT >
struct matrix4t
{
    enum { MAJOR_VECTOR = order };
    typedef scalar_t                    scalar_type;
    typedef vector4t<scalar_t, policy>  vector_type;
    typedef vector3t<scalar_t, policy>  coord_type;
    union
    {
        vector_type v[4];
        scalar_type s[16];
    } m;
public:
    matrix4t() {}
    matrix4t(   scalar_type _11, scalar_type _12, scalar_type _13, scalar_type _14,
                scalar_type _21, scalar_type _22, scalar_type _23, scalar_type _24,
                scalar_type _31, scalar_type _32, scalar_type _33, scalar_type _34,
                scalar_type _41, scalar_type _42, scalar_type _43, scalar_type _44 )
                :m.v[0](_11,_21,_31,_41),
                 m.v[1](_12,_22,_32,_42),
                 m.v[2](_13,_23,_33,_43),
                 m.v[3](_14,_24,_34,_44)
    {
    }
    template <class scalar>
    explicit matrix4t( const scalar* a )
        :m.v[0](a[0], a[4], a[8], a[12] ),
         m.v[1](a[1], a[5], a[9], a[13] ),
         m.v[2](a[2], a[6], a[10],a[14]),
         m.v[3](a[3], a[7], a[11],a[15])
    {
    }
};

// Direct3D용 행 우선 배열을 위한 부분 스페셜라이제이션
template < class scalar_t, SPECIALIZE_POLICY policy >
struct matrix4t< scalar_t, ROW_MAJOR, policy >
{
    enum { MAJOR_VECTOR = ROW_MAJOR };
    typedef scalar_t                    scalar_type;
    typedef vector4t<scalar_t, policy>  vector_type;
    typedef vector3t<scalar_t, policy>  coord_type;
    union
    {
        vector_type v[4];
        scalar_type s[16];
    } m;

   
    static const matrix4t zero;
    static const matrix4t identity;
public:
    matrix4t() {}

    matrix4t(   scalar_type _11, scalar_type _12, scalar_type _13, scalar_type _14,
                scalar_type _21, scalar_type _22, scalar_type _23, scalar_type _24,
                scalar_type _31, scalar_type _32, scalar_type _33, scalar_type _34,
                scalar_type _41, scalar_type _42, scalar_type _43, scalar_type _44 )
                :m.v[0](_11,_12,_13,_14),
                 m.v[1](_21,_22,_23,_24),
                 m.v[2](_31,_32,_33,_34),
                 m.v[3](_41,_42,_43,_44)
    {
    }

    template <class scalar>
    explicit matrix4t( const scalar* a )
        :m.v[0](a[0], a[1], a[2], a[3] ),
         m.v[1](a[4], a[5], a[6], a[7] ),
         m.v[2](a[8], a[9], a[10],a[11]),
         m.v[3](a[12],a[13],a[14],a[15])
    {
    }
};

// SSE 템플릿 매개변수는 명시적으로 스페셜라이제이션할 경우에만 허용하도록 막아둠
template< class scalar_t, MAJOR_ORDER order >
struct matrix4t< scalar_t, order, SPECIALIZE_SSE >
{
    scalar_t error[0]; // SSE spcecialization is not allowed unless explicit
};

쓸 때는 어떤 오더링을 쓸 것인지와 SSE 스페셜라이제이션을 쓸 것인지를 명시하면 된다.

matrix4t<float, COLUMN_MAJOR, SPECIALIZE_DEFAULT >

물론, 위와 같이 쓰는 것은 불편하므로, OpenGL 모듈에서는 다음과 같이 타입으로 선언하는 것이 편리.

[render_ogl.h]
typedef matrix4t<float, COLUMN_MAJOR, SPECIALIZE_DEFAULT > matrix4;

물론, Direct3D모듈에서도 비슷하게 타입으로 선언할 수 있다.

[render_d3d.h]
typedef matrix4t<float, ROW_MAJOR, SPECIALIZE_DEFAULT > matrix4;

구현은 두개를 다 해야 하니 빡세도, 쓸때는 골라쓰는 재미가 있을듯; ice 프로젝트의 철학은 '클라이언트 측에서 엔진의 구성요소를 고를 수 있게 한다'는 것이므로.

저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by uhm
dev.log2009/02/11 01:02
회사에서 하는 금요 세미나. 저번주에는 K군이 크리티컬 섹션에 대해 발표했었다. K군이 발표에 삽질을 좀 했지만, 크리티컬 섹션 문제는 parallelism에서 매우 중요한 개념이므로 정리하고 넘어가자.

크리티컬 섹션이란, 두 개 이상의 쓰레드(혹은 프로세스)가 있을 때, 같은 공유자원에 접근하고자 시도하는 코드 구간을 말한다. 크리티컬 섹션이 왜 치명적인가 하면, 크리티컬 섹션이 아닌 구간에서는 패러럴리즘에 의해 잘못된 결과가 나올 리가 없기 떄문이다. 논리적으로 타당함에도 불구하고 패러럴리즘에 의해 잘못된 결과가 나올 수 있는 부분은 공유자원을 두 개 이상의 쓰레드에서 동시에 접근하려고 시도하는 곳 외에는 없다. 그래서, 다른 쓰레드와의 공유 자원에 접근하려고 하는 부분을 '치명적 구간'이라고 하는 것이다. 일부 번역서에서는 '임계영역'이라고 하기도 하는데, '제대로 관리하지 않으면 오류가 일어나는 부분'이라는 의미가 잘 살아나지 않는 것 같다. 개인적으로는 '치명적 구간'이라고 하기를 좋아하지만, 이래저래 (잘못된) 번역에 익숙해진 한국 프로그래머가 많으므로 난 그냥 '크리티컬 섹션'이라고 한다.

크리티컬 섹션을 제대로 처리해주지 않으면 잘못된 결과가 나오는 것은 전형적으로 다음과 같은 두 쓰레드를 생각하면 된다.

thread a :
int i=0;
void thread_a()
{
    i++;
}

thread b:
extern int i;
void thread_b()
{
    i--;
}

쓰레드 a와 b가 동시에 시작했다고 치면, 두 쓰레드가 종료했을 때 i의 값은 -1, 0, 1의 세 값중 어느 값이라도 가질 수 있게 된다. 이는 C++의 단항 증감연산자가 기계어로는 로드/산술연산/저장의 3가지 단계로 나뉘기 때문이다. 단순하게 생각하자면 A쓰레드의 로드/연산/저장이 순서대로 실행될 때 사이사이에 B쓰레드의 로드/연산/저장이 끼어든다고 보면 된다. 가능한 경우의 수는 많지만 대표적인 결과가 나오는 경우만 보면 다음과 같다.

A로드 > A연산 > A저장 > B로드 > B연산 > B저장 ==>  0
A로드 > B로드 > A연산 > A저장 > B연산 > B저장 ==> -1
A로드 > B로드 > A연산 > B연산 > B저장 > A저장 ==>  1

따라서 크리티컬 섹션 문제를 해결하는 것은 특정 쓰레드의 동작 사이에 다른 쓰레드가 끼어들지 못하도록 어떻게 하느냐 하는 것으로 귀결된다. 대략적으로 크리티컬 섹션 문제의 해결책은 다음과 같이 사용하게 된다.

wait( S )      // wait for entering critical section
..............    // critical section
signal( S )   // allow other threads to enter critical section

OS책에서는 크리티컬 섹션 문제의 해결책은 다음 조건을 만족해야 한다고 나온다. 중요하다.
  • Mutual Exclusion (상호 배제)
  • Progress (진행)
  • Bounded Waiting (유한 대기)
상호 배제는, 크리티컬 섹션 문제의 본질인, 한 쓰레드가 자원 R에 대한 크리티컬 섹션을 수행중일 때에는, 다른 쓰레드가 같은 자원 R에 대한 크리티컬 섹션에 들어가서는 안된다는 뜻이다. 진행 조건은, R에 대한 크리티컬 섹션을 수행중인 쓰레드가 하나도 없을 때에는, 어떤 쓰레드가 R에 대한 크리티컬 섹션에 들어가려고 하면 즉시 진행이 되어야 한다는 조건이다. 유한 대기는 말 그대로, 어떤 쓰레드가 R의 크리티컬 섹션을 수행하려고 할 때에는 다른 쓰레드의 R 크리티컬 섹션 진입이 유한한 횟수만큼만 이루어져야 한다는 조건. 당연한 소리같아보이지만, OS책에 나오는 크리티컬 섹션 문제의 솔루션들은 위 3가지 조건을 만족하게 만들기 까지의 발전과정이다. 언뜻 금방 생각할 수 있는 해결책들은 위 3가지 조건을 모두 만족하기가 쉽지 않다.

크리티컬 섹션 문제 해결에 가장 중요한 개념은 wait와 signal에 atomic operation이 도입되었다는 점이다.atomic operation을 뭐라 번역해야 할지는 모르겠지만, (원자적 연산;같은 구린 번역은 꺼져라!) atom의 원래의미인 '쪼갤 수 없는' 연산이란 뜻이다. (atom을 물질계에 적용한 용어인 '원자'가 어울리는 번역이 아닌 이유) 위에서C++의 단항 증감 연산자가 로드/연산/저장의 3단계로 나뉘어서 배열될 수 있는 데 비해, 아토믹 연산은 항상 단체로몰려다니고, 쪼개져서 배열될 수 없는 연산을 가리키는 개념이다. 현대적인 CPU는 모두 다 CPU의 기본 명령어중에 아토믹 연산이 명시되어 나오므로, OS는 크리티컬 섹션 문제의 해결책에 CPU의 아토믹 명령을 쓸 수 있다. 하드웨어의 도움으로 좀 더 우아한 해결책이 나올 수 있는 것은 자명한 일.

대표적인 해결책은 위대한 컴퓨터 과학자인 Dijkstra가 제안한 세마포다. 세마포의 기본적인 아이디어는 공유자원에 동시에 접근할 수 있는 쓰레드의 갯수를 미리 정해두고 지정된 수의 쓰레드만이 크리티컬 섹션에 진입할 수 있도록 한다;는 것이다. 2개 이상의 쓰레드가 동시에 같은 자원을 공유하는 크리티컬 섹션에 들어갈 수 있으면 counting semaphore, 오직 1개만이 크리티컬 섹션에 진입할 수 있으면 binary semaphore라고 한다. 사실 counting semaphore는 binary semaphore를 이용해서 구현할 수 있으므로 binary semaphore가 더 중요하다.
세마포를 이용한 크리티컬 섹션 문제의 해결책은 크게 두가지 구현방법이 있을 수 있다. 한 가지는 busy waiting, 혹은 spinlock이라고 부르는, 쓰레드가 계속 루프를 돌면서 크리티컬 섹션에 진입하기를 기다리는 방법이고, 다른 한 가지는 쓰레드 자체를 대기 상태로 바꿔버리고 세마포의 대기 큐에 들어가게 만드는 것이다. 각기 일장 일단이 있는데, 스핀락 구현은 크리티컬 섹션 진입에 컨텍스트 스위칭에 들어가는 오버헤드가 없다는 점은 좋지만 싱글 프로세서 시스템에서는 쓸 수 없다는 단점이 있다. (한 쓰레드가 CPU에서 계속 루프를 돌고 있는데 다른 쓰레드가 CPU에 올라갈 수 있을 리가 없다) 반면, 쓰레드를 대기 상태로 바꾸는 해결책은 싱글 프로세서 시스템에서도 별다른 문제 없이 쓸 수 있지만, 컨텍스트 스위칭 비용이 들어가게 된다는 점이 좋지 못하다.
결국, 크리티컬 섹션 진입 빈도에 따라 적절한 방법을 선택하는 것이 중요하다는 말씀.

저작자 표시 비영리 동일 조건 변경 허락
크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by uhm