본문 바로가기

진리는어디에/C++

[C++20] 모듈(module) 컴파일 성능 비교

 

 

들어가며

C++의 프로젝트의 규모가 점점 커짐에 따라 프로젝트 빌드 속도도 그에 비례해 늘어나게 된다. 심한 경우는 오타하나 수정했는데 빌드하느라 몇 십분이 소모되기도 한다. 아무것도 하지 못하고 빌드 프로그래스바만 바라보고 있어야 한다는 것은 엄청난 인력과 시간의 낭비가 아닐 수 없다.

여기에 나와 같은 고민을 하든 분들을 위한 반가운 소식이 있다. C++20 스펙에 추가된 모듈(module)을 사용하면 단순 헤더 파일을 사용하는 것 보다 훨씬 빠른 컴파일 속도를 얻을 수 있다고 한다.

모듈이 궁금하신 분들은 '[C++20] 모듈(module)' 포스트에서 보다 자세한 내용을 확인 할 수 있다.

이번 포스트에서는 이 모듈이 어떻게 헤더파일을 사용하는 것 보다 빠른 컴파일 속도를 제공하는지에 대해 간단하게 알아본 후 헤더만을 사용하는 프로젝트, 미리 컴파일 된 헤더를 사용하는 프로젝트와 빌드 속도를 비교하여 어떤 방식이 가장 빠른 빌드 속도를 제공하는지 실험해 보도록 하겠다.

모듈은 어떻게 빠른 컴파일 속도를 제공하는가?

전통적인 헤더파일 컴파일 방식은 모든 개별 빌드 단위(일반적으로 .cpp)마다 헤더파일의 소스를 복사하고 각각 컴파일하는 작업을 반복한다. 반면에 모듈은 한번 컴파일 되면 그 결과는 내보내진(export) 모든 '타입', '함수' 및 '템플릿'에 대해 기술하는 이진 파일에 저장되고, 이 이진 파일의 내용를 빌드 단위에서 링킹만 한다.

예를 들어 설명하면 헤더파일 방식의 경우 템플릿 함수가 작성된 Header.h를 include하고 템플릿 함수를 사용하는 Source_001.cpp, Source_002.cpp ... Source_100.cpp 파일들이 있다면 개별 cpp 파일 마다 템플릿 함수 코드를 생성하고 컴파일하는 과정이 필요하다. 예에서 cpp 파일이 100개였다면 100번의 템플릿 코드 생성과 컴파일 작업이 반복 된다(그것이 동일한 할지라도 말이다!!)

모듈의 경우는 일단 모듈이 컴파일 되고 나면 각 빌드 단위(cpp)에서 템플릿 함수를 생성하고 컴파일하는 것이 아니라 단순히 모듈에 기술된 내용을 링크만 함으로써 컴파일에 소모되는 시간을 대폭 감소 시킬 수 있다. 이는 cpp파일이 많아지면 많아질 수록..즉 프로젝트가 커지면 커질수록 더욱 빛을 발하게 된다.

보다 자세한 내용은 [C++20] 당신이 모듈(module)을 써야만 하는 이유에 정리 되어 있으니 꼭 같이 보도록하자.

그래서 얼마나 빠른가?

필자는 실험을 위해 세 개의 프로젝트를 생성했다.

  • 전통적인 헤더파일만을 사용하는 프로젝트
  • 미리 컴파일된 헤더(stdafx.h)를 사용하는 프로젝트
  • 모듈을 사용하는 프로젝트

그리고 각 프로젝트 마다 템플릿 클래스와 템플릿 함수를 작성한 헤더 파일 2개씩(모듈 프로젝트에서는 모듈 2개)을 포함하고 각 헤더를 별도로 include하는(모듈의 경우 import)하는 15개의 cpp들을 추가했다.

실험에 사용된 프로젝트는 [여기]에서 확인 할 수 있다.

왜 헤더 파일과 모듈 파일을 하나가 아닌 2개..정확히는 여러개로 만드는지 의문이 갖는 사람도 있을 것이다. 이는 뒤에서 설명 하도록 하겠다.

좌측 부터 전통 헤더 프로젝트, 미리 컴파일된 헤더 프로젝트, 모듈 프로젝트

결론 부터 말하자면 당연히 전통 헤더파일 프로젝트의 성능이 가장 좋지 않았다. 가장 성능이 좋았던것은 나의 희망과는 달리 미리 컴파일 된 헤더 프로젝트였다. 모듈 프로젝트도 나쁘지 않은 성능을 보이긴 했지만..그리고 cpp파일이 더 많아지면 많아질 수록 더 나은 성능을 내긴하겠지만..그래도 위의 실험에서는 미리 컴파일된 헤더 프로젝트 보다는 낮은 성능을 보였다.

미리 컴파일 된 헤더 프로젝트가 제일 빠름
  • 전통적인 헤더 프로젝트 : 8.86 초
  • 미리 컴파일 된 헤더 프로젝트 : 2.10 초
  • 모듈 프로젝트 : 4.65 초

심지어 exe파일의 사이즈도 미리 컴파일된 헤더의 것이 가장 작았다.

  • 전통적인 헤더 프로젝트 : 100kb
  • 미리 컴파일 된 헤더 프로젝트 : 100kb
  • 모듈 프로젝트 : 199k

가만히 생각해보면 미리 컴파일 된(될?) 헤더의 모든 코드들을 읽어 미리 컴파일하여 기계어 파일에 저장하고(비주얼스튜디오에서 pch 파일이라고 생성하는게 바로 이것이다) 각 빌드 단위의 컴파일 타임에 매번 새로 빌드하는 것이 아니라 컴파일된 내용을 가져다 쓰는 것은 모듈 이전에도 미리 컴파일 된 헤더에서 예전 부터 해왔던 일들이었다.

앞에서 필자가 헤더 파일과 모듈을 2개씩 만든것을 기억할 것이다. 그 이유는 헤더 또는 모듈 파일의 수정이 발생하는 경우 프로젝트에 얼마나 큰 영향을 미치는지 알아보기 위함이다.

각 프로젝트의 Header2.h 또는 Module2.ixx 파일만을 수정하고 다시 빌드해 보았다. 먼저 전통 헤더 프로젝트의 결과를 살펴 보자.

1>ClCompile 대상:
1>  Source_104.cpp
1>  Source_105.cpp
1>  Source_204.cpp
1>  Source_205.cpp
1>  Source_304.cpp
1>  Source_305.cpp
1>
1>경과 시간: 00:00:05.82

전체 cpp들이 아닌 Header2.h에 의존성을 가지고 있는 파일들만 컴파일되고 빌드 되었으나 가장 긴 시간이 소요 되었다. 역시 전통적인 헤더 프로젝트는 답도 없다. 프로젝트에 빌드 시간이 문제가 된다면 전통 헤더 프로젝트에서 모듈이나 미리 컴파일된 헤더로 프로젝트를 변환하는것을 적극 권장한다.

미리 컴파일된 헤더 프로젝트의 경우 예상대로 어떠한 헤더를 수정하더라도 stdafx.h에 영향을 미치고, 모든 cpp들은 stdafx.h에 의존성을 가지고 있으므로 전체 프로젝트의 빌드로 연결 되었다. 

1>ClCompile 대상:
1>  stdafx.cpp
1>  Program.cpp
1>  Source_101.cpp
1>  Source_102.cpp
1>  Source_103.cpp
1>  Source_104.cpp
1>  Source_105.cpp
1>  Source_201.cpp
1>  Source_202.cpp
1>  Source_203.cpp
1>  Source_204.cpp
1>  Source_205.cpp
1>  Source_301.cpp
1>  Source_302.cpp
1>  Source_303.cpp
1>  Source_304.cpp
1>  Source_305.cpp
1>
1>경과 시간: 00:00:01.73

미리 컴파일 된 헤더의 경우 애초에 빌드 시간이 극단적으로 짧으므로 전체 빌드를 한다고 하더라도 가장 빠른 성능을 보여 준다. 

모듈의 경우는 아래와 같이 변경된 모듈 파일에 의존성을 가진 cpp들에 대해서만 컴파일이 발생하는 것을 볼 수 있다.

1>ClCompile 대상:
1>  Module2.ixx
1>  Source_104.cpp
1>  Source_105.cpp
1>  Source_204.cpp
1>  Source_205.cpp
1>  Source_304.cpp
1>  Source_305.cpp
1>
1>경과 시간: 00:00:02.69

빌드 시간도 줄어들긴 했다. 하지만 더 적은 숫자의 파일들을 빌드하는 미리 컴파일 된 헤더 보다 더 길다.

결론

모듈을 사용하는 것은 확실히 전통 헤더 방식 보다 컴파일 속도를 향상 시켜 준다. 특히 템플릿을 사용하고 많은 cpp파일들이 하나의 모듈에 의존성을 가지는 경우 그 빛을 발한다.

하지만 필자의 희망과는 달리 미리 컴파일된 헤더 프로젝트가 가장 좋은 빌드 성능을 보여주었다. 필자가 몸 담고 있는 프로젝트는 이미 미리 컴파일 된 헤더가 적용되었음에도 불구하고 빌드 속도가 극악으로 느리기 때문에 모듈에서 새로운 해결책을 찾아 보려 했으나 이 부분은 실패로 돌아가서 마음이 아프다.

부록 1. 같이 읽으면 좋은 글

유익한 글이었다면 공감(❤) 버튼 꾹!! 추가 문의 사항은 댓글로!!