C++ 프로젝트의 빌드 속도 개선하기 – 이론편

  • Post author:
  • Post category:
  • Post comments:2 Comments
  • Post last modified:February 8, 2020

이 문서는 NDC 2011 발표 분량 중 KGC 2011 에서 빠진 내용을 다룹니다. 마이크로소프트웨어 2012년 2월호에 실렸습니다.

KGC 2011

상용 게임을 개발하다 보면 초기에는 문제가 되지 않았던 것이 뒤에 부각되기도 한다. 느린 빌드는 그러한 문제 중 하나이다. 소스 코드와 리소스(그래픽, 사운드 등)가 늘어가면서 빌드를 한번 뽑는데 들어가는 수고가 만만치 않게 된다. 빌드 시간이 증가하면 개발진이 변경을 최소화하려 하며 때에 따라서는 올바른 방식을 두고 지름길을 택하게 된다. 그럼으로써 기술 빚이 늘어가며 종국에는 빌드 시간이 감당 못할 만큼 증가하는 악순환을 겪게 된다.

컴뱃암즈의 경우, 한때는 빌드 과정이 A4용지 24쪽에 달할 만큼 복잡했다. 그나마 모든 과정을 기술한 문서는 아니었으며 빌드 작업자의 지식과 경험에 의존하는 부분이 컸다. 이렇게 복잡한 빌드 과정의 대부분을 자동화한 후에 수작업일 때보다 실수할 여지가 줄어들고 평균적인 빌드 시간도 줄었다. 하지만 빌드는 여전히 도전적인 과제였다. 단 한번만 실수하거나 예기치 않은 오류가 발생하면 야근으로 이어지기 일쑤였다.

야근을 방지하기 위한 선택지는 몇 가지 있었다. 하나는 빌드 과정을 완전히 자동화하고 오류 상황에 대해 완벽히 대비하는 것이다. 하지만 비교적 쉬운 부분이 끝나고 투자 대비 시간 소모가 심한 작업만 남아 있는 상황이었다. 두 번째는 빌드 시간을 단축해 피드백 주기를 짧게 하는 것이다. 빌드 시간이 매우 짧다면 설사 실수가 있더라고 얼른 고치고 퇴근하면 될 일이다.

어느 정도의 예산 지원만 있다면 후자가 더 나은 접근 방법이라고 판단했다. 무엇보다 빌드 시간이 단축되면 개발진 전부, 특히 프로그래머 모두가 그 혜택을 누리기 때문이다.

컴뱃암즈의 사례

하드웨어 업그레이드

처음에는 공용 머신의 사양을 업그레이드하는 일에 착수했다. 하드웨어 업그레이드는 가장 손쉽고 효과가 분명한 조치이다. 고사양의 머신을 새로 장만한다 해도 그 비용은 빌드가 느려서 생기는 생산성 저하와 인건비 소모에 비하면 매우 저렴하다.

빌드를 출시할 때 사용하는 주요 머신을 꼽아보니 다음과 같았다.

  • 소스/웹 서버 머신
  • 빌드 머신
  • 파일/백업 머신
  • 패치 머신

이 중에서 빌드 작업에 영향을 크게 미치는 소스 서버와 빌드 머신의 하드웨어를 업그레이드하였다.

CPU

쿼드 코어 CPU 에서 헥사 코어 CPU 로 바꾸었다. 그러나 병렬성의 증가는 그다지 도움이 되지 않았다. 대부분의 빌드 도구가 병렬성을 제대로 활용하지 못하기 때문이다. 그나마 Visual Studio 2008 의 작업 속도가 빨라진 게 위안이었다.

I/O 장치

결과적으로 대부분의 성능 향상은 입출력 장치로부터 왔다. 8GB 의 메모리를 달고 대용량 메모리를 잘 운영하는 Windows 7 x64 를 설치하여 스왑을 줄였다. 빌드는 I/O 작업이 많기 때문에 스왑을 줄이는 게 핵심이다.

그림1

이와 더불어 HDD 를 SSD 와 하이브리드 SSD 로 교체하였다. 제일 중요한 빌드 머신은 SSD 를 갖추고, 이보다 덜 중요한 소스 서버에는 하이브리드 SSD 를 장착하였다.

Intel X25-M SATA Solid-State Drive (SSD)

Intel X25-M SATA Solid-State Drive (SSD) (Photo credit: IntelFreePress)

하드웨어 업그레이드의 효과

이렇게 하드웨어를 업그레이드하여 얻은 효과를 정량적으로 분석해보았다.

그림2

위와 같이 총 네 대의 빌드 머신(구형, 신형 등)을 두고 동일한 빌드 스크립트를 돌려보니 다음과 같은 결과가 나왔다.

그림3

결과적으로 하드웨어 업그레이드로 빌드 시간은 35% 단축되었다.

소스코드 빌드의 향상

분산 빌드 시스템의 도입

4 코어에서 6 코어로 CPU 를 업그레이드하고 SSD 까지 구매하였지만 소스코드 빌드는 예상보다 크게 개선되지 않았다.

그림4

작업관리자를 펼치니 Visual Studio 가 코어 6개를 다 활용하였다. 그렇다면 방법은 크게 세 가지였다.

  • 코어 개수를 늘린다. 하지만 시중에 6 코어 이상의 제품은 아직 나오지 않았다. 
  • 성능이 더 뛰어나고 더 값비싼 CPU 를 산다. 하지만 이번에 선택한 제품도 꽤 고가였다. 그리 나아질 것 같지 않다.
  • 분산 빌드 소프트웨어를 구매한다. 다행히 회사에 라이선스가 있었다.

게임 업계에서 주로 쓰는 분산 빌드 시스템은 IncrediBuild 라는 상용 제품이다. 이 제품은 Visual Studio 와 연동이 되고 설치와 운영이 쉽다. 또한 공용 머신의 하드웨어 업그레이드 효과를 모든 프로그래머에게 파급시킬 수 있어서 그 효과가 분명했다.

IncrediBuild 의 효과

IncrediBuild 를 도입하기 전후의 하드웨어 구성은 다음과 같았다.

도입 전
도입 후
  • Intel Core i5 750 @ 2.67GHz (코어 4개) + HDD
  • AMD Phenom II X6 1055T 2.80Ghz (코어 6개) + SSD
  • AMD Phenom II X6 1055T 2.80Ghz (코어 2개) + SSD

도입 후에 별도로 추가한 머신이 있지는 않았다. 다만 분산 빌드 시스템이다 보니 IncrediBuild Agents 가 설치된 모든 머신을 빌드에 쓸 수 있게 됐을 뿐이다. 소프트웨어 라이선스 비용 외에 별도로 부담된 곳은 없다.

그럼에도 분산 빌드의 효과는 놀라울 정도였다. 소스코드 빌드 시간이 9분 45초에서 5분 8초로 줄었다. 게다가 실제 운용 단계에서는 더 많은 라이선스를 구입해 Agent 숫자가 늘어나 이 테스트보다 더 빨라졌다.

그림5

물론 앞으로 살펴보겠지만 전체 출시 빌드에서 소스코드 빌드가 차지하는 비중이 일반적인 인식만큼 크지는 않다. 하지만 대부분의 프로그래머가 일상적으로 수행하는 개발자 빌드에서는 소스코드 빌드가 빌드의 거의 전부라 볼 수 있으므로 IncrediBuild 를 도입해 생산성이 매우 좋아졌음이 분명하다.

 

리소스 빌드의 향상

소스코드 빌드, 리소스 빌드, 패키징, 배포 등이 총망라되는 출시 빌드의 경우엔 소스코드 빌드 외의 과정이 차지하는 비중이 높다. 앞서 제시한 성능 측정 표를 살펴보면 패키징과 리소스 빌드, 그리고 아마도 대부분이 이와 관련된 작업일 파일 복사/삭제에 소요된 시간을 합하면 줄잡아 전체 시간의 절반을 넘긴다. 그나마 배포는 측정하지 않았다는 점을 고려하면 그 실제 비중은 더 크다고 하겠다.

외부 도구의 한계

성능 측정 결과를 보면 현대 소프트웨어 설계에서 병렬성이 왜 그토록 강조되는지 알 수 있다. 하드웨어 성능이 극적으로 좋아졌음에도 패키징은 고작 18%가 향상됐다. 이는 패키징 도구가 병렬성을 지원하지 않아 코어를 하나만 사용하는 까닭이다.

패키징이 전체 시간 중 무려 36%나 차지하는 과정임을 생각하면 병렬성 미지원이 초래하는 결과가 아쉬울 뿐이다.

디스크 입출력의 중요성

성능 측정 결과만 보면 I/O는 2% ~ 6%에 불과한 듯 보인다. 하지만 이는 성능 로그의 한계로 인한 관측 효과일 뿐, 실제 비중은 어마어마하다. 파일 복사/삭제 항목 외에 SVN 작업 대부분이 파일 검색과 비교에 소모되며, 컴파일과 패키징도 I/O를 수반한다.

정확한 수치가 명확히 드러나지는 않지만 디스크 입출력의 개선이야 말로 출시 빌드의 시간을 단축하는 가장 중요한 요소이다. 뒤이어 결과를 보면 알겠지만 앞선 작업에서 비교적 기대에 못 미치는 성능 향상 효과가 있었지만, 그 결과를 합쳐서 봤을 때는 매우 긍정적인 결과가 나온다. 이는 순전히 디스크 입출력을 개선했기 때문이다.

디스크 입출력 개선은 크게 세 가지 방향으로 접근한다.

  1. SSD 도입.
  2. 디스크 입출력 필요성의 최소화 – 리소스를 빌드할 때 SVN Diff 명령을 사용해 바뀐 부분만 정확히 찾아내 빌드한다.
  3. RoboCopy 의 사용 – 병렬 파일 복사, 버퍼링, 바뀐 파일만 복사하는 등의 기능을 갖추었다.

컴뱃암즈의 성과

이러한 노력의 결과, 컴뱃암즈의 빌드는 2시간에서 30분으로 단축되었다. 물론 이는 출시 빌드의 뒷부분인 배포 과정을 빼고 측정한 것이긴 하다. 하지만 절대 값만 보더라도 한번의 피드백 주기에서 1시간 30분이 줄어든 셈이다. 이는 여태까지 소개한 방법이 도전해볼 만한 가치가 있음을 보여준다.

 

못다한 이야기

꽤 만족할 만한 결과를 보긴 했으나 빌드 속도를 향상시키는 방법을 다 보인 것은 아니다. 이제부터는 여러 가지 이유로 컴뱃암즈 프로젝트에 아직 적용하지 못했으나 다른 프로젝트에서는 효과를 크게 보았던 방법을 알아본다.

UnityBuild

UnityBuild 는 소스코드 빌드의 속도를 극적으로 향상시키는 기법이다. 언리얼 엔진에는 이를 지원하는 도구가 탑재되어 있을 정도이고, IncrediBuild 같은 분산 시스템과 함께 사용하면 놀랄 만한 효과를 발휘한다.

이 기법의 이론적 기반은 소스 파일 여러 개를 하나로 합쳐 헤더 파일을 다시 읽어들이는 횟수를 극적으로 줄이는 것이다. 세부적인 내용은 NDC 2010(넥슨 개발자 컨퍼런스)에서 송창규씨가 발표한 ‘UnityBuild로 빌드타임 반토막내기’라는 자료를 참고한다.

UnityBuild의 문제점

UnityBuild 는 효과가 분명하고 그 이론적 기반은 매우 단순하다. 하지만 이를 대규모 프로젝트에 도입하려면 다음과 같은 문제가 있다.

  • 기존 프로젝트에 UnityBuild를 적용해주는 자동화 도구가 필요하다. 만약 여러분이 Visual Studio 2008을 사용한다면 운이 좋다. Earlgrey.BuildTools 에 포함된 UnityBuild 도구를 사용하면 되기 때문이다. 바이너리와 문서는 http://github.com/andromedarabbit/earlgrey/downloads/list에서 제공하는 압축 파일에 함께 포함되었다.
  • 소스 파일을 하나로 묶기 때문에 전역 변수 등의 이름이 충돌하곤 한다. 이는 근본적으로 소스 코드를 복사해 붙여넣는 등의 잘못된 관행에 따라 발생하므로 UnityBuild를 도입할 때 겸사겸사 수정하는 편이 좋다. 하지만 프로젝트 규모가 크고 충돌하는 곳이 많으면 그 과정이 순탄치 않을 수도 있다.

C++ 프로젝트의 물리적 구조를 이해하기

C++ 은 매우 어려운 언어이다. 프로그래밍 문법 뿐 아니라 그 물리적 속성(컴파일러의 특징, 프로젝트 간의 의존성 및 관계, 헤더 파일과 소스 파일의 관계 등)을 이해해야 대규모 프로젝트를 말썽 없이 이끌어나갈 수 있기 때문이다. 그러나 안타깝게도 대부분의 프로그래머가 C++의 물리적 속성에 대해서는 잘 모른다.

물리적 속성까지 제대로 알려면 수많은 책을 탐독하고 실제 경험을 쌓아야 한다. 그래도 이 글에서 몇 가지 힌트는 제시해보겠다.

미리 컴파일된 헤더

The Care and Feeding of Pre-Compiled Headers 를 참고하자. 미리 컴파일된 헤더는 컴파일 속도를 향상시키는 매우 훌륭한 기법이긴 하지만 주의할 점도 있다.

  • 순환 참조 등의 의존성 문제를 해결하려고 미리 컴파일된 헤더를 사용해서는 안 된다.
  • 해당 프로젝트가 참조하는 다른 프로젝트의 헤더 파일을 포함(#include)시킬 때 쓰고, 해당 프로젝트에 속한 헤더 파일은 되도록 포함시키지 않는다.
  • 작은 프로젝트라면 사용하지 않는 편이 낫기도 하다.
헤더 파일은 간단 명료하게

귀차니즘을 이기지 못해 헤더 파일에 모든 걸 몽땅 때려 박는 경우가 비일비재하다. UnityBuild의 이론을 공부하면 알겠지만 이는 빌드 시간을 증가시키고 파일/프로젝트 간 의존성을 복잡하게 하는 주범이다. 때로는 인라인 함수가 성능을 향상시킨다며 헤더 파일에 구현을 모조리 담기도 한다. 하지만 컴파일러는 사용자의 요구를 참조만 할 뿐 인라인 키워드를 매우 제한적으로 적용한다. 게다가 인라인 함수의 효과가 분명한 경우는 생각보다 드물며 헤더 파일에 포함(#include)되는 파일이 많아져서 생기는 문제에 비하면 그 효과가 의심스럽다.

따라서 헤더 파일을 간결하게 유지해야 한다. 소스 파일 쪽에 넣어도 되는 #include 문을 귀찮다고 헤더 파일에 넣어서는 안 된다. 해당 헤더 파일을 참조하는 다른 변환 단위(translation unit, 소스 파일)마다 불필요한 헤더 파일을 읽어 들이게 되는 까닭이다.

또한 필요한 경우에는 pimpl 같은 기법을 사용하는 것이 좋다. 이렇게 함으로써 파일 간 의존성이 깨지고 파일 하나를 살짝 고쳤을 뿐인데 전체 프로젝트를 다시 빌드해야 하는 일이 없어지게 된다. 단 pimpl 같은 기법을 난발하면 오히려 코드의 유지보수가 버거워지므로 주의해야 한다.

참고 자료

C++의 물리적 속성을 이해하려면 워낙 다양한 측면을 살펴봐야 하는데 아래 글이 요점을 잘 정리했다.

대규모 프로젝트에서 C++을 제대로 관리해나가는 법을 알고자 한다면 John Lakos 가 쓴 고전 Large-Scale C++ Software Design을 읽어야 한다. 번역서가 없고 내용이 만만치 않은 수준이라 버겁긴 하지만 그만큼 얻는 게 분명한 책이다. 위의 블로그 글도 결국엔 이 책을 요점 정리한 것에 불과할 뿐이다.

Large-Scale C++ Software Design

이 책이 버겁다면 Martin Reddy 의 저서 API Design for C++을 펼쳐보자. Large-Scale C++ Software Design만큼 빌드 시간에 초점을 맞추진 않지만 이와 관련해 핵심적인 사항은 모두 전달한다. 더불어 외부로 공개하는 API 의 특성답게 견고한 코드를 작성하는 법도 배울 수 있다.

 

마치는 글

느린 빌드는 생산성과 품질의 적이다. 피드백 주기가 길면 작업자가 짜증을 내고, 언짢은 마음으로는 좋은 품질의 제품이 나오기 어렵다. 그러므로 프로젝트 초기부터 빌드 시간이 지나치게 길어지는 법이 없도록 관리해야 한다.

이 글에서 살펴본 기법은 대부분의 C++ 프로젝트에 적용 가능하다. 따라서 규모가 큰 회사라면 데이터베이스 관리자처럼 빌드 엔지니어 또는 빌드 엔지니어링 조직을 따로 두고 여러 프로젝트를 지원하면 좋을 것이다. 상당수의 글로벌 게임사는 이러한 조직 체계를 도입해 프로젝트를 체계적으로 관리하며 우리가 배울 점이 많다.

아무쪼록 이 글이 여러분의 프로젝트에 도움이 되길 바란다.

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.

2 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Initialjk
Initialjk
12 years ago

UnityBuild의 숨겨진 단점 중 하나는 Header include에 대한 주의가 소홀해 진다는 것.
자주쓰는 헤더 파일은 선언 안해도 왠만해선 컴파일이 깨지지 않기 때문에 알아차리기 쉽지 않은데, 파일 크기 변화로 unity 경계를 넘거 가거나 하는 경우에 종종 빌드를 깨뜨리는 원인이 됨 (지시자를 달리해 빌드시 어떤 빌드는 성공하고 어떤 빌드는 실패하는 등)

그래서, 종종 unity build를 하지 않는 빌드를 테스트에 포함 시켜놓는게 좋음

CHOI, Jaehoon
12 years ago
Reply to  Initialjk

미리 컴파일된 헤더에도 비슷하게 지적할 수 있으나, 또 한 주제가 되니 일단 패스! ㅋㅋ