문자열(std::string)을 키로 가지는 맵 같은 경우, 문자열 비교 자체에 걸리는 시간 때문에 검색이 느려질 수 있다. 이 경우, 키로 사용하는 문자열이 별로 중요한 내용이 아니라면 아래와 같은 클래스를 사용함으로서 성능을 약간 증가시킬 수 있다.

/// \class cStringKey
/// \brief STL 컨테이너를 위한 문자열 키
class cStringKey
{
private:
  enum {
       BYTE_SIZE = 32,
  };

  char m_Text[BYTE_SIZE]; ///< 문자열


public:
  /// \brief 생성자
  cStringKey() {
       memset(m_Text, 0, sizeof(m_Text));
  }

  /// \brief 생성자
  cStringKey(const char* text) {
       memset(m_Text, 0, sizeof(m_Text));
       memcpy_s(m_Text, sizeof(m_Text), text, std::min(sizeof(m_Text), strlen(text)));
  }

  /// \brief 생성자
  cStringKey(const std::string& text) {
       memset(m_Text, 0, sizeof(m_Text));
       memcpy_s(m_Text, sizeof(m_Text), text.c_str(), std::min(sizeof(m_Text), text.size()));
  }

  /// \brief 복사 생성자
  cStringKey(const cStringKey& rhs) {
       memcpy_s(m_Text, sizeof(m_Text), rhs.m_Text, sizeof(m_Text));
  }


public:
  /// \brief 대입 연산자
  inline const cStringKey& operator = (const cStringKey& rhs) {
       if (this != &rhs)
           memcpy_s(m_Text, sizeof(m_Text), rhs.m_Text, sizeof(m_Text));
      return *this;
  }

  /// \brief 비교 연산자
  ///
  /// 이 함수는 약간 유의해야 하는데, 속도를 위해 루프를 풀어버렸기 때문이다.
  /// 클래스의 크기가 변경되면, 이 함수도 같이 변경해줘야 한다.
  inline bool operator < (const cStringKey& rhs) const {
       const int* buf1 = reinterpret_cast<const int*>(this);
       const int* buf2 = reinterpret_cast<const int*>(&rhs);

       if (*buf1 != *buf2) return *buf1 < *buf2; // 0-3

       ++buf1; ++buf2;
       if (*buf1 != *buf2) return *buf1 < *buf2; // 4-7

       ++buf1; ++buf2;
       if (*buf1 != *buf2) return *buf1 < *buf2; // 8-11

       ++buf1; ++buf2;
       if (*buf1 != *buf2) return *buf1 < *buf2; // 12-15

       ++buf1; ++buf2;
       if (*buf1 != *buf2) return *buf1 < *buf2; // 16-19

       ++buf1; ++buf2;
       if (*buf1 != *buf2) return *buf1 < *buf2; // 20-23

       ++buf1; ++buf2;
       if (*buf1 != *buf2) return *buf1 < *buf2; // 24-27

       ++buf1; ++buf2;
       return *buf1 < *buf2; // 28-31
  }
};

대소문자 구별없이 비교를 할 수 없다는 점이 좀 아쉽다. 어셈블리도 좀 안다면 비교 연산자를 좀 더 깔끔하게 만들 수 있을 텐데. 어쨌든 테스트해보니, 릴리즈 빌드에서 약 25~33% 정도의 성능이 향상되었다.

typedef std::map<std::string, std::string> OLD_MAP;
typedef std::map<cStringKey, std::string> NEW_MAP;

OLD_MAP oldMap;
NEW_MAP newMap;

for (int i=0; i<1000; ++i) {
  std::string key = generic::to_string(rand() % 1000, 4);
  std::string value = generic::to_string(rand() % 1000, 4);
  oldMap.insert(OLD_MAP::value_type(key, value));
  newMap.insert(NEW_MAP::value_type(key, value));
}

DWORD begin = 0, oldTime = 0, newTime = 0;
int repetition = 200000;

begin = timeGetTime();
for (int i=0; i<repetition; ++i)
  oldMap.find(generic::to_string(rand() % 1000, 4));
oldTime = timeGetTime() - begin;

begin = timeGetTime();
for (int i=0; i<repetition; ++i)
  newMap.find(generic::to_string(rand() % 1000, 4));
newTime = timeGetTime() - begin;

std::cout << "OLD: " << oldTime << std::endl;
std::cout << "NEW: " << newTime << std::endl;

OLD: 241
NEW: 146

이런 자잘한 최적화 해봐야 얼마나 달라지겠냐만은 기왕이면 다홍치마 아닌가.
2009/09/03 09:07 2009/09/03 09:07
받은 트랙백이 없고, 댓글이 없습니다.

댓글+트랙백 RSS :: http://serious-code.net/tc/rss/response/11

댓글+트랙백 ATOM :: http://serious-code.net/tc/atom/response/11

"고유 ID 생성"이라는 문제는 서버 프로그래밍을 하다보면 빈번히 마주치는 문제다.

전형적인 문제 중에 하나로서 게임 서버에서 플레이어가 사용할 아이템을 생성하는 경우가 있다. 아이템에 대한 정보를 데이터베이스에 집어넣을 때는 고유 ID가 필요하다. 물론 고유 ID 없이 집어넣을 수도 있겠지만, 운영 상의 문제 등을 위해 집어넣는 것이 좋다.

고유 ID를 사용해야 하는 객체, 즉 게임 서버가 하나라면 별 문제가 없다. 그냥 프로그램 내부에서 변수 하나 선언하고, 필요할 때마다 1씩 증가시켜가며 사용하면 되니까. 하지만 게임 서버가 여러 대라면 좀 귀찮아진다.

이런 경우, 일반적으로 가장 먼저 나오는 방법이 데이터베이스 상에서 제공하는 auto increment 기능을 이용하는 방법이다. SQL Server 같은 경우에는 @@IDENTITY 값을 이용하면, 하나의 행을 추가할 때마다 고유한 ID를 얻을 수 있다.

@@IDENTITY 값은 여러 가지 제약 사항이 있기 때문에, 데이터베이스를 이용하지 않고, ID 발급을 위한 중앙 서버를 따로 둘 수도 있다. 이에 관한 건 GPG 6권 7.3 장에 잘 소개되어 있다.

하지만 데이터베이스 또는 ID 발급 서버 같이 중앙 저장소가 따로 존재하는 경우, 데이터 생성 시점에서 고유 ID를 바로 알 수 없다는 문제점이 있다. 데이터베이스는 INSERT 후에야 고유 ID 값을 알 수 있고, 중앙 서버를 두는 경우에는 패킷이 한번 왔다갔다해야 고유 ID 값을 알 수 있다는 말이다. 이 처리가 은근히 귀찮다. 로직이 비동기 방식이 되기 때문이다.

이 문제를 해결하는 의외로 간단하다. 각각의 게임 서버에서 고유 ID 값을 1씩 증가시키는 것이 아니라 게임 서버의 갯수만큼씩 증가시키면 된다.

0번 게임 서버 : 0 --> 4 --> 8  --> ...
1번 게임 서버 : 1 --> 5 --> 9  --> ...
2번 게임 서버 : 2 --> 6 --> 10 --> ...
3번 게임 서버 : 3 --> 7 --> 11 --> ...

위에서는 0을 베이스로 삼았지만, 처음에 게임 서버가 뜰 때 데이터베이스 상에 존재하는 아이템 중에 최대값의 고유 ID를 읽어와 베이스로 삼아야한다. 즉 데이터베이스 상에 고유 ID 최대값이 379라면...

0번 게임 서버 : 379 + 0 = 379 --> 383 --> 387 --> ...
1번 게임 서버 : 379 + 1 = 380 --> 384 --> 388 --> ...
2번 게임 서버 : 379 + 2 = 381 --> 385 --> 389 --> ...
3번 게임 서버 : 379 + 3 = 382 --> 386 --> 390 --> ...

실제로 적용할 때에는 추가적으로 게임 서버가 뜨는 시간차를 고려해서 베이스 값을 보정해줘야 한다. 어쨌든 이렇게 처리하면 아이템 생성 시점에서 이 아이템에 부여할 고유 ID를 알 수 있다. 다만 모든 게임 서버의 아이템 생성 속도가 같지 않기 때문에 중간중간에 갭이 생긴다는 단점이 있다. 이는 뭐 서버 정기 점검 시간에 압축을 하거나, 64비트 값을 써버리면 된다.

알고 보면 정말 별 거 아닌 트릭이지만, 의외로 모르는 이도 있는 듯 하여 정리해 봤다.
2009/09/03 09:05 2009/09/03 09:05
받은 트랙백이 없고, 댓글이 없습니다.

댓글+트랙백 RSS :: http://serious-code.net/tc/rss/response/7

댓글+트랙백 ATOM :: http://serious-code.net/tc/atom/response/7

C++ 소멸자 이야기

개발 2009/09/03 09:04 김성민
C++ virtual 함수의 동작 방식에 대해 모르는 사람은 드물 거라고 생각한다.
class base
{
public:
  void nvf();               // 1 <----+
  virtual void vf();        // 2      |
  ...                       //        |
};                          //        |
                            //        |
class derived : public base //        |
{                           //        |
public:                     //        |
  void nvf();               // 3      |
  virtual void vf();        // 4 <----|--+
  ...                       //        |  |
};                          //        |  |
                            //        |  |
base* ptr = new derived;    //        |  |
ptr->nvf();                 // 5 -----+  |
ptr->vf();                  // 6 --------+
delete ptr;                 // 7

5번의 경우에는 1번 함수가 호출될 것이고, 6번의 경우에는 4번 함수가 호출될 것이다.

소멸자도 이와 다를 게 없다. 7번에서 소멸자가 virtual 이라면 derived::~derived()가 호출되고, virtual이 아니라면 base::~base()가 호출된다. (자식 클래스에서 소멸자 호출시, 부모 클래스의 소멸자가 연달아 호출된다는 점은 넘어가자.)

즉 위와 같이 derived 클래스를 base 포인터에다 할당하고 삭제하는 경우에는 소멸자를 virtual로 해줘야 한다. 그렇지 않으면 ~derived() 함수가 호출되지 않아, 이 안에 있는 리소스(?) 정리 코드가 호출되지 않기 때문이다. 즉 메모리 릭이 발생할 수 있다. 하지만 아래와 같은 경우에는 굳이 소멸자를 virtual로 만들 필요가 없다.

derived* ptr = new derived();
ptr->nvf();
ptr->vf();
delete ptr;

포인터가 derived* 타입이므로, 소멸자가 virtual이든 아니든 derived::~derived() 함수가 호출되기 때문이다.

즉 소멸자를 virtual로 선언해야 하는지를 판단하기 위해서는, 해당 클래스를 상속받는 클래스가 있느냐/없느냐를 기준으로 삼을 것이 아니라, 실제로 사용할 때 베이스 클래스 포인터를 이용해 접근하는가/아닌가를 기준으로 삼아야 한다.

다만 안타깝게도, 누군가가 작성한 클래스를 다른 사람이 사용한다고 했을 때, 그 사람이 base 포인터를 이용해 작업을 할지, derived 포인터를 이용해서 작업을 할지 최초 작성자는 알 수 없다. 결국  안전빵(?)으로 가기 위해 소멸자는 무조건 virtual로 선언하는 경우가 많다.

일반적인 PC 환경에서 virtual 소멸자 마구 쓴다고 해서 딱히 크게 문제될 것은 없다. 인스턴스당 4바이트 정도의 메모리가 더 들어가고, vtable 또한 어딘가에 생성되겠지만, 시대가 어떤 시댄데...

다만 C 구조체를 상속받아서 그 구조체를 인자로 받는 함수로 전달하거나, 객체를 통채로 직렬화하려는 경우에는 주의해야 한다.

2009/09/03 09:04 2009/09/03 09:04
받은 트랙백이 없고, 댓글 하나가 달렸습니다.

댓글+트랙백 RSS :: http://serious-code.net/tc/rss/response/6

댓글+트랙백 ATOM :: http://serious-code.net/tc/atom/response/6


프로그래머들은 프로그램을 만들 때 다음과 같은 사항들을 생각한다.

  1. 좋은 기능을 지나치고 있지 않은가?
  2. 이 코드가 어떻게 수행되는가?
  3. 얼마나 읽기 쉬운 코드인가?
  4. 기타 등등

하지만 뛰어난 프로그래머들을 포함해 대부분의 프로그래머들은 코드를 작성할 때 디버깅의 용이성을 생각하지는 않는다. 이를 위한 몇 가지 팁이 아래에 있다.

  1. 역할을 설명하는 변수(explaining variable)들을 많이 사용하라. (http://c2.com/cgi/wiki?IntroduceExplainingVariable) 디버거 안에서 그 변수의 값을 쉽게 값을 치환할 수 있다. 다음 코드를 실행하기 전에 리턴값을 쉽게 체크할 수 있다.
  2. 덩치가 큰 여러 함수를 한 구문 안에서 호출하지 말라. 예를 들어 다음과 같은 형식이다. Func1(Func2(Func3())); 디버거 상에서 함수를 골라서 들어가려면 상당히 골치아프다. 리턴값을 체크하기도 어렵고, 리턴값을 변경하기도 어렵다.
  3. IfFailGo(Func()) 류의 구문을 사용하지 말라. IfFailGo는 매크로로서 다음과 같은 내용이다.
    #ifndef IfFailGoto
    #define IfFailGoto(EXPR, LABEL) \
    do { hr = (EXPR); if(FAILED(hr)) { goto LABEL; } } while (0);
    #endif

    #ifndef IfFailGo
    #define IfFailGo(EXPR) IfFailGoto(EXPR, ErrExit)
    #endif

    이 또한 리턴값을 체크하기 어렵다. 대신 아래와 같은 형식을 사용하라.

    hr = Func();
    IfFailGo(hr);
  4. 매크로보다는 인라인 함수를 사용하라. C++ 컴파일러는 매크로의 내용을 PDB에다 집어넣지 않는다. 그러므로 인라인 함수를 사용하면, 디버거에서 단계별로 실행하는 것이 가능하다.
  5. #define이나 GUID보다는 enum 값을 사용하라. enum 값의 경우에는 디버거가 사람이 읽을 수 있는 포맷으로 변환해주는 것이 가능하다. 하지만 #define을 사용한 경우에는 이 변환이 절대 안 되고, GUID는 가끔 될 뿐이다. 분명히 #define을 사용할 수 밖에 없는 때가 있기는 하다. 그러나 둘 다 가능한 경우라면 반드시 enum을 사용하라.
  6. void* 형식의 데이터를 받은 다음 캐스트해서 사용하지 말고, 애초에 원래 데이터 타입을 넘겨라. 원래 형식으로 사용하면, 디버거에서 쉽게 표시할 수 있기 때문이다.
2009/09/03 08:51 2009/09/03 08:51
받은 트랙백이 없고, 댓글이 없습니다.

댓글+트랙백 RSS :: http://serious-code.net/tc/rss/response/4

댓글+트랙백 ATOM :: http://serious-code.net/tc/atom/response/4

lua 5.0 -> lua 5.1 체험기 -_-;

개발 2009/09/03 08:50 김성민

뭐 모듈 시스템이 변경됬느니, 가비지 컬렉션이 어떻게 됬느니하는 이야기는 접어두고, 일단 기존의 코드를 변환하면서 문제가 된 것들이다.

1. LUA_PATH 문제

require 문을 위한 검색 경로를 설정할 때, 이전에는 전역 변수인 LUA_PATH 문자열 값을 이용했으나, 이제 package 테이블 안의 path 변수를 이용해야한다. 자세한 건 http://serious-code.net/moin.cgi/LuaSnippets 페이지 참고

2. _TRACEBACK 함수 문제

lua_pcall 함수를 호출할 때, 전역 테이블에서 "_TRACEBACK" 함수를 가져와서 사용했었는데, 없어져버렸다. debug.traceback의 alias였는지 아닌지는 잘 모르겠다만...

int errfunc = lua_gettop(L);
lua_getglobal(L, "_TRACEBACK");
lua_insert(L, errfunc);

int status = lua_pcall(L, 0, 0, errfunc);
if (status != 0)
{
  cerr << "ERROR: " << lua_tostring(L, -1) << endl;
  lua_pop(L, 1);
}

lua_remove(L, errfunc);

이걸 해결하기 위해서...

int traceback(lua_State* L)
{
  lua_getfield(L, LUA_GLOBALSINDEX, "debug");
  if (!lua_istable(L, -1)) {
       lua_pop(L, 1);
       return 1;
  }

  lua_getfield(L, -1, "traceback");
  if (!lua_isfunction(L, -1)) {
       lua_pop(L, 2);
       return 1;
  }

lua_pushvalue(L, 1); // pass error message
  lua_pushinteger(L, 2); // skip this function and traceback
  lua_call(L, 2, 1);     // call debug.traceback
  return 1;
}

int base = lua_gettop(L);
lua_pushcfunction(L, traceback);
lua_insert(L, base);

int status = lua_pcall(L, 0, 0, base);
lua_remove(L, base);

if (status != 0)
{
  cerr << "ERROR: " << lua_tostring(L, -1) << endl;
  lua_pop(L, 1);
}


3. 기본 라이브러리 열기 함수 변경

루아 라이브러리를 여는 함수들이 변경되었다. 원래는  lua_baselibopen 같은 이름이였는데, luaopen_base 와 같은 이름으로 변경되었다. 그런데 문제는 이 라이브러리들을 하나씩 로드하니 AV가 일어난다. 함수 호출 순서가 상관있나? 혹시나 싶어서  모든 라이브러리를 열어주는 함수 luaL_openlibs 를 이용하니 잘 된다. 뭘까...

luaL_openlibs(L);
/*
luaopen_base(L);
luaopen_table(L);
luaopen_os(L);
luaopen_io(L);
luaopen_string(L);
luaopen_math(L);
luaopen_debug(L);
luaopen_package(L);
*/


개인적으로 루아의 아주 작은 부분 집합만을 사용하기에, 뭐하러 버전업한 건지 이유를 모르게 되어버렸다.

2009/09/03 08:50 2009/09/03 08:50
받은 트랙백이 없고, 댓글 하나가 달렸습니다.

댓글+트랙백 RSS :: http://serious-code.net/tc/rss/response/3

댓글+트랙백 ATOM :: http://serious-code.net/tc/atom/response/3

TEXT/BLOB 이 들어있는 테이블을 긁어올 때는 TEXT/BLOB 컬럼을 SELECT 리스트의 맨 뒤에다 둬야 한다.

SELECT ID, BLOB_COLUMN, INT_COLUMN FROM SomeTable --> X
SELECT ID, INT_COLUMN, BLOB_COLUMN FROM SomeTable --> O

http://msdn.microsoft.com/library/default.asp?url=/library/en-us/odbc/htm/odbcgetting_long_data.asp

There are a number of restrictions on using SQLGetData. In general, columns accessed with SQLGetData:
  • Must be accessed in order of increasing column number (because of the way the columns of a result set are read from the data source). For example, it is an error to call SQLGetData for column 5 and then call it for column 4.
  • Cannot be bound.
  • Must have a higher column number than the last bound column. For example, if the last bound column is column 3, it is an error to call SQLGetData for column 2. For this reason, applications should be careful to place long data columns at the end of the select list.
  • Cannot be used if SQLFetch or SQLFetchScroll was called to retrieve more than one row.

시팍, 프로그램 잘못 짠 줄 알고 한참 찾았다.

2009/09/03 08:49 2009/09/03 08:49
받은 트랙백이 없고, 댓글이 없습니다.

댓글+트랙백 RSS :: http://serious-code.net/tc/rss/response/2

댓글+트랙백 ATOM :: http://serious-code.net/tc/atom/response/2