C/C++ 대규모 프로젝트에서 빌드 속도를 올려보자

🧐 | 2021-12-28

안녕하세요, 넷마블 TPM실 기술분석팀 김범진입니다.

커피 한잔을 앞에 두고 비주얼 스튜디오 2019에서 ‘F7’키나 ‘Ctrl + Alt + F7’키를 누른 후, 올라가는 프로그레스바를 무념히 바라보신 기억이 있으신 분들께 이 글을 올립니다.

현업 프로젝트에서 빌드하는 코드는 짧아도 몇만 줄, 길게는 몇백만 줄이 되기도 합니다. 당연히 코드가 늘어날수록 빌드 속도는 더디어지고, 이 시간이 누적되면 테스트 시간을 확보하지 못하는 문제까지 이어지기도 합니다.

C와 C++ 대규모 프로젝트에서 빌드 속도를 올릴 방법이 있지 않을까요?

참고로, 언리얼 엔진(Unreal Engine)에서는 공식 문서에서 ‘bUseUnityBuild’ 옵션을 볼 수 있습니다. 이 글에서는 이 옵션에 대한 내용은 다루지 않습니다. 만약 언리얼 엔진에서 ‘bUseUnityBuild’ 옵션을 사용하실 분들께선 충분히 검토 후 사용하시길 당부드립니다.

상용 툴

상용 툴 중에는 인크레디빌드(Incredibuild)가 있습니다. 인크레디빌드는 그리드 컴퓨팅 방식을 활용해 빌드 속도를 높여주는 툴입니다. 제가 간단히 찾아본 결과, 엔터프라이즈 라이선스 비용이 상당했었습니다. 제품 소개 상으로는 80~90%가량 빌드 시간을 단축해주는 만큼, 비용 효과는 충분할 수 있습니다.

유니티 빌드(Unity Build)

상용 툴 이외에는 어떤 방법이 있을지, 효율적으로 빌드속도를 개선할 방법은 없을지 궁금해서 더 찾아보던 중 눈에 들어온 방법이 유니티 빌드(Unity Build)였습니다. 여기서 이야기하는 ‘유니티 빌드(Unity Build)’ 방법은 지금 우리가 흔히 떠올리는 유니티(Unity) 게임 엔진 이야기가 아닙니다. 개발 아이디어의 한 종류입니다.

10년 넘게 지난, NDC 2010에서 ‘UnityBuild로 빌드타임 반토막 내기’라는 발표가 있었습니다. (발표 내용 자체는 원문 링크를 참고해주세요.)

참고링크1: 위키피디아
참고링크2: Unity Builds

유니티 빌드에서 받은 영감

컴파일 단계

빌드 속도에 영향을 주는 요소를 살펴보려면, 컴파일 단계를 먼저 이해해야 합니다.

컴파일 단계는 크게 4단계로 구분할 수 있습니다.

1단계 – 전처리(Preprocessing)

전처리(Preprocessing) 단계는 매크로(#define), 헤더 (#include), 메타 소스 생성(template) 등을 분석해 소스를 텍스트(Text)화하는 단계입니다.

2단계 – 컴파일(Compilation)

컴파일(Compilation) 단계는 전처리 단계에서 텍스트화된 소스와 cpp 파일에 들어 있는 소스를 어셈블리 언어로 번역하는 단계입니다.

3단계 – 어셈블(Assemble)

어셈블(Assemble) 단계는 컴파일 단계에서 번역한 어셈블리 언어를 기계어(obj)로 변경하는 단계입니다. 완전히 기계어로 바꿔주는 부분이라고 생각하시면 됩니다.

4단계 – 링크(Linking)

링크(Linking) 단계는 어셈블 단계에서 생성한 obj와 라이브러리를 묶어, 실행 파일 또는 라이브러리로 만드는 단계입니다.

아이디어

유니티 빌드에서는 무의미한 코드 라인을 제거하고 헤더 파일을 정리하면서, 컴파일러 입력량과 참조 파일 개수를 줄였습니다. 그리고 그렇게 줄인 만큼 전체 빌드 속도가 향상됐었습니다.

유니티 빌드에서 했던 내용 중, “컴파일 입력량 줄이기”에 집중해보기로 했습니다. 컴파일 단계의 시작점인 전처리 단계에서 산출되는 텍스트양을 줄일 수 있다면, 컴파일 작업도 동시에 줄일 수 있을 테니까요.

전처리 산출물 테스트

이제 생각했던 아이디어를 실제로 해보면서 효과를 확인해보겠습니다.

먼저 테스트 파일 3개를 준비해, 모두 ‘#include <vector>를 추가했습니다. (딱히 어떤 특징적이라거나 기능적인 요소가 있는 참조는 아닙니다.) 전처리만 실행한 결과를 보기 위해 ‘cl.exe’를 ‘/E’ 옵션으로 실행했습니다.

$ cl.exe /E a.cpp > precompile_a.txt
$ cl.exe /E b.cpp > precompile_b.txt
$ cl.exe /E main.cpp > precompile_main.txt

전처리 이전에는 362바이트(53+85+53+86+85)였으나, 전처리 이후에는 3,423,451바이트(919650+919651+1584150)로 약 3MB 정도 늘어났습니다. 전처리로 무엇이 바뀌었는지 보기 위해 파일 내용을 열어봤습니다.

전처리 전에 7줄이었던 ‘a.cpp’ 파일과 b.cpp’ 파일은 약 4만 줄 정도로 늘었고, 전처리 전에 9줄이었던 ‘main.cpp’ 파일은 약 6.7만 줄이 될 만큼 늘었습니다. 간단히 늘어난 내용을 훑어보니, 공백도 많고 중복된 소스도 많아 보였습니다. 이후 진행할 컴파일 단계에서 공백은 총 컴파일 시간에 영향을 주지 않으리라 보이지만, 중복된 소스는 생성된 수만큼 영향을 줄 우려가 있어 보입니다.

cpp 파일 수에 따라 중복이 생길 것이라 예상되므로, 중복 소스 생성을 줄일 수 있다면 전처리 시간도 n배수만큼 줄일 수 있을지 확인해보기로 했습니다. 만약 컴파일할 타깃 cpp 파일을 1개로 줄여서 산출물과 중복 모두 제거하면, 어느 정도 빌드 속도가 빨라질까요?

상황별 테스트

단일 cpp 파일 컴파일 테스트

위에서 세팅했던 테스트 소스를 cpp 파일 하나에 다 모아서, 전처리 결과를 살펴보겠습니다.

$ cl.exe /E merge.cpp > precompile_merge.txt

전처리를 한 파일이 1개였으므로, 산출물도 1개만 나왔습니다. 파일 용량은 919,895바이트가 됐습니다. 약 0.9MB 정도입니다. 3개 파일이었을 때 약 3MB였던 용량에 비해 약 3분의 1 정도로 줄었습니다.

용량이 줄어든 만큼, 빌드 속도도 빨라졌지 않았을까요? 비주얼 스튜디오에서 비교 테스트를 해보겠습니다.

비주얼 스튜디오에서 확인해보니, 3개 파일을 1개 파일로 합친 것만으로 빌드 시간이 728ms에서 492ms로 줄어든 것을 볼 수 있었습니다. 링크 단계 시간을 제외하고 계산해보면 350ms에서 131ms로 줄어들었습니다. 빌드 시간도 약 3분의 1정도로 단축됐습니다.

파일 개수를 줄인 것만으로 전처리와 컴파일 속도는 3배 빨라졌고, 용량은 3분의 1로 줄었습니다. 분명 효과는 확실합니다.

하지만 이미 다들 예상하시겠지만, 이렇게 1개 파일만으로 작업한다면 소스관리는 어떻게 해야 할지 아득합니다. 당연히 유지보수가 힘들 수밖에 없습니다. 또한, 파일 크기가 일정 크기를 넘어서면 컴파일러 메모리 사용 초과로 인해 컴파일 자체가 되지 않습니다. (파일 크기 제한은 비주얼 스튜디오 속성 페이지에서 ‘/bigobj’ 옵션을 설정해서 ‘.obj’ 파일 수용 범위를 늘려줘야 합니다. 자세한 내용은 공식 문서를 확인하시면 됩니다.)

즉, 단순히 빌드 속도 개선을 위해 1개 파일로 모으는 방법은 현업 프로젝트에서는 실현 불가능하다고 봐야 합니다.

cpp 파일 개수를 유지하면서 단일 파일로 컴파일하는 방법

그렇다면, cpp 파일 개수는 유지하면서 단일 파일로 컴파일하는 방법은 없을까요?
우선, ‘#include’에 헤더 파일만 넣을 수 있다는 상식을 버려야 합니다. cpp 파일도 ‘#include’에 넣을 수 있으며, 실제로 cpp 파일을 병합(merge)한 것과 동일하게 동작합니다. 실제로는 어떠한지 확인해보겠습니다.

‘main.cpp’안에서 ‘#include “a.cpp”와 ‘#include “b.cpp”만 추가했습니다. cpp 파일은 헤더 파일보다 아래에 추가해야 하니, 꼭 주의하셔야 합니다. 또한, 비주얼 스튜디오에서 ‘a.cpp’와 ‘b.cpp’는 컴파일에서 제외하는 옵션을 설정해야, 중복 링크 에러가 발생하지 않습니다. 이제 전처리 단계를 실행해보겠습니다.

$ cl.exe /E main.cpp > precompile_main_2.txt

전처리 이후, 단일 cpp 파일로 테스트했던 결과와 비교해도 큰 차이가 없는 산출물이 나왔습니다.

빌드 속도도 빨라졌고 cpp 파일 개수도 유지했으므로, 소스관리와 유지보수에 대한 걱정을 덜 수 있게 됐습니다. 그래도 너무 많은 파일을 모으기보다는 적당히 분산해야 수용 용량 내에서 컴파일러가 원활히 동작하므로, 잘 분산해서 사용하셔야 합니다.

윈도우 프로젝트라면 초간단하게

윈도우 프로젝트라면 정말 간단하게 위 방법을 적용할 수 있습니다. 단, 비주얼 스튜디오 2017에서는 베타 기능으로 제공되고 있으므로, 비주얼 스튜디오 2019 이상에서 쓰시길 추천합니다.

비주얼 스튜디오 2019 이상에서는 ‘프로젝트→ 속성→ 고급’에서 기본 기능으로 제공하고 있습니다.

만약 파일의 최대 소스 개수를 변경해야 한다면, ‘프로젝트→ 속성→ C/C++→ Unity 빌드 옵션’에서 최소 소스 개수와 최대 소스 수로 포함할 파일 개수를 조절할 수 있습니다. 단, 이 옵션은 위에 있던 ‘Unity(JUMBO) 빌드 사용’을 활성화해야만 나타나므로, 보이지 않으시다면 유니티 빌드 사용 설정을 먼저 확인해보셔야 합니다.

집에 일찍 가자

현업 프로젝트에서는 cpp 파일에 중복된 헤더가 많았던 만큼, 더 큰 효과가 나오기도 했습니다. 실제로 인크레디빌드를 쓰면서도 컴파일 시간이 20분 이상 걸렸던 프로젝트에서는 위 방법을 써서 2분 30초로 줄였습니다. 로컬 테스트에서 50분 이상 걸리던 빌드 시간을 10분 안쪽으로 당겼던 사례도 나왔습니다.

간혹, 헤더 정리나 소스 관리 전략을 이미 잘해둔 경우에는 큰 단축 효과를 못 보기도 했습니다. 그래도 30분 이상 걸리던 빌드를 10분 이내로 줄일 수 있다면, 우리는 새로 확보한 20분을 어디에 쓸지 고민해야 하는 행복회로를 돌릴 수 있지 않을까요?

단축한 시간에 무엇을 할지 고민하시기보다는, 우선은 집에 일찍 가는 것을 목표로 하시길 추천해 드립니다. 이상 워라밸 가디언으로 활동하는 넷마블 기술분석팀 김범진이었습니다.