조성호
shcho@gnome.org 2016

# 무엇을 최적화 할까요?

그놈을 최적화할 때 우선 기억해야 할 점이 있습니다. 프로그램을 더 좋게 만들려는게 아니라, 사람이 컴퓨터를 더 즐겁게 쓸 수 있게 하려는 과정이 최적화입니다.

더 좋은 프로그램은 여러 사람을 즐겁게 하지만, 반응성, 시동 시간, 명령 접근 용이성, 프로그램을 둘 이상 여는 순간 컴퓨터가 스왑에 접근하지 않게 하도록 개선하면 더 많은 사람을 즐겁게 합니다.

기존의 최적화에서는 CPU 사용, 코드 길이, 마우스 클릭 횟수, 프로그램의 메모리 사용과 같은 개념을 다루었습니다. 두번째 목록은 첫 목록과 서로 관련 있도록 선정했지만, 중요한 차이점이 있습니다. 그놈을 사용하는 사람은 두번째 목록은 신경 쓰지 않지만, 첫 목록에는 상당히 신경을 씁니다. 그놈 프로그램을 최적화 한다면 CPU 사용량, 메모리 사용량 같은걸 줄이겠지만, 이건 결국 최종적으로 해야 할 일이지, 우선적인 목표는 아닙니다. 프로그램을 최적화한다는건 사람을 위한 일입니다.

# 최적화하기

앞 부분에서는 우선적으로 중요한 이야기가 빠졌습니다. 무언가를 최적화하려면 최적화 대상을 측정할 수 있어야합니다. 측정이란 결코 즐거울 수 없습니다만, 시작 시간을 측정하여 개선 여부를 확인할 수 있습니다. 그 다음에야 즐거움은 원하는 대로 따라옵니다.

최적화는 측정, 개선, 재측정의 과정입니다. 따라서, 우선 해야 할 일은 최적화 할 대상을 찾는 일입니다. 이상적으로 측정 대상에서 나오는 값은 한가지 입니다. 예를 들자면, 작업을 수행하는데 걸리는 시간입니다. 이것이 바로 우세한지 열등한지 확인할 수 있는 벤치마크입니다. 빨라야 하는 프로그램과 빠른 프로그램은 큰 차이가 있습니다.

기본 벤치마크 과정을 수행하고 나면 동작해야 할 코드가 왜 동작하지 않는지 찾아야합니다. 자세하게 살펴보면 이 과정에 귀가 솔직할 수 있습니다. 그냥 코드를 보고 개선이 필요한 부분을 찾는겁니다. 여러분은 예외 없이 잘못을 저지를 수 있습니다. 프로파일러를 사용하면 실제 프로그램이 하는 동작을 자세하게 확인합니다.

보통 문제는 코드 일부에 한정합니다. 잘못된 부분을 찾아서 해당 부분에 집중하십시오. 일단 이 과정이 끝나면 프로파일러를 다시 실행하고 반복하십시오. 각 과정마다 결과가 계속 줄어들면서 어떤 시점에 도달하면, 충분한 결과에 도달합니다. 10%의 개선 노력만 해도 그만두어야 할 시점을 충분히 지나칠 수 있습니다.

전체 상황을 잊지 마십시오. 예를 들자면, 코드 일부 속도를 끌어올리기보다는, 전체 관점에서 실행해야 하는 지 자신에게 질문해보십시오. 다른 코드 부분과 붙을 수있나요? 이전 처리 결과를 저장하고 다시 사용할 수 있나요? 사용자가 살펴볼 일이 없는 부분은 굳이 최적화할 필요가 없습니다. 애석하게도, 이미 코드를 최적화했음에도 불구, 나중에 치워버려야 할 엄청난 처리량을 감당할 수도 있습니다. 이 경우,코드는 자체 환경에서 동작하지 않으며, 최적화 과정을 거치지도 않았습니다.

# 길잡이

## 기초

코드를 바꿀 때마다 벤치 마크를 다시 실행하시고 벤치 마크에 영향을 주는 모든 항목의 기록을 남겨두십시오. 이 기록을 통해 실수를 되돌릴 수 있고 실수를 반복하지 않게 해줍니다.

최적화하기 전 코드가 올바른 지, 버그가 없는지 확인하십시오. 최적화 후에도 제대로 된 상태이며 버그가 없는지 확인하십시오.

세부적인 부분을 최적화하기 전 고수준 영역을 최적화하십시오.

올바른 알고리즘을 활용하십시오. 고전 교과서의 예제에서는 버블 정렬 알고리즘 대신 퀵 정렬 알고리즘을 활용합니다. 일부 메모리를 절약하고 CPU 사용 시간을 줄이는 다른 알고리즘 예제도 있습니다. 또한 어떤 간편책을 활용할 수 있는지 살펴보십시오. 더 나은 해결책이 있다면 퀵 정렬보다 빠른 알고리즘을 활용할 수 있습니다.

최적화는 상충 관계입니다. 캐싱 처리 하면 처리 속도가 빨라지지만 메모리 사용량이 많아집니다. 디스크에 데이터를 저장하면 메모리 사용량을 줄일 수 있지만 디스크에서 다시 읽어들일 경우 시간이 오래걸립니다.

최적화할 다양한 입력 방식을 고려하십시오. 이를 고려하지 않는다면 입력 파일에 대해서만 신중하게 코드 최적화하는 방식으로 끝낼 수 있습니다.

비용이 많이 나가는 처리 방식은 피하십시오. 디스크를 조금씩 여러번 읽으십시오. 메모리 사용량을 늘리면 디스크 스왑 공간이 필요할 수도 있습니다. 하드디스크의 불필요한 읽기 쓰기 동작을 피하십시오. 네트워크도 역시 느립니다. X 서버 응답이 필요한 그래픽 처리도 피하십시오.

## 부주의의 함정

부작용을 상기하십시오. 코드의 다른 부분 사이에 어느 한 부분의 속도가 빨라지면서 다른 부분이 느려지는 이상한 동작이 종종 일어날 수 있습니다.

일부 시스템에서 코드 동작 시간을 맞출 때, 프로그램 외부에서 발생하는 이벤트는 타이밍 결과에 문제를 일으킬 수 있습니다. 보통 여러 프로그램을 실행합니다. 코드가 매우 짧다면 시간 처리 최소 단위가 문제일 수도 있습니다. 이 경우 컴퓨터로 코드를 100번 내지는 1000번 정도 실행하여 시간을 측정하십시오. 여러분이 확인하는 시간이 고작 몇 초 밖에 안된다면, 이걸로 만족하는게 좋습니다.

프로파일러의 결과로 상황을 매우 쉽게 오판할 수 있습니다. 운영체제 대기 루프를 최적화하는 이야기가 많은데 이 부분에서 대부분의 시간을 소요하기 때문입니다! 사용자가 신경쓰지 않는 부분에 대해서는 코드를 최적화하지 마십시오.

X 서버의 자원을 기억해두십시오. 프로그램의 메모리 사용량에는 X 서버 프로세스의 픽셀 매핑 항목이 없지만, 여전히 메모리를 사용합니다. 프로그램의 어떤 자원을 사용하는지 보려면 xrestop을 활용하십시오.

## 초수의 길잡이

메모리 사용량을 최적화할 때, 순간 최대 사용량과 평균 메모리 사용량의 차이에 주의하십시오. 일부 메모리 공간을 거의 항상 할당하지만, 보통 좋지 않은 습관입니다. 일부는 최소화하여 할당하는데, 이 정도면 납득할만합니다. massif 같은 도구는 메모리 사용량과 할당 시간을 곱한 수치인 공간시간 값의 개념을 사용합니다.

여러분이 알고 있는 동작만 처리하는 단순 코드 일부의 동작 시간은 상당히 중요한데, 코드가 동작하는데 걸리는 최소한의 본질적인 최소 시간 한계를 제시합니다. 예를 들어, 비어있는 반복문의 루프 처리 시간을 최적화한다고 가정해보겠습니다. 여전히 오래 걸린다면 어떤 세부 최적화도 도움이 되지 않으며, 설계 자체를 바꿔야합니다. 컴파일러에서 비어있는 반복문을 최적화하지 않는지 확인해보십시오.

반복문 내에서 코드를 줄여나가십시오. 좀 더 복잡한 코드를 한번 실행하는게 단순한 코드를 여러 번 실행하기보다 빠릅니다. 코드를 종종 느리게 하는 호출을 피하십시오.

가능한대로 컴파일러에 여러 힌트를 주십시오. const 키워드를 사용하십시오, 종종 호출하는 함수에 대해 `G_INLINE_FUNC`를 사용하십시오. `G_GNUC_PURE`, `G_LIKELY` 및 glib 기타 매크로를 찾아보십시오. gcc용 키워드보다는 매크로를 사용하여 이식성을 개선하십시오.

어셈블리 언어를 사용하지 마십시오. 어떤 프로세서에서는 빠르게 동작할 지 모르겠지만 다른 시스템으로 이식할 수 없으며, 해당 아키텍처를 지원하는 모든 프로세서(예: 애슬론 vs 펜티엄4)에서 빠를거라는 보장은 없습니다.

불필요하게 느려졌다는 확신이 서기 전에는 기존 라이브러리 루틴을 재작성하지마십시오. 대부분 CPU를 집중적으로 사용하는 라이브러리 루틴은 이미 최적화 해둔 상태입니다. 다르게 말해서, 일부 라이브러리 루틴은 운영체제의 시스템 콜을 호출할 때 느립니다.

연결하는 라이브러리 수를 최소화 하십시오. 최소한의 라이브러리를 연결하면 프로그램 시작 속도가 빨라집니다. 그놈에서는 이렇게 하기 어렵습니다.

## 고수의 요령

동시성의 이점을 취하십시오. 다중 프로세서를 활용하라는 의미가 아니며, 앞을 내다보고 일부 처리 과정의 다음 처리 과정을 생각하여 사용자의 프로그램 사용 시간상 이점을 취하라는 의미이기도 합니다. 디스크에서 데이터를 불러오면서 기다리는 동안 다른 무언가를 처리하십시오. 여러 자원의 이점을 활용하여 동시에 활용하십시오.

꼼수를 활용하십시오. 사용자는 오직 컴퓨터가 빠르다고 생각하며, 실제로 컴퓨터가 빠른지 아닌지는 상관하지 않습니다. 속도는 명령과 중요한 응답 사이의 시간이며 응답을 미리 처리했는지, 캐싱했는지, 사용자가 기대한 만큼 오랜 시간동안, 실제로 더욱 편리한 시간에 처리할 지는 상관 없습니다.

대기 루프에서 처리하십시오. 완전한 멀티스레딩을 활용하기보단 쉽겠지만, 이 과정에서 사용자가 못 보는 영역에서 처리한 내용을 가져옵니다. 하지만 대기 루프의 처리 시간이 매우 오래 걸린다면 프로그램이 느려질 수 있으므로 주의하십시오. 그래서 보통 제어권을 메인 루프에 넘깁니다.

이렇게 해도 실패했다면, 사용자에게 코드 동작이 느려질 수 있다고 알려주면서 진행 표시줄을 놓으십시오. 사용자는 이 결과에 만족하지 않겠지만, 최소한 프로그램이 갑자기 끝나진 않을거라는걸 알고 커피 한잔을 할 여유를 가질 수 있습니다.