MSVC 2008 STL vector

사건의 발단은 VC9으로 테스팅하던 코드를 VC7.1로 포팅하면서 발생했습니다. 이상한 점은 VC7.1이 더 빠른겁니다. ?! 그것도 무려 60%정도였습니다. VC7.1은 STLPort를 사용하고 있었고, VC9은 MS에서 제공하는 녀석을 쓰고 있었지요.

일단, 코드부터 봅시다.

// STLPort 5.2.1 _vector.h:121
typedef _Tp value_type;
typedef value_type* pointer;
typedef const value_type* const_pointer;
typedef value_type* iterator;
typedef const value_type* const_iterator;
// MSVC 2008 vector:1886
// 줄바꿈은 제가 한겁니다. @_@
typedef _Vb_const_iterator<size_type, difference_type, _Myt>
    const_iterator;
typedef _Vb_iterator<size_type, difference_type, _Myt>
    iterator;

..(생략)..

typedef iterator pointer;
typedef const_iterator const_pointer;
typedef std::reverse_iterator<iterator>
    reverse_iterator;
typedef std::reverse_iterator<const_iterator>
    const_reverse_iterator;

두둥. iterator의 타입이 포인터가 아니라.. iterator라는 클래스로 되어 있습니다. 뿐만 아니라, pointer도 iterator클래스로 되어있습니다. 내부를 파고 들어가보니 이렇습니다.


// MSVC 2008: vector:114
// 역시 줄바꿈은 제가..
_Myt& operator++()
{ // preincrement
_SCL_SECURE_VALIDATE(this->_Has_container());
_SCL_SECURE_VALIDATE_RANGE(
_Myptr < ((_Myvec *)(this->_Getmycont()))->_Mylast
);

#if _HAS_ITERATOR_DEBUGGING
if (
this->_Mycont == 0 ||
((_Myvec *)this->_Mycont)->_Mylast <= _Myptr ) _DEBUG_ERROR("vector iterator not incrementable"); #endif /* _HAS_ITERATOR_DEBUGGING */ ++_Myptr; return (*this); } [/cpp] _SCL_SECURE_VALIDATE라는 Macro를 장렬하게 호출합니다. (.. ) 이 매크로는 _SECURE_SCL 매크로에 의해 제어가 가능한데, 현재 상태가 제대로 된 상태인지 검사해주는 역할을 합니다. 잘못되면 예외를 발생시킵니다. ;; 일반적으로 vector는 T*를 반복자로 사용해도 문제가 없는 스펙을 갖고 있습니다. STLPort에서 T*를 반복자로 사용하는 이유도 그렇게 써도 되기 때문이지요. (표준안이 바뀌었을지는 모르겠습니다만..) 포인터를 반복자로 사용할 수 있다는 점은 시사하는바가 큽니다. 바로, 성능문제이지요. native타입인 T*는 컴파일러가 최적화 할 수 있는 소지도 많을 뿐더러, 인라인처리가 될까 말까 고민할 필요도 없으니까요. 대신, T*를 사용하게 되면, 이를 악용하는 프로그래머의 실수와 함께, range check와 같은 디버깅 관련 기능을 추가할 수 없다는 단점이 존재합니다.

MSVC 9.0은 단점을 위해 장점을 버린 케이스라고 할 수 있겠습니다. 뭐 성능문제는 _SECURE_SCL매크로를 끄니 5%내외로 낮아지긴 했습니다만.. 왠지 찜찜하군요. 결국 STLPort를 VC9용으로 빌드해버렸습니다. STLPort도 STLP_DEBUG모드로 사용하면, 저런 range check를 해주거든요. 🙂

사실 놀랐습니다. 저런 assertion관련 기능은 Release모드에서 별도로 켜주어야 작동하는게 맞는거 같은데 말이죠. 참고로 _SECURE_SCL을 켜두면 set에서 나는 차이는 더더욱 커집니다. 아무리 CPU가 발달해서 넘쳐나는 세상이라지만, 이건 좀 심하네요. -_-

물론, 개발툴입장에서 개발자의 편의를 추구하는 것은 당연하지만, 이런류의 편의성을 채택할거면 C#이나 Java를 쓰지 누가 C++을 쓰겠습니까. -_-; 디버깅할때나 필요할 법한 기능을 Release모드에 때려박다니.. 무슨 숨은 뜻이라도 있는걸까요. 🙁 뭐 저는 STLPort를 쓰기때문에 딱히 상관은 없습니다만. 🙂

ps1. 표준 관련 호환성은 많이 좋아지긴 한듯 합니다.
ps2. 그냥 STLPort에 투자하면 안되겠니 MS… -_-
ps3. 사고싶어요 징징.

TortoiseGIT와 Subversion. Local Commit의 사용!

TortoiseSVN을 이용한 Subversion의 사용은 매우 쾌적합니다. 멋진 UI가 가져다 주는 쾌적함과 Windows Explorer의 Context Menu확장을 통한 통합된 이용은 아주 매력적이죠.

하지만!!!!!!!!

도저히 참을 수 없는 단점이 있습니다. 바로 “Local Commit”의 부재입니다.

Local Commit은 중앙의 저장소에 Commit하는게 아니라, 프로그래머의 개인 저장소에 Commit하는 개인적인 Commit의 개념입니다. 작업량이 많을 경우에는 중간 중간 Commit하는게 필요한데 중앙의 저장소에 Commit했다간 동료들의 불평불만을 한꺼번에 받을 가능성이 있지요. 이런 이유로 Local Commit은 상당히 편리한 기능입니다.

하지만, Subversion에는 Local Commit이 없습니다. OTL. 이를 위해 Mercurial을 사용하기도 해보았지만, TortoiseMercurial의 (TortoiseSVN에 비해) 떨어지는 UI는 사용할 맛이 안날 뿐더러, Subversion과의 연동에도 문제가 많았습니다.

이리저리 찾아본 결과 GIT이라는 훌륭한 시스템이 있었습니다. GIT은 리눅스 커널등의 개발에 사용되는 소스형상관리 시스템인데, 분산된 소스형상관리 시스템의 개념을 사용하고 있으므로, Local Commit이 매우 자연스럽게 적용됩니다. Subversion과의 연동도 훌륭한 편이여서 내심 노리고 있었습니다. 하지만, 사용이 불편하다는 단점이 있었지요. 이런 와중에.. TortiseGIT이 발표되었습니다. 그렇습니다. CVS, SVN에 이어 GIT도 거북이를 등에 업은겁니다!

제가 사용한 방법은 매우 간단합니다. GIT를 일종의 SVN클라이언트로 사용하는거죠. 🙂

  1. git svn clone 명령을 이용해서 작업할 영역을 GIT repository로 복사해옵니다.
  2. 생성된 GIT내에서 작업을 합니다. blah blah TortoiseGIT가 있으니 세상이 아름답습니다.
  3. TortoiseGIT의 SVN rebase명령을 이용해 Subversion측의 변경내역을 가져옵니다.
  4. TortoiseGIT의 SVN dcommit명령을 이용해 GIT측의 변경내역을 Subversion으로 밀어 넣습니다.

스크린샷을 찍어가면서 열심히 쓰고 싶지만, 그건 나중에… git svn clone을 TortoiseGIT에서 하지 않은 이유는 TortoiseGIT이 svn clone을 수행할때 인증절차를 제대로 처리하지 못하는 문제가 있었기때문입니다. 🙁 clone할때 한번 인증 시키면 그 뒤에는 잘 되더군요.

이렇게 하니, 부담없이 작업기록을 로컬의 GIT에 남겨두고 완성한 뒤에 중앙의 SVN에 커밋하는 일이 가능해졌습니다. 🙂

하지만.. 로컬의 GIT에 담겨있던 코드들이 SVN에 넘어갈때 자잘한 커밋로그까지 다 넘어가는 문제가 있더군요. 🙁 GIT에서 branch하고 dcommit하는 부분을 따로 관리하면 이것도 정리해서 가능하지만, 커밋로그라는게 작업했던 당시의 상황을 담는 용도도 있는지라 무시하기로 했습니다. 🙂 Blame을 써보면, 자잘하게 적은 로그이지만, 나중에 유용할때가 있거든요.

나중에 필요하거나 시간이 되면 좀 더 자세히 정리해보도록 하겠습니다. 🙂

boost::array를 배워봅시다.

1. Old. Old. Old style

char buffer[1024];
...(기타 여러 코드들)...
sock.recv(buffer, 1024);

혹시 위와 같은 스타일의 코드를 작성하고 있지는 않으신가요? 물론, 잘 작동하고 문제가 없는 코드일겁니다. 하지만, 여기엔 문제가 하나 있습니다. buffer는 배열이지 객체가 아닙니다. iterator도 없으며, 멤버함수도 없습니다. 불편할 뿐더러 위험합니다. 한번 볼까요?

char buffer[1024];
...(기타 여러 코드들. 복잡해서 눈에 잘 안들어옴)...
sock.recv(buffer, 4096); // 뭔가의 필요에 의해 고쳐졌군요.

와우. 망했습니다. 스택이 깨지면서 어떤 문제가 발생할지 감도 잡기 힘듭니다. 주로 보안사고가 이런 코드에서 많이 발생합니다. buffer overflow죠. 이런 류의 “실수”를 하지 않는다고 보장할 수 있을까요? 네. 이런걸 막기 위해 사람들은 이렇게 작업하기도 합니다.

#define BUFFER_SIZE 1024
char buffer[BUFFER_SIZE];
...(기타 여러 코드들. 복잡해서 진짜 눈에 잘 안들어옴)...
sock.recv(buffer, BUFFER_SIZE);

별 문제 없어보입니다. 하지만, 버퍼의 크기가 각 위치마다 달라져야 한다면.. 그 수만큼 #define도 늘어날겁니다. enum을 쓰더라도 이건 매한가지죠.

그렇다면, 이런건 어떨까요.

std::vector<char> buffer;
buffer.resize(1024);
sock.recv(&buffer[0], buffer.size());

훌륭하죠. 이제 C++답습니다. 하지만, buffer는 여전히 stack변수이며 이 부분이 실행될때마다 매번 새로 만들어질겁니다. 그리고, 매번 새로 만들어진다는건… 매번 할당자를 호출해야한다는 겁니다. (쉽게말해 new/malloc이 매번 호출됩니다. 재앙이죠.)

이걸 막기 위해선, 그렇죠. buffer를 재사용하면 됩니다. 하지만, 재사용시에는 언제나 멀티쓰레드 환경을 고민해야하는 상황이 다가옵니다. 후.

좀 더 C++다우면서, 배열과 같은 효과를 내는건 불가능할까요?


2. olleh! array!

boost::array<char, 1024> buffer;
sock.recv(&buffer[0], buffer.size());

네. 완벽하게 C++ container스러우면서, 배열의 역할을 수행하는 자료구조입니다. boost::array이죠! 이녀석의 내부구조는 다음과 같습니다.

template<class T, int N>
class array
{
    ...(생략)...
    T elems[N];
};

유일하게 갖고 있는 멤버변수는 elems. 배열입니다! 결국 배열을 선언한 것과 동일한 효과를 갖습니다. 여기까지 왔으면, 파직 하고 뭔가 옵니다.

array는 iterator, const_iterator, size(), begin(), end(), front(), back(), empty()등의 STL컨테이너라면, 다들 갖고 있는 형식과 함수들을 갖추고 있습니다. 그러면서도 []연산자를 지원하여 기존의 배열과 흡사하게 사용할 수 있지요. 이것이 시사하는 바는 생각보다 큽니다.

기존의 배열에 알고리즘을 적용해봅시다.

char buffer[1024];
...(생략)...
char* found = std::find(buffer, buffer+1024, 'c');

위와 같은 알고리즘을 적용할 경우, 1024라는 크기상수가 지속적으로 따라다닙니다. 또한, buffer를 vector나 다른 자료구조로 바꾸기라도 하면, 꽤 많은 수정을 가해야하지요. 하지만, boost::array를 사용한다면 다음과 같이 바뀝니다.

// 편의를 위한 typedef. C++ 프로그래머의 친구죠. :)
typedef boost::array<char, 1024> buffer_type;
buffer_type buffer;
buffer_type::iterator found =
    std::find(buffer.begin(), buffer.end(), 'c');

3. 정리

사실, boost::array(TR1에 들어갔으니 이젠 tr1::array려나요.)는 굉장히 간단하고, 의미없게 느껴질 수 도 있습니다. 하지만, 과거의 배열은 원소들을 담는 컨테이너의 역할을 함에도 불구하고 STL스럽지 않다는 단점이 있었습니다. 자료구조를 다루는데 있어서, STL 컨테이너들과 굉장히 동떨어진 사용법은 코드의 일관성을 해치게 됩니다.

이런 면에서 볼 때, 배열과 동일한 구조/성능을 가지고 있으면서 STL 컨테이너와 유사한 인터페이스를 갖춘 array는 C++내에서 C의 유산에 의해 코드가 망가지는 것을 피할 수 있도록 해주는 상당히 중요한 도구입니다. 🙂 그리고, 200줄이 안되는 이 코드는 좋은 참고자료이기도 합니다. 시간 나시면 꼭 한번 읽어보시길. 🙂


A. References
boost::array의 문서