ndc15 - 사례로 살펴보는 msvc 빌드 최적화 팁

52
사례로 살펴보는 MSVC 빌드 최적화 팁 넥슨지티 서든어택2실 프로그램팀 황의권

Upload: yi-kwon-hwang

Post on 12-Aug-2015

300 views

Category:

Software


5 download

TRANSCRIPT

Page 1: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

사례로 살펴보는 MSVC 빌드 최적화 팁

넥슨지티 서든어택2실 프로그램팀

황의권

Page 2: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

발표에 앞서

발표자는 누구고 무슨 이야기를 하려 하는가

Page 3: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

황의권 서든어택2 프로그램팀 팀장 2007 - Tibero(RDBMS) 2009.봄 - 가을 카바티나스토리 - 2011.여름 메이플스토리 - 2012.봄 아이러브커피 외

- 현재 서든어택2

발표자 소개

취미 : 코스츔 플레이어

Page 4: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

빌드 시간은 개발 생산성에 밀접한 문제 - 특히 게임에선 개발 iteration 비용이 중요

누구나 얘기하지만 잘 챙기긴 어렵다

왜 빌드 발표요?

출처 : compiling (https://xkcd.com/303/)

Page 5: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

간단히 검색해 본 NDC 관련 세션

• Unity Build로 빌드타임 반토막내기 – 송창규 [NDC10]

• 레가시 프로젝트의 빌드 자동화 – 최재훈 [NDC11]

• 효율적인 모바일 게임 개발을 위한 모바일 빌드 시스템과 모바일 배포 시스템 구축 노하우 –

윤보선/김태효 [NDC15]

• 하루에 3번! 삼시세끼 빌드 만들기! – 안현석 [NDC15]

빌드 엔지니어링은 단골 주제

Page 6: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

다양한 개발 환경은 말할 것도 없고, 하나의 개발 환경에서도 수많은 상황을 고려해야 함

빌드 엔지니어링은 넓고 깊은 주제

Page 7: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

사례로 살펴보는

MSVC

빌드 최적화

제목 부연 설명

서든어택2 서버 개발 중 있었던 일

Microsoft Visual C++

빌드 생성 과정에 대한 엔지니어링

Rule of Thumb 중심 + 가벼운 연관지식

Page 8: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

사건의 개요

무슨 일이 있었나

Page 9: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

게임 서버 주요 환경 • 언어 환경 : VS2013, C++11(의 일부)

• 주요 라이브러리 : boost 1.5x, Intel TBB 4.x • 저장소 : 아직은 Perforce 그 외 개발 스택 • C#, Python/flask • MSSQL, Redis, MongoDB • Linux, Git, AWS, …

개발환경

Page 10: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

Incredibuild – 상업 분산 빌드 솔루션 사용 중 모든 빌드 관여 머신에 SSD 장착 -> 적절한 돈으로 해결할 수 있는 건 이미 해둔 상태 -> 대신 빌드 머신은 평범

개발 환경 - 빌드 관련 외부 환경

Page 11: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

“서버 프로젝트 CI빌드가 느려요. 언제부터인진 몰라도..” “원인은 파악 됐나요?” “실마리는 아직, 해결하려면 좀 봐야 할 것 같아요” “그럼 이번 릴리즈 끝난 후 봅시다” 이 대화가 세 번 쯤 반복된 후, 드디어 뚜껑을 열어보기로 결정

회의에서 나온 이야기

Page 12: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

서버프로젝트 CI 빌드 시간이 15분+ 서버 통합바이너리 용량이 109MB

상황 확인

Page 13: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

전체 개발 사이클의 병목은 아니지만 commit이 망설여짐 상식 밖의 빌드 시간 • 당시 소스파일 크기(LOC) : 약 175K • 데디케이티드 서버는 별도

심각성 분석 – 시간 문제

175K

도표 : 개발일정에 따른 LOC 변화

코드 정리

큰 마일스톤

Page 14: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

큰 용량이 기능과 성능에 영향을 주지는 않았음

하지만 바이너리가 109메가면 이거 너무 심한 거 아니오

심각성 분석 – 용량 문제

Page 15: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

가설 1 : 이런 이상 상황이 갑자기 발생한 특정한 시점이 있지 않을까?

진행 1 : 로그! 로그를 보자!

전략 선택 – 1단계

Page 16: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

이분검색을 해서 문제가 되는 커밋을 찾자

- 총 서버 변경 리비전 수 : 약 3000 -> 이분검색 12(2^12=4096)번, 간단하네?

로그를 분석한다

Page 17: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

빌드 시간 검색 • CC.NET CI 빌드 로그

• 검색하기 너무 불편 • 검색툴을 만들까? -> 배보다 배꼽이…

• 직접 빌드해보면 되지 않을까? • 12번 리비전이동/빌드 • 하지만 문제가 생긴 시점이 특정된다는 보장이 없다!

로그분석 – 시간 추적

Page 18: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

CI 빌드 바이너리는 저장소에 커밋되지 않고 있었고orz 대신 서비스 서버 관리 툴이 쓰는 중간 저장소에 드문드문 로그가 있었다

- 최초 서비스 배포 버전(리비전 약 1000)이 이미 30MB - 시간에 따라 거의 선형 증가하는 모습

로그분석 – 용량 추적

30MB

109MB

Page 19: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

과거 기록 분석은 FAIL, 지금의 상황만 가지고 문제점을 찾아 돌파하는 정공법으로 전환

가설 2 : 바이너리 용량이 크게 나오는 게 빌드 시간에 영향을 주지 않을까?

진행 2 : 용량! 용량을 보자!

전략 선택 – 2단계

Page 20: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

바이너리 용량 문제 분석

내가 빌드가 느린 건 아무리 생각해도 용량 탓이야!

Page 21: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

단순화한 MSVC 컴파일 과정

전처리기

컴파일러

링커

전처리 지시자 처리(#include,#define, …)

모든 헤더파일이 풀린 큰 소스파일 생성 한 소스파일을 컴파일하여 obj 파일 생성 obj 파일과 lib 연결 exe/lib/dll 생성

Page 22: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

단순화한 SA2 게임서버 빌드 과정

코어 lib 프로젝트

코어.lib 공통로직.lib 네트워크.lib

데이터베이스.lib

역할별 서버

프론트엔드서버.lib 매칭서버.lib

커뮤니티서버.lib (그 외 로직 서버들)

단일 런처

서버런처.exe

Page 23: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

어떤 문제인지 명확히 모르는 상태에서, 해결책 try 용량 관련한 빌드 옵션 체크 리스트 • 최적화 옵션 O1로? • 디버깅 정보를 빼고 해볼까? • 문자열 풀링이 안 되어 있다거나?

샷건 디버깅 – 용량 편

-> 효과는 0.1MB 미만 -> 위와 같음 -> 릴리즈 빌드에서는 강제 켜짐

실패

Page 24: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

프론트엔드서버.lib 748MB 매칭서버.lib 107MB 커뮤니티서버.lib 74MB … • 로직이 집중된 프론트엔드가 가장 크다 • 나머지들도 정상은 아니다. 로직 양에 비례하는 듯

빌드 중간 파일을 분석해보자

Page 25: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

맵 파일 : 빌드 때 옵션을 주면 생성되는 바이너리 구조 명세 TXT 영역별 시작 주소와 길이, 코드 영역의 각종 기호 포함

맵 파일 분석

Start Length Name Class

0001:00000000 0001:00025f30 … 0002:00000000 0002:00000b40 … 0002:000078e0 0002:063c7bf0 0003:00000000 …

00025f2cH 002ccb00H 00000b40H 00000008H 0637bf20H 00088b70H 000029b8H

.text$di

.text$mn .idata$5 .CRT$XCA .rdata .xdata .data

CODE CODE DATA DATA DATA DATA DATA

수상하다!

.rdata : (주로) 전역 상수, 문자열같은 것이 위치

Page 26: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

DUMPBIN : MSVC 내장 툴 실행파일/라이브러리 구조를 조금 더 면밀히 보고 싶을 때 사용 File Type: EXECUTABLE IMAGE Summary 2606000 .data 28000 .pdata

6455000 .rdata 9000 .reloc 1000 .rsrc 31D000 .text 1000 .tls

DUMPBIN 활용 – 기본 정보

0x6455000

= 105205760 (100MB)

Page 27: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

DUMPBIN /section:.rdata File Type: EXECUTABLE IMAGE SECTION HEADER #2 .rdata name

6454BFA virtual size 31E000 virtual address (000000014031E000 to 0000000146772BF9)

6454C00 size of raw data 31C800 file pointer to raw data (0031C800 to 067713FF) …

-> 문제가 맞아 보임, 하지만 아직 정보가 부족

DUMPBIN 활용 – 섹션 상세 정보

Page 28: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

DUMPBIN 활용 – rawdata 덤프

DUMPBIN /section:.rdata /rawdata 어마무시한 결과 속에서 반복되는 특정 문자열 패턴 발견 0000000140B9DCB0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000000140B9DCC0: 00 00 00 00 00 00 53 00 41 00 32 00 5F 00 45 00 ......S.A.2._.E. 0000000140B9DCD0: 43 00 5F 00 4D 00 55 00 4C 00 54 00 49 00 50 00 C._.M.U.L.T.I.P. 0000000140B9DCE0: 4C 00 45 00 5F 00 43 00 4F 00 4E 00 4E 00 45 00 L.E._.C.O.N.N.E. 0000000140B9DCF0: 43 00 54 00 49 00 4F 00 4E 00 5F 00 44 00 45 00 C.T.I.O.N._.D.E. 0000000140B9DD00: 4E 00 49 00 45 00 44 00 00 00 00 00 00 00 00 00 N.I.E.D......... 0000000140B9DD10: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000000140B9DD20: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................

Page 29: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

초창기에 만들고 잊어버린 에러설명 자동생성코드 ErrorCode.h

const ErrorCode ErrorCode_Table[] = { … { -150,

L"SA2_EC_MULTIPLE_CONNECTION_DENIED", L"동일한 아이디로 접속을 시도했습니다", …},…

코드에서 찾아보자!

Page 30: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

헤더파일에서 데이터 입력이 이루어짐 struct ErrorCode {

int code;

wchar_t create_part[21];

wchar_t message[201];

};

const ErrorCode ErrorCode_Table[] = { …

{ -150, L“SA2_EC_MULTIPLE_CONNECTION_DENIED", L”동일한 아이디로 접속을 시도했습니다.”, …},…

왜 문제였는가?

헤더파일에 선언된 전역 상수 고정크기 문자열이 각 obj 파일마다 공간 차지

Page 31: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

상수 데이터가 오브젝트파일마다 중복 생성되는 것을 제거 • ErrorCode_Table을 헤더에서 extern으로 선언 • 테이블 초기화 코드는 ErrorCode.cpp로 옮김

처치

Page 32: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

바이너리크기 : 109MB -> 9MB!

빌드시간 : 미미한 영향(약 10초 감소…)

여담 : 고정크기 문자열을 const wchar_t*로 바꾸기만 해도 13MB로

결과!

Page 33: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

빌드 시간 문제 분석

우리 서버 빌드가 이렇게 느릴 리가 없어

Page 34: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

C++ 코딩 관련 기본 상식을 잘 지키는지 체크 • 헤더, 특히 시스템 헤더 include를 최소화 • 모듈 의존성을 단순하게 • 헤더와 몸통(cpp) 엄격한 분리 등

빌드 옵션 조절 • 디버깅 레벨 조절

샷건 디버깅 – 시간 편

Page 35: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

• PCH(Pre-Compiled Header) 사용 중

• 헤더와 몸통의 분리 • 손으로 짠 모듈에서는 엄격히 지키는 편 • Pimpl idiom도 일부 선택 활용 중

• 자동 생성 코드에서 소홀한 점 추가 발견

• 시스템 / 외부 헤더 파일 관리 • 템플릿 전방선언을 끼얹을 요소 발견 • (헤더에서 boost 타입 참조하는 일부 경우)

우리 프로젝트 체크리스트

Page 36: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

자주 포함되는 코어 헤더들의 시스템 헤더 include를 정리하고 자동생성된 코드에도 규칙을 엄격히 적용한 결과 PacketAll.h(모든 패킷 헤더를 다 뭉쳐본 파일)의 전처리 끝난 LOC 변화

183,620 줄 -> 5,278 줄

줄어든 시간 : • 10초 내외로 거의 변화 없음 orz • 그 외 디버깅 레벨 등 옵션 조절도 모두 미미한 영향

코드 정리 결과

Page 37: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

어이쿠 이 야크 봐라 아주 털이 잘 깎였네?(출처:로이터)

Page 38: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

그러고보니, 중간중간 링크 과정에서 대부분의 시간을 소요

빌드 과정을 유심히 보자

링크 과정

Page 39: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

봐야 할 기호의 수가 너무 많은 경우 • 네임스페이스를 열어버린 경우(예 : 헤더에서 using namespace std;)

헤더파일 의존성이 복잡한 경우 • 컴파일 타임에 드러나지 않고 링크 타임에 드러난다거나? • 이번에도 자동 생성 파일이 문제인가?

분산 컴파일은 되는데 왜 분산 링크는 없나? • 분산된 걸 모으는 게 링크인데 링크를 또 분산하면 분산된 링크를 모으는 링크가..

브레인스토밍 : 링크 시간에 관하여

Page 40: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

여기서 떠오른 팀원과의 대화 Q: 근데 빌드하는 데 15분이면 로컬 작업도 힘들잖아요? A: 아뇨 로컬에선 디버그 빌드로..

디버그 빌드는 : 2분 30초

..아 이 이야기를 분명 처음에 들었는데

잠깐!

Page 41: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

링크 과정에서의 병목 현상이 없음

디버그 빌드 과정을 유심히 보자

Page 42: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

1. 링크 타임만 오래 걸린다 2. 디버그 모드 빌드에서는 링크 타임이 오래 걸리지 않는다 : 최적화 관련?

-> 전체 프로그램 최적화 / 링크 타임 코드 생성(LTCG)

실마리 발견

Page 43: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

컴파일 단계에서 하나의 소스만 가지고 진행되는 일반 최적화에 더해,

컴파일 결과물(obj, lib)에 최적화를 위한 정보를 포함시키고

링크 때마다 포함된 정보+실시간 프로파일링으로 최적화된 바이너리 생성

전체 프로그램 최적화 / 링크타임 코드 생성

Page 44: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

빌드 옵션 확인

잘 보이지도 않음 심지어 기본 사용..

Page 45: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

링크 타임 코드 생성 옵션 OFF

빌드 시간 15분 -> 1분 55초 후속 조치 : Release 다음 단계, Shipping 빌드 신설 • Release : LTCG OFF • Shipping : LTCG ON

조치 및 결과

Page 46: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

원래 오래 걸린다고 한다 • 링크 타임 오래 걸린다고 하면 LTCG부터 체크하라는 답글이… 우리 프로젝트 빌드 과정 특성때문에 더 심했던 것으로 추측

LTCG, 왜 그리 오래 걸리나?

Page 47: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

[다시보기] 단순화한 SA2 게임서버 빌드 과정

코어 lib 프로젝트

코어.lib 공통로직.lib 네트워크.lib

데이터베이스.lib

역할별 서버

프론트엔드서버.lib 매칭서버.lib

커뮤니티서버.lib (그 외 로직 서버들)

단일 런처

서버런처.exe

Page 48: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

[다시보기] 개선 전 릴리즈 빌드 과정

링크 과정

Page 49: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

들인 시간 • 업무시간(관여한 모든 사람의) 8시간 남짓 한 일 • 시행착오(샷건 디버깅), 야크쉐이빙(코드정리)에 시간 허비 • Map 파일 분석과 DUMPBIN을 써서 용량문제 발견

• 자동생성코드 위치이동으로 해결 • LTCG 옵션 꺼서(…) CI빌드 시간 단축

회고

Page 50: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

소위 삽질이 많았다

- 샷건 디버깅은 핵심 문제에 접근하는 데에는 비효율적 - 귀찮아도 문제를 분석하고 접근하는 버릇을 들이자!

- 실무에서 멀어졌던 팀장이 일 잡았을 때의 폐해

교훈

Page 51: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

링크 타임 코드 생성의 단위 비용을 줄일 순 없을까? - 다단계 링크 구조를 개선해보면?

- Unity Build를 쓰면 더 빠를 것 같은 예감? - 마지막엔 lib들끼리 링크하는데 소용 없을지도..

Future work

Page 52: NDC15 - 사례로 살펴보는 MSVC 빌드 최적화 팁

감사합니다.