고성능 서버 응용프로그램을 위한 고민: 메모리 편

  • Post author:
  • Post category:칼럼
  • Post comments:0 Comments
  • Post last modified:January 3, 2011

이 글은 월간 마이크로소프트웨어(일명 마소) 2010년 5월 19일자 뉴스캐스트에 기고한 글입니다. 물론 구성이나 내용 상의 차이가 있을 수 있습니다.

메모리 최적화

얼그레이가 제공하는 메모리 관리자는 나름의 최적화 전략을 취한다. 구현된 부분이 있는가 하면, 아직 작업 중이거나 계획 상으로만 존재하는 부분도 있다. 하지만 얼그레이의 메모리 관리 전략을 알아보기 전에 일반적인 최적화 원칙을 알아보자.

  • 캐시라인 활용

  • 메모리 페이지 활용

  • TLB (Translation Lookaside Buffer) 활용

  • JIT (Just-in-time) 읽기 회피
    소프트웨어 수준에서 하드웨어 프리페칭을 돕는다.

  • 병렬성

    • False sharing
      각기 다른 스레드가 동일한 캐시 라인을 사용할 때 발생한다. 결국 전역 변수가 문제의 원인이 된다.

    • 공통 작업 집합
      다수의 스레드가 동일한 데이터를 읽는 건 좋다. 그러나 동일한 메모리 위치에 결과를 놓는 건 문제가 된다. 스레드 별로 작업집합을 두고 막판에 가서 결과를 합병하는 식으로 문제를 해결한다.

  • 동기화

이렇게 나열한 목록을 이제부터 상세히 살펴보자.

대역폭

캐시라인, 메모리 페이지, TLB 등은 메모리 대역폭과 관련된 이슈이다. 한정된 대역폭을 최대한 활용하려면 데이터를 묶어서 전송하되 코어 간에는 데이터 이동이 많지 않게 해야 한다.

메모리는 보통 4096 바이트 단위로 할당된다. 하지만 최신의 인텔 CPU의 캐시라인은 4096 바이트보다 크며 이 크기에 맞춰 메모리를 할당하면 메모리 대역폭을 향상시킬 수 있다. 페이지 크기가 프리페치 성능을 결정한다고 보면 된다.

상당수의 최신 프로세서는 Large page, Medium page란 걸 지원하기도 한다. 이 역시 프리페치와 관련이 있다. Large page의 크기는 캐시라인 크기의 배수이다. 그만큼 대역폭을 활용할 여지가 크다고 하겠다.
캐시라인 또는 Large page의 크기는 프로세서마다 다른데 Windows 같은 운영체제는 이 값을 동적으로 알아내는 API를 제공한다.

병렬성

False sharing 문제는 메모리 할당 단위를 캐시라인보다 크게 유지하면 해결된다. 동기화 문제는 뒤에 다룰 스레드 로컬 힙 메모리 관리자에서 해결한다.

메모리 – 전역 힙 메모리 관리자

운영체제 힙 관리자의 성능에 대한 불만은 오래 전부터 있었다. 최신 Windows는 Low Fragmentation Heap (LFH)을 제공하여 성능이 향상되었지만 서버 개발자들은 스스로 메모리 풀과 힙 관리자를 만들어 쓰길 선호한다. 제대로 설계하고 구현하면 그만한 득을 거둘 수 있기 때문이다.

얼그레이에선 GreedyAllocator가 전역 힙 메모리 관리자 역할을 한다. 물론 빌드 옵션을 바꾸면 운영체제나 써드파티의 메모리 관리자를 전역 힙 메모리 관리자로 쓸 순 있다. 여하튼 이 메모리 관리자의 특징은 다음과 같다.

독점 전략

운영체제에서 한번 할당 받은 메모리 영역은 다시 반환하지 않고 응용프로그램 내부에 둔다. 메모리 사용량이 가장 많을 때를 기준으로 메모리를 확보해두어 다음을 대비한다. 전용 머신을 둔 서버 응용프로그램이란 가정을 두었기에(지난 글: 스레딩 편) 이런 구현이 가능하다.

캐시라인 최적화

앞서 메모리 대역폭 이슈는 살펴보았다. GreedyAllocator는 메모리 할당 단위를 프로세서에 맞춰 최적화한다. 물론 할당 단위가 커짐으로써 낭비되는 공간은 많아지지만 스레딩을 다룬 1편에서 말했듯 메모리는 충분하다는 가정에 바탕을 둔다.

메모리 – 스레드 로컬 힙 메모리 관리자

스레드 로컬 힙 메모리 관리자는 전역 힙 메모리 관리자 위에 작동한다. 전역 힙 관리자는 아무리 최적화하더라도 내부 데이터 구조에 접근할 때 동기화 이슈가 발생한다. 이런 문제를 해결하려면 스레드마다 메모리 풀을 따로 두면 된다. 단지 해당 스레드가 확보한 메모리 공간이 바닥났을 때는 전역 힙 메모리 관리자에 메모리 할당을 요청하고 이때 동기화 이슈가 발생한다. 그러나 전역 힙 메모리 관리자에 메모리를 요청할 때 당장 필요한 것보다 큰 메모리 단위를 요구함으로써 할당 요청 횟수를 줄이면 동기화로 인한 성능 감소 폭도 줄어든다.

성능 벤치마크란 게 테스트 사례에 따라 결과가 달라지므로 100% 믿을 건 못 된다. 그렇더라도 얼그레이 단위테스트에 포함된 성능 테스트를 돌려보면 ThreadLocalAllocator가 Windows의 Low Fragmentation Heap보다 10배 가량 빠르다. 이 정도면 그럭저럭 괜찮게 나왔다 하겠다.

메모리 – 스택 메모리 관리자

메모리를 스택에서 할당 받는 전략은 새로운 게 아니다. __malloca 같은 함수는 기존에도 있었다. 그럼에도 스택 메모리 관리자를 따로 구현하는 이유는 간단하다. 빌드 옵션으로 스택의 크기를 정하는 방식은 유연하지 않기 때문이다. 고정된 크기 이상의 메모리는 할당하지 못 하는 문제 등은 더 이상 존재하지 않는다.

마치는 글

얼그레이는 한정된 컴퓨터 아키텍처만 고려한다. 멀티프로세서는 감안하지만 NUMA(Non-Uniform Memory Access) 플랫폼은 신경 쓰지 않는다. 만약 이러한 플랫폼이 온라인 게임 분야에 널리 쓰이게 되거나 그럴 준비가 되면 몰라도 말이다. 그 만큼 얼그레이의 메모리 할당 전략에는 개선의 여지가 있다.

참고 자료

Author Details
Kubernetes, DevSecOps, AWS, 클라우드 보안, 클라우드 비용관리, SaaS 의 활용과 내재화 등 소프트웨어 개발 전반에 도움이 필요하다면 도움을 요청하세요. 지인이라면 가볍게 도와드리겠습니다. 전문적인 도움이 필요하다면 저의 현업에 방해가 되지 않는 선에서 협의가능합니다.
0 0 votes
Article Rating
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments