성능평가의 비직관성

거창한 얘기를 하려는 것은 아님. 일반론을 좀 벗어난 단순한 사례를 하나 소개해보겠다.

지난 주말에 intel 에서 제공하는 threading building block1 의 해쉬 맵(tbb::concurrent_hash_map)과, MS VS 2005에서 제공하는 해쉬 맵(stdext::hash_map)의 성능을 비교하려고 간단한 프로그램을 짜기 시작했다. 성능평가를 위해 테스트할 순서를 다음과 같이 정했다.

  1. 싱글 스레드에서 임의 삽입, 탐색으로 성능 비교
  2. 싱글 스레드에서 임의 삽입, 탐색. 다만 stdext::hash_map은 spin-lock을 하나 잡았다 놨다는 한다.
  3. 멀티 스레드에서 임의 삽입, 탐색으로 성능 비교

개략적으로 내가 원했던 것은,

  • 1, 2의 차이에서 spin-lock 성능을 비교하려고 했고,
  • 2, 3의 차이에서 하나의 spin-lock을 경쟁하는 것과 tbb의 해쉬 맵의 각 버켓마다 별도로 잡히는 read-write lock의 성능을 비교하려는 것. 물론 동시 접근이 된다는 것으로 인한 차이도 나게 될 것이다.

이런 식으로 두 가지 성능 차이를 낼 수 있는 원인을 분석하려고 한 것.

일단 내가 사용하는 컴퓨터가 그런데로 듀얼 코어니 거기서 테스트를 하려고 대충 이런 프로그램을 구상하고 작성했다.

  • N개의 UCS-2 문자열을 만들어낸다. 문자열의 문장에 의미가 있을 필요는 없다
  • 문자열을 해쉬 맵에 넣는다
  • 임의 탐색을 시도한다

근데 1에서 내가 직관적으로 생각한대로 동작하질 않는다. tbb::concurrent_hash_mapstdext::hash_map 보다 1.5배~3배 정도 빠르게 동작하는 것이다. 생각했던 결론 — concurrent_hash_map이 더 복잡한 자료구조 + lock이 복잡하니 느릴 것이다 — 과는 완전히 모순되는 결과.

그렇지만 과학적 방법론을 사용한다면 폐기된 가설은 버리고 다른 원인을 찾아야지.

N을 바꿔가면서 테스트하니 결론이 나오더라. N 이 일정 크기 이하가되면 stdext::hash_map 의 성능이 상승하며, 일정 크기 이하에선 오히려 stdext::hash_map이 4배 이상 빠르다. 결국 원인은 이런 것.

  • tbb의 컨테이너들은 intel에서 제공하는 scalable-allocator를 사용한다
  • stdext::hash_map은 MSVC에서 제공하는 (그래도 커스텀 할당자이긴 하지만) 할당자를 쓴다
  • N의 초기값이 컸다. 대략 400만개~800만개

결국은 성능 분석을 지배한 요인이 lock 의 오버헤드가 아니라 메모리 할당 속도였던 것.

성능 평가에 관한 내용은 쉽게 말할 수가 없다. 복잡한 컴퓨터 시스템 만큼이나 요소는 많고, 결국엔 독립변수를 변화시켜가면서 돌려보고, 프로파일링해보고, — 그리고 이번 경우처럼 숨겨진 변인도 제거하고 — 적합한 패러미터를 찾아야 한다.

그러니 직관적으로(…) 성능을 평가하는 사람이 있다면 경계하자. 성능평가는 절대로 직관적이지 않다.

ps. 정작 저 둘의 성능 비교 이야기는 다음으로 미룬다 :p


  1. intel의 TBB에 관한 소개는 여기에서. 멀티코어 환경을 활용하기 위한 여러가지 알고리즘과 컨테이너들을 제공한다. ↩︎