본문 바로가기

C/C++

최적화

@markdown


# 최적화


어떤 코드를 작성하더라도, 쉽게 빠지지 않는 중요한 것들 중 하나를 뽑자면 역시 최적화라고 할 수 있다. 같은 설비 투자 비용으로 더 많은 사용자에게 더 좋은 사용자 경험을 안겨주기 때문이다.

_

개발자 관점에서도 최적화는 왠지 모르게 멋져 보여, 욕심이 날 때가 있다. 그러나 최적화를 하기 전 가장 먼저 생각해야 하는 것은 '이걸 꼭 해야 하는가?' 다. 가볍고 빠른 최적화된 프로그램은 결코 쉽게 얻을 수 있는 것이 아니다.

_

그럼에도 불구하고 서비스나 프로그램을 '누가 왜 어떻게 무엇을 언제' 사용하는지 파악하고, 최적화가 필요하다는 결론이 나오면 작업을 시작한다. 최적화를 할 지점은 도구의 도움을 최대한 받는다. 도움을 줄 도구는 어지간한 메이저 개발 언어라면 당연히 준비되어 있을 것이고, 리눅스라면 플랫폼 수준에서도 도움을 주는 도구가 있으니 적극 활용한다.

_

최적화가 필요한 스팟을 찾아내면, 그 부분이 최적화가 가능한지에 대해 먼저 생각해본다. 그리고 프로그램 디자인을 바꿔서 최적화를 해야 하는 부분인지, 프로그래밍 테크닉을 사용해서 최적화를 해야 할 부분인지 판단한다.

- 디자인을 고쳐야 할 경우, 많은 시간과 비용이 동원되어야 할 수 있으나, 결과적으로는 괜찮은 성능 향상을 달성하면서도 괴상망측한 코드가 피어나는 것을 최대한 억제할 수 있다. 그러나 좋은 디자인은 경험과 많은 리서치가 필요한 부분이다. R&D의 균형이 중요한데, Research를 이런 저런 이유로 멀리하면 오래 가기 어려운 디자인이 나올 확률이 높아질 것이다.

- 프로그래밍 테크닉을 사용해서 최적화를 해야 할 부분이라면 상대적으로 적은 시간과 비용이 필요할 수 있으나, 큰 소득을 보기 어려울 수 있고, 높은 확률로 최적화만을 위한 괴상망측한 코드가 나올 수 있다. 사실 이 경우는 평소에 최적화를 생각하며 코드를 작성하는 것으로 미약하게나마 방지할 수 있다.

_

최적화를 실제로 달성하기 위해 내가 적용하는 것들은 다음과 같다.

- 코드를 짧게 유지한다. 혹은 개발할 서비스가 너무 많은 것을 제공하려고 하지는 않는가 되돌아봐야 한다.

- N이 충분히 작을 것이라고 예상되는 경우, 멋지지만 규모가 있는 알고리즘보다 짧고 단순한 알고리즘을 사용한다.

- 동적 할당을 최소화한다. 미리 정해진 만큼을 할당하고, 최대한 많은 객체가 참조하게 만들거나 자원의 풀로 취급한다.

- 미리 계산해 둘 수 있는 것들이 있다면 계산해서 상수화한다. 계산을 안하는 게 제일 빠르다.

- 최대한 게으르게 작동하게 만든다. 나중으로 미룰 수 있는 것들은 최대한 나중으로 미룬다.

- 컴파일러나 인터프리터의 최적화 관련 플래그 중 현재 적용할 수 있는 것들은 최대한 많이 적용한다.

- 컴파일러나 인터프리터의 확장 기능을 적극적으로 활용한다.

- 컴퓨터 구조를 고려해서 코드를 작성한다.

    - 같은 메모리 페이지를 최대한 자주 사용하게 한다. 이를 위해 메모리 풀을 만들거나 Huge Page를 사용한다.

    - 캐시를 고려해 메모리 주소를 정렬한다. cache line alignment 라고 한다.

    - 프리패치 인스트럭션을 사용한다.

    - cpu intrinsics를 용도에 맞게 활용한다. SIMD 명령은 퍼포먼스 향상에 꽤나 도움이 된다.

    - 시스템 콜 사용을 최소화한다. 최대한 많은 것들을 유저모드에서 처리한다.

    - 메모리 채널 및  프로세서 소켓 구조를 고려한다. 객체를 각 메모리 채널로 흩뿌리고, NUMA 상황에서 가장 짧게 접근이 가능한 곳을 알아둔다.

- 복사를 최소화한다. 할 수 있다면 Zero Copy까지.

- 최대한 빠른 IPC 방식을 선택한다. 가능하다면 Huge Page를 사용한 Shared Memory까지.

- busy polling 대신 event와 callback을 사용한다. 단, 하드웨어가 낼 수 있는 대여폭 수준까지 고성능이 필요하면 busy polling을 사용한다.

- 락을 최소한으로 사용한다. 자원이 정말 공유되어야 할 것이 아닌 경우, 공유하지 않고 여러개로 만든다.

- 스레드 생성을 최소화한다.

    - 로직 자체가 복잡해 계산이 많이 필요하다면 어쩔 수 없다. 스레드를 사용하되 코어 수 x 2개의 스레드만 유지하려 노력한다.

    - 입/출력은 IOCP 또는 epoll을 사용해 단일 스레드로 처리하려 노력한다. 비동기 처리로 인해 코드가 복잡해질 것이 확실시되면 코루틴을 사용한다.

_

각고의 노력을 통해 가볍고 빠른 프로그램이 나왔다면, 최적화를 달성하기 위해 했던 노력들을 문서화한다. 문서화가 되어있지 않으면, 높은 확률로 다른 사람은 당신의 결과물을 이해하기 힘들 것이고, 같은 시행착오를 반복할 것이다.

_

[CPU Cache 관련 실험 링크](http://igoro.com/archive/gallery-of-processor-cache-effects/)