Cocos2d-x 로 된 C++ 프로젝트를 처음 맡았다. 한 줄 고치고 ndk-build 를 돌리면 20 분이 걸렸다.
Table of contents
Open Table of contents
매번 전체가 다시 됐다
처음에는 원래 그런 줄 알았다. ndk-build 는 안쪽이 GNU Make 라 바뀐 것만 다시 만든다. 그런데 거의 전체가 다시 빌드됐다.
무엇이 다시 빌드되는지 보니 .cpp 하나를 고쳐도 수백 개가 딸려 왔다. Make 가 안 도는 것이 아니라 의존 관계가 넓은 것이었다.
공통 헤더가 원인이었다
Common.h 하나에 전부 들어 있고 거의 모든 .cpp 가 그것을 #include 하고 있었다. GCC 가 만든 .d 파일을 열어 보니 그 의존 목록이 그대로 있었다. Common.h 를 고치면 포함한 전부가 다시 빌드되니 결과가 전체 빌드와 같아진다.
그리고 자주 고치는 값이 하필 거기 있었다.
#define MAX_ENEMY 30
밸런스를 조정할 때마다 이 줄을 바꾸는데 그때마다 20 분이었다. #define 은 전처리 단계라 값 하나만 바뀌어도 그 헤더를 본 전부가 다시 컴파일된다. 자주 바뀌는 것과 널리 포함되는 것이 한 파일에 있으면 그 조합이 비용을 만든다.
나누고 값을 밖으로 뺐다
Common.h 를 자주 바뀌는 것과 안 바뀌는 것으로 나눴다. GameConfig.h 는 쓰는 .cpp 에서만 #include 하게 했다. 고쳐도 일부만 다시 빌드된다.
더 자주 바뀌는 값은 아예 assets 아래 파일로 뺐다.
int maxEnemy = config.get("max_enemy");
빌드 자체가 필요 없어졌다. 고쳐서 adb push 로 밀어 넣고 다시 실행했다. 밸런스를 여러 번 조정하는 흐름이 크게 달라졌다.
다만 전부 뺄 수는 없었다. 실행할 때 읽는 값은 읽는 비용이 있다. 매 프레임 쓰는 것은 const 로 뒀다. 조정하는 값은 파일로 두고 성능에 민감한 값은 상수로 두는 것으로 나눴다.
포함 줄이기와 측정
각 .cpp 가 필요 없는 것까지 #include 하는 것도 줄였다. 다만 무엇을 빼도 되는지 알기 어렵다. 하나 빼고 ndk-build 를 돌려 보고 안 되면 다시 넣는 식이었다. 오래 걸려서 자주 고치는 파일만 했다.
하다가 알게 된 것이 있다. 포인터로만 쓰면 전체 정의가 필요 없다.
class Enemy; // 선언만
class Level { Enemy* enemy; };
이걸로 #include 관계가 많이 줄었다. 한 줄 고치고 기다리는 시간이 20 분에서 몇 분이 됐다.
처음부터 하는 전체 빌드는 여전히 비슷했다. ndk-build -j4 로 코어를 다 쓰게 하고 NDK_CCACHE 에 ccache 를 걸어 두니 두 번째부터는 짧아졌다.
그래도 전체는 하루에 한두 번이고 증분은 하루에 수십 번이다. 자주 하는 쪽이 줄어든 것이 컸다. 무엇을 자주 하는지가 어디를 줄일지의 기준이었다.
정리
- 공통 헤더 하나에 전부 넣으면 한 줄 고쳐도 전체가 다시 빌드된다
- 자주 바뀌는 것과 널리 포함되는 것이 한 파일에 있으면 비용이 커진다
- 자주 바뀌는 것과 안 바뀌는 것을 나눈다
- 더 자주 바뀌는 값은 파일로 빼면 빌드가 필요 없다
- 다만 성능에 민감한 것은 상수로 둔다
- 각 파일이 필요한 것만 포함하게 한다
- 포인터로만 쓰면 선언만으로 된다
- 전체 빌드는
-j와NDK_CCACHE로 줄인다 - 자주 하는 쪽을 줄이는 것이 체감에서 크다