티스토리 툴바


  

 

지난 몇 주간 유지보수중인 프로그램의 성능 문제 때문에 Visual Studio 2005/2008 의 Profiler 덕을 많이 봤습니다.

Visual Studio 2005 Profiler 로 성능에 문제가 있는 함수를 하나씩 파고 들면서 거기서 가장 많은 시간을 소비하거나, 쓸데 없이 많이 호출되는 함수들을 개선해 나가는 식으로 진행했는데요.

 

작업을 하다 보니 STL map 을 이용해서 insert 하고, find 하는 코드들이 시간 소비가 많다는 것을 확인했습니다.

 

워낙 많이 호출되다 보니 그렇겠거니 하고 일단 쉽게 수정 가능한 다른 부분들을 먼저 수정해서 큰 고비는 넘겼습니다.

 

그래도 마음 속으로는 계속 map 에 대한 성능부분이 마음에 걸려 Visual Studio 에서 제공하는 다른 map 들과 간단하게 성능을 비교해 봤습니다.

성능 비교에 쓰인 3가지 클래스입니다.

 

std:map  - C++ 표준에서 제공하는 일반적인 map, 바이너리 트리 검색

CAtlMap – ATL 에서 제공하는 map, 해시 검색

CRBMap – ATL 에서 제공하는 map, 바이너리 트리 검색

 

구조적으로 STL map ATL CRBMap 는 비슷한 방식으로 가지고 있어 성능도 비슷할 것으로 보였지만, 실제 결과는 상당한 성능 차이가 있었습니다.
 

일단 대상으로 삼은 <Key, Value> 조합은 <LPCTSTR, TEST_DATA> 입니다.

문자열을 키로 삼아 원하는 구조체를 찾는 일반적인 방식을 사용했습니다.

참고로 TEST_DATA 구조체는

struct TEST_DATA

{

           long lData;

           wstring sData;

};

와 같이 임의로 정의했습니다.

 

테스트를 위해 사용할 오늘의 컨테이너들을 소개합니다.

 

map<LPCTSTR, TEST_DATA> m_mapData;

CAtlMap <LPCTSTR, TEST_DATA> m_atlmapData;

CRBMap <LPCTSTR, TEST_DATA> m_RBmapData;


Insert할 데이터의 공정성(?)을 위해 키가 되는 문자열은 일방향적인 삽입이 일어나지 않도록 하기 위해서 rand() 함수를 통해 문자열로 변환했습니다. 아래는 map 에 insert 를 위해서 vector 에 랜덤한 문자열을 만들어 저장하는 코드입니다. vector<> 에 저장된 데이터들은 순서대로 map 에 추가하는 방식을 사용했습니다.

 

for (int i = 0; i < MAX_ELEMENT; i++)

{

             data.lData = rand();

             _stprintf_s  (szFormat, _countof(szFormat), _T("%010d%i\n"), data.lData,i);

             data.sData = szFormat;   

            

             m_vtKey.push_back( make_pair(data.sData,data) );

}

 

1.     Insert 비교

10,000   ~ 100,000 개의 TEST_DATA Insert 하는 시험을 했습니다. 3가지 모두 동일한 조건으로 추가했고 추가를 위해 사용한 코드는 아래와 같습니다.

 

// STL MAP INSERT

DWORD dwStart = timeGetTime();

for (itV = m_vtKey.begin(); itV != m_vtKey.end(); ++itV)

{

           m_mapData.insert(make_pair(itV->first.c_str(), itV->second) );

}

TRACE("%d,", timeGetTime() - dwStart);

 

// ATL MAP INSERT

dwStart = timeGetTime();

for (itV = m_vtKey.begin(); itV != m_vtKey.end(); ++itV)

{

           m_atlmapData.SetAt(itV->first.c_str(), itV->second);

}

TRACE("%d,", timeGetTime() - dwStart);

 

// RBMAP INSERT

dwStart = timeGetTime();

for (itV = m_vtKey.begin(); itV != m_vtKey.end(); ++itV)

{

           m_RBmapData.SetAt(itV->first.c_str(), itV->second);

}

TRACE("%d,", timeGetTime() - dwStart);


 

위와 같은 코드로 데이터를 10,000 ~ 100,0000 개까지 map Insert 하는 동안 걸린 시간은 아래 차트와 같았습니다.

 

 

 

X 축은 Insert 횟수를, Y 축은 X 축의 횟수 만큼 Insert를 했을 때 걸린 시간을 의미합니다.

그림을 대충 봐도 알 수 있듯이 STL map CAtlMap Insert 시간은 상당한 차이를 보입니다. 게다가 비슷한 방식의 CRBMap 과도 상당한 차이가 나는 것을 볼 수 있습니다. 이러한 차이는 데이터가 늘어날 수록 극명해 짐을 알 수 있었습니다.

 

2.     Find 비교

Insert 와 같은 방식으로 이번에는 각각의 map에 들어 있는 모든 항목을 하나씩 찾는데 걸린 시간을 비교했습니다.

비교에 사용된 코드입니다.

 

// STL MAP FIND

dwStart = timeGetTime();

// find 성능분석

for (itV = m_vtKey.begin(); itV != m_vtKey.end(); ++itV)

{

           it = m_mapData.find(itV->first.c_str());

           if (it == m_mapData.end())

                     TRACE(_T("%s\n"), itV->first.c_str());

}

TRACE("%d,", timeGetTime() - dwStart);

 

// ATL MAP FIND

dwStart = timeGetTime();

for (itV = m_vtKey.begin(); itV != m_vtKey.end(); ++itV)

{

           pPair1 = m_atlmapData.Lookup(itV->first.c_str());

           if (pPair1 == NULL)

                     TRACE(_T("%s\n"), itV->first.c_str());

}

TRACE("%d,", timeGetTime() - dwStart);

 

// RBMAP FIND

dwStart = timeGetTime();

for (itV = m_vtKey.begin(); itV != m_vtKey.end(); ++itV)

{

           pPair2 = m_RBmapData.Lookup(itV->first.c_str());

           if (pPair2 == NULL)

                     TRACE(_T("%s\n"), itV->first.c_str());

}

TRACE("%d,", timeGetTime() - dwStart);


 


비교 차트입니다. 바이너리 검색보다 역시 해시 검색에서 확실히 더 나은 성능을 보여주는 것을 알 수 있습니다.  그리고 조금 놀란 부분은 같은 바이너리 검색으로 알고 있는 STL map CRBMap 이 상당한 성능 차이를 보인다는 점이었습니다.  사실 1회성인 insert 에서 보다, 그 빈도 수가 훨씬 많은 편인 find 함수에서 이 정도의 격차가 난다는것만으로도 STL::map 구현에서 성능적인 문제들이 많이 간과되었다고 밖에 생각할 수가 없었습니다. (물론 Visual Studio 2008 의 구현에 국한 된 테스트라 다른 컴파일러에 대한 얘기로 확대 해석 할 수는 없습니다)

 

3.     Find & Erase 비교

일반적으로 Find 이후에 Erase 를 하는 경우가 많을 거라 생각되어서 Find Erase 를 함께 사용하여 수행 시간을 측정했습니다.

// STL MAP ERASE

dwStart = timeGetTime();

for (itV = m_vtKey.begin(); itV != m_vtKey.end(); ++itV)

{

           it = m_mapData.find(itV->first.c_str());

           if (it != m_mapData.end())

                     m_mapData.erase(it);

}

TRACE("%d,", timeGetTime() - dwStart);

 

// ATL MAP ERASE

dwStart = timeGetTime();

for (itV = m_vtKey.begin(); itV != m_vtKey.end(); ++itV)

{

           m_atlmapData.RemoveKey(itV->first.c_str());

}

TRACE("%d,", timeGetTime() - dwStart);

 

// RBMAP ERASE

dwStart = timeGetTime();

for (itV = m_vtKey.begin(); itV != m_vtKey.end(); ++itV)

{

           m_RBmapData.RemoveKey(itV->first.c_str());

}

TRACE("%d", timeGetTime() - dwStart);



 

 

Erase 부분도 역시 CAtlMap 의 압승입니다. CRBMap STL map 은 어느 정도 비슷한 시간을 보였습니다.

 

혹시나 키가 문자열이 아닌 숫자라면 어떨까요.
이번에는 키를 숫자로 설정해서 <long,
TEST_DATA> 로 컨테이너를 만들어 비교해 봤습니다.

사용된 컨테이너들입니다.

 

map<long, TEST_DATA> m_mapData2;

CAtlMap <long, TEST_DATA> m_atlmapData2;

CRBMap <long, TEST_DATA> m_RBmapData2;

 

코드는 거의 중복이기 때문에 생략하겠습니다.

1.     Insert

 

2.     Find

 

3.     Find & Erase

 

 

위 두 가지 예는 데이터가 너무 많은 감이 있어 데이터를 1,000 ~ 10,000 개 정도로 줄여서 비교해 봤습니다. <LPCTSTR, TEST_DATA> 를 사용했습니다.

 

1.     insert


2.     Find


 

3.     Find & Erase



 

데이터가 적은 경우에도 STL map Insert Find 에 가장 많은 시간이 소요됐고, Erase 부분에서는 오히려 RBMap 이 가장 많은 시간이 걸린 것으로 조사됐습니다.

 

그리 어렵지 않은 테스트로 얻은 교훈은

 

- Visual Studio STL map 구현이 뭔가 최적화 되지 않은 느낌입니다.

- 일반적인 경우에는 상관없겠으나 성능이 중요한 이슈인 프로그램에서는 CAtlMap 이나 다른 대안이 될만한 Map 을 찾거나, 가장 최적화된 Map 을 직접 만들어야 합니다.

 

그 동안 좀 무분별하게 STL MAP 을 선호하는 편이었는데 최근의 프로파일링 결과도 그렇고, 테스트 결과도 그렇고 좀 더 성능에 신경 써야겠다는 생각이 들었습니다.


조금 다른 얘기이긴 하지만 STL 자료구조의 성능 관련 아래 글도 읽어 볼만 합니다.

STL 자료구조 성능테스트: 포인터? 객체?



  1. 오스카 2008/10/29 11:55

    visual studio 2008(2005도 해당될 듯)의 stl 쪽엔 iterator 관련한 디버깅 코드가 있습니다. 이게 속도를 몇 배로 떨어지게 하는데요, 요걸 끄고 한 번 돌려보시면 어떨런지...

    프로젝트 생성 시, debug/release 모두 디폴트로 해당 옵션이 켜져 있습니다. 끄실려면 #define _HAS_ITERATOR_DEBUGGING 0 을 추가하시면 됩니다. 그리고 release 테스트 하셔야 할 듯 하네요.

    제 pc 에서는

    MAX_ELEMENT : 100000
    #TEST1
    TestStlMap : 194
    TestAtlMap : 62
    TestRBMap : 80
    #TEST2
    TestStlMap : 191
    TestAtlMap : 68
    TestRBMap : 85
    #TEST3
    TestStlMap : 190
    TestAtlMap : 67
    TestRBMap : 85
    #TEST4
    TestStlMap : 195
    TestAtlMap : 67
    TestRBMap : 88
    #TEST5
    TestStlMap : 188
    TestAtlMap : 71
    TestRBMap : 88

    결과적으로는 AtlMap이 가장 빠른가요? ㅎㅎ

    • Favicon of http://eslife.tistory.com BlogIcon esstory 2008/10/29 13:03

      오스카님 제주도에서 복귀하신건가요?.
      _HAS_ITERATOR_DEBUGGING 는 처음 알았습니다. 예전에 ATL 쪽은 이런 define 들이 꽤 많았던걸로 기억하는데 요것도 확인해 볼게요 ^^

    • object 2008/10/29 13:22

      AtlMap이 가장 빠를 수 밖에 없습니다. 아시다시피 해쉬테이블이라 O(1)이고 나머지 두 RBMap과 StlMap은 O(logn)이니 성능은 당연히 차이가 나겠죠. RBMap, STLmap은 키가 소팅이 되어있으니 소팅이 필요한 경우에만 사용해야합니다. 해쉬는 그렇지가 않죠. 그래서 굳이 AtlMap과 성능을 비교하는 건 목적이 다르기 때문에 정확한 비교는 아닙니다.

      비교를 하려면 AtlMap과 STL extension에 있는 hash_map을 해야겠죠. 문제는 문제는 MSVC의 STL 구현이 너무 멍청한건지.. 너무 느리다는거죠. hash_map은 황당할 정도로 느렸습니다.

    • Favicon of http://eslife.tistory.com BlogIcon esstory 2008/10/29 13:43

      object 님
      말씀대로 구현 방식이 너무 틀려서 비교 자체는 문제일 지 모르지만, 성능이 어떤가 확인차 만든 자료이니 이해를 ^^;
      그리고 CRBMap 과 stl map 은 구현방식이 거의 같아야 하는데 insert/delete 에서도 상당한 차이가 나고 find 마자도 stl map 의 완패입니다. 어디서 싼 라이브러리를 사온건 아닌지.

    • Favicon of http://eslife.tistory.com BlogIcon esstory 2008/10/29 14:07

      오스카님 '_HAS_ITERATOR_DEBUGGING' 는 디버그 빌드에서만 해당하는거네요.
      위 테스트는 디버그에서 대충 테스트 한거긴 한데 나머지 컨테이너도 같은 디버그 조건이라 생각하고 진행했더랬습니다 ㅎㅎ.
      릴리즈에서도 한번 해 봐야겠네요. 하나 마나 결론은 같을 거 같습니다.

  2. Favicon of http://www.jiniya.net BlogIcon codewiz 2008/10/29 12:02

    와 성능 차이가 엄청나군요... ㅎㅎ^^
    오스카님이 지적하신 부분을 감안하더라도 꽤나 있네요. ㅋ~
    저도 이제껏 무분별하게 썼던것 같은데 앞으로 주의해야 겠습니당...

    • Favicon of http://eslife.tistory.com BlogIcon esstory 2008/10/29 13:04

      저도 STL 전도사처럼 마구 기존 MFC 맵을 stl map 으로 바꾸고 있던 참인데 테스트 결과를 보고 많이 놀랬습니다.
      저 땜에 전체 시스템을 느리게 한건 아닌가 싶기도 하구요

  3. Favicon of http://www.nekothink.com BlogIcon 외계고양이 2008/12/02 00:55

    재미있는 실험이네요. :)
    프로젝트 마감이 다가오니까 성능문제에 관심을 가지게 되는데 많은 지식이 필요하다는걸 절실히 느끼네요.

    • Favicon of http://eslife.tistory.com BlogIcon esstory 2008/12/02 08:48

      우리가 알게 모르게 도움을 받고 편하게 쓰는 라이브러리들이 그 목적에 맞게 제대로 안 쓰이면 독이 되는거 같습니다. 프로파일링 꼭 해 보세요 ^^

  4. 김종욱 2008/12/13 15:12

    우옷.....엄청난 속도차이네요....

    STL이 짱인줄 알았더니만....^^

    ATL에도 관심을 가져야 겠네요.....

    • Favicon of http://eslife.tistory.com BlogIcon esstory 2008/12/14 12:47

      위에 object 님이 쓰신 내용처럼 해쉬 검색을 하는 map 과 바이너리 트리 검색을 하는 map 의 근본적인 차이도 있을거예요. 그래고 같은 바이너리 검색을 하는 컨테이너에서도 속도차이가 제법 있으니, 따져보고 써야 할 거 같습니다. 방문 감사합니다.^^

  5. Favicon of http://neodreamer.tistory.com BlogIcon NeoDreamer 2009/04/02 20:10

    VC에 기본으로 포함되어 있는 STL 말고 STLPort 를 이용하면 결과가 달라질까요?

  6. Favicon of http://ikpil.com BlogIcon 최익필 2009/05/04 17:50

    릴리즈에서 테스트 해보셨나요? 어떤가요?

  7. Favicon of http://ummae.zz.io BlogIcon ummae 2009/08/21 10:11

    음.. STL의 Map은 래드블랙 트리를 기본으로 구성되어 있는걸로 압니다. ATL의 Map이 해시 기반이라면 당연히 STL의 Map이 느릴 수 밖에 없습니다. 즉, 비교자체가 무의미 한 것으로 보입니다. ATL의 Map과 비교하려면 TR1의 UnorderedMap과 비교를 하는 것이 맞는것 같습니다. UnorderedMap은 ATL의 Map과 동일하게 해시 기반으로 구현되어있습니다.

    • Favicon of http://eslife.tistory.com BlogIcon esstory 2009/08/21 13:26

      네 unmae 님 말씀이 정확하시구요.
      일반적으로는 검색을 위해 당연하게 map을 남용하고 있어서 속도에 대해 정확히 알아 보고자 적은 글입니다.
      그리고 VC 에 구현된 unordered map 도 성능이 많이 안좋아 구박을 많이 받고 있는걸로 압니다.
      댓글 감사합니다

  8. 까마귀 2011/01/27 16:32

    CATLMap 의 경우 해쉬다 보니 키에 문자열이 들어가지 않네요. -.-
    테스트 코드를 보면 그냥 포인터 비교로 끝나버리게 되니 일반적으로 만들경우 정상적으로 find가 되지 않는군요.

    속도가 느려서 변경해봤다가. 조금 탈력입니다.

    • Favicon of http://eslife.tistory.com BlogIcon esstory 2011/01/27 16:45

      키로 CString 을 사용할 수 있습니다. 아래 정도 코드일거예요
      CAtlMap<CString, 구조체, CStringElementTraits<CString>>

댓글을 남겨주세요 :)




submit
연관글(트랙백)이 2개 있습니다.
http://eslife.tistory.com/trackback/325 관련글 쓰기
  1. [펌] STL map 과 ATL 의 map 성능 비교 // standyhon님의블로그 2011/01/24 06:49 [Del]
  2. STL의 map 자료구조는 생각보다 느리다?! // don' care about rits' taskin' 2011/12/15 09:12 [Del]

latest comment