조성호
shcho@gnome.org 2016

# 그놈 프로그램의 메모리 사용량을 조사하는 Massif 사용

이 부분은 그놈 프로그램 프로파일러 Massif 사용법을 설명합니다. Massif 실행법, 해석법, 출력시 조치법을 설명하겠습니다. 스웰 푸프 게임을 예제로 활용하겠습니다.

# 도입부

Massif는 valgrind 메모리 프로파일링 도구 모음의 구성물입니다. 프로그램의 존재 목적은 프로그램 실행 시간동안 동적 메모리 사용 상황을 자세하게 보여주기 위함입니다. 특별히 힙과 스택 메모리 사용 상태를 기록합니다.

힙은 malloc과 같은 함수에서 할당한 메모리 영역입니다. 요청할 때마다 크기가 늘어나며, 프로그램에서 메모리의 많은 부분을 차지합니다. 스택은 로컬 데이터 및 함수를 저장하는 영역입니다. 이 부분에는 C의 "자동" 변수와 하위 루틴의 반환 주소가 들어있습니다. 스택은 보통 힙보다 작으며, 더 활발하게 동작합니다. Massif가 스택을 힙의 다른영역 처럼 다루기 때문에 스택을 분명하게 고려하진 않겠습니다. Massif에서는 힙을 관리하는 메모리를 얼마나 사용하는지도 보여줍니다.

Massif에서는 포스트스크립트 형식의 그래픽 개요 파일과 텍스트 파일 형식의 자세한 내용 파일을 제공합니다.

# 그놈과 Massif 활용

Massif에는 대부분의 프로그램에서 불필요한 몇가지 옵션이 있습니다. 그러나 glib 또는 GTK에서 처리하는 메모리 할당이 깊숙히 자리잡은 그놈 프로그램의 경우, Massif에서 내려다볼 호출 스택 레벨의 깊이가 늘어나야합니다. --depth 매개 변수를 활용하면 이 문제를 해결할 수 있습니다. 기본값은 3입니다. 이 값을 5 정도로 늘려주면 호출 스택의 밑바닥으로 들어갈 수 있을지도 모릅니다. 한 두단계 정도 레벨을 더 깊게 들어가면 여러분의 코드와 일부 정황을 납득할 수도 있습니다. 상세 수준을 깊이 들어가면, 금새 대응하기 힘들어지므로, 나타나는 내용이 충분하지 않을 때, 깊이 매개 변수를 작게 하여 시작하시는 게 좋습니다.

Massif는 glib에서 어떤 함수가 메모리를 할당하는지 확인할 때 쓸만합니다. 보고서에서 불필요한 함수 포출 계층은 없애고 어떤 코드에서 메모리를 할당하는지 명백한 정보를 던져줍니다. glib의 할당 함수는 g_malloc, g_malloc0, g_realloc, g_try_malloc, g_mem_chunk_alloc 입니다. --alloc-fn 옵션을 사용하여 Massif가 이 내용을 언급하게 할 수 있습니다.

그러니까 명령행 입력은 다음과 같아야합니다:

valgrind --tool=massif --depth=5 --alloc-fn=g_malloc --alloc-fn=g_realloc --alloc-fn=g_try_malloc \ --alloc-fn=g_malloc0 --alloc-fn=g_mem_chunk_alloc swell-foop

예제로 스웰 푸프를 활용하겠습니다. valgrind가 CPU를 에뮬레이션하므로 매우 느릴 수 있음을 미리 경고합니다. 상당한 메모리가 필요합니다.

# 결과 해석

Massif의 그래픽 출력 내용은 그 자체로 상당히 많은 내용을 설명합니다. 각각의 띠에서는 하나의 함수를 여러 번에 걸쳐 호출하여 메모리를 할당했음을 나타냅니다. 어떤 부분이 메모리를 많이 차지하는지 확인하고 나면, 보통 최상단의 두꺼운 부분이 자세한 내용이 담긴 텍스트 파일을 통해 확인해야 할 부분입니다.

텍스트 파일은 섹션 계층별로 정리된 상태이며, 가장 윗 부분은 공간시간별 내림 차순으로 나타낸 최악의 메모리 사용 주체를 나타냅니다. 이 부분 아래는 콜 스택을 따라 내려가 처리한대로 자세하게 결과를 찍어내려간 부분입니다. 위 명령의 출력 결과를 활용하여 결과를 나타내보겠습니다.

## 최적화하지 않은 스웰 푸프 프로그램의 Massif 출력.

다음 그림은 Massif에서 보통 포스트스크립트 형식으로 출력한 모습을 보여줍니다. 스웰 푸프(버전 2.8.0)을 한 번 하고 빠져나왔을 때의 결과입니다. 포스트스크립트 파일 이름은 massif.12345.ps와 같고, 텍스트 파일 이름은 massif.12345.txt와 같습니다. 가운데 있는 숫자는 시험삼아 동작한 프로그램의 프로세스 ID입니다. 실제로 이 예제 동작을 해보면 각기 다른 두 가지 버전의 다른 번호를 가진 파일이 나오는데, 스웰 푸프는 두번째 프로세스로 시작하고, Massif가 그 프로세스 동작을 따라가기 때문입니다. 두번째 프로세스는 매우 적은 메모리를 사용하므로 무시하겠습니다.

그래프 상단을 보시면 gdk_pixbuf_new 이름이 붙은 두꺼운 노란 띠가 보입니다. 아마도 이상적인 최적화 대상이 아닐까 싶은데, gdk_pixbuf_new를 어디서 호출하는지 찾아야합니다. 텍스트 파일 상단은 다음과 같습니다:

명령: ./swell-foop

== 0 =========================== Heap allocation functions accounted for 90.4% of measured spacetime

Called from: 28.8% : 0x6BF83A: gdk_pixbuf_new (in /usr/lib/libgdk_pixbuf-2.0.so.0.400.9)

6.1% : 0x5A32A5: g_strdup (in /usr/lib/libglib-2.0.so.0.400.6)

5.9% : 0x510B3C: (within /usr/lib/libfreetype.so.6.3.7)

3.5% : 0x2A4A6B: __gconv_open (in /lib/tls/libc-2.3.3.so)

'=' 기호가 있는 줄은 스택 추적 단계 깊이 진입 단계를 나타내며, 지금 같은 경우 최상단에 있습니다. 이후 공간시간 사용량 내림 차순으로 최대 사용 주체가 나타납니다. 공간시간은 메모리 사용량과 메모리 점유 시간의 곱입니다. 그래프 띠의 영역과 관련이 있습니다. 파일의 이 부분에서는 우리가 이미 알고 있는 내용을 알려줍니다. 공간시간의 대부분은 gdk_pixbuf_new 함수 호출이 차지합니다. gdk_pixbuf_new 호출 주체를 찾으려면 텍스트 파일을 더 살펴 내려가보아야 합니다:

== 4 =========================== Context accounted for 28.8% of measured spacetime 0x6BF83A: gdk_pixbuf_new (in /usr/lib/libgdk_pixbuf-2.0.so.0.400.9) 0x3A998998: (within /usr/lib/gtk-2.0/2.4.0/loaders/libpixbufloader-png.so) 0x6C2760: (within /usr/lib/libgdk_pixbuf-2.0.so.0.400.9) 0x6C285E: gdk_pixbuf_new_from_file (in /usr/lib/libgdk_pixbuf-2.0.so.0.400.9)

Called from: 27.8% : 0x804C1A3: load_scenario (swell-foop.c:463)

0.9% : 0x3E8095E: (within /usr/lib/libgnomeui-2.so.0.792.0)

and 1 other insignificant place

첫 줄에서는 스택 추적 깊이가 4단계 내려갔음을 나타냅니다. 그 하단에는 gdk_pixbuf_new를 호출한 함수 호출 목록을 나타냅니다. 마지막은 그 다음 단계에서 이 함수를 호출한 함수 목록을 나타냅니다. 물론 1, 2, 3 단계의 항목도 있지만 GDK 코드를 따라 스웰 푸프 코드를 타고 내려갔을 때 도달하는 첫 단계입니다. 이 목록에서, 문제의 코드가 load_scenario에 있음을 볼 수 있습니다.

이제 우리가 살펴볼 수 있는 어떤 코드 부분이 공간시간을 사용하며 왜 점유하는지 알았습니다. load_scenario가 파일에서 픽셀 버퍼를 불러온 후 메모리를 해제하지 않는 부분을 살펴볼 차례입니다. 코드에 문제가 있다는걸 알았으니, 이제 고쳐볼 수 있습니다.

# 결과 동작

공간시간을 줄이는게 좋겠지만, 공간시간을 줄이는덴 두가지 방식이 있으며 이 두가지 방식이 동일하진 않습니다. 메모리 할당량을 줄이거나 할당 시간을 줄일 수 있습니다. 두 프로세스가 동작 중인 모델 시스템을 잠깐 살펴보도록 하겠습니다. 두 프로세스는 실제 메모리 전체를 사용하는데, 전체 메모리를 사용한다면 시스템에서는 디스크 상 스왑 공간을 활용하고, 모든 프로그램의 동작이 느려집니다. 분명하게도 두 프로그램의 인자로 각 프로세스의 메모리 사용량을 줄인다면, 굳이 스왑 공간을 활용하지 않고도 두 프로세스가 아무일 없이 존재할 수 있겠죠. 대신에 두 프로그램의 인자로 메모리 할당 시간을 줄이면 두 프로그램이 함께 동작하겠지만, 대용량 메모리 사용 주기가 겹쳐지지 않을 때만입니다. 결국, 메모리 할당량을 줄이는게 최선입니다.

안타깝게도, 최적화 방안의 선택 부분은 프로그램의 요구 부분에서 필요로합니다. 스웰 푸프의 픽셀 버퍼 데이터의 크기는 게임 그래픽의 크기로 셜정하며 쉽게 줄일 수 없습니다. 다만, 메모리에 불러오는 시간을 확실히 줄일 수는 있습니다. 하단의 그림에서는 픽셀 버퍼에서 불러오던 그림을 X 서버에서 불러오도록 바꾸고 난 스웰 푸프의 Massif 분석을 보여줍니다.

## 최적화한 스웰 푸프 프로그램의 Massif 출력.

gdk_pixbuf_new의 공간시간 사용량은 이제 몇 번의 급격한 변화량 만을 보여주는 얇은 띠의 형상을 보여줍니다(16개의 띠로 줄어들었고 마젠타 색으로 채워졌습니다). 게다가, 기타 부분의 메모리 할당을 처리하기 전 순간 값 상승이 일어나면서 최대 순간 메모리 사용량이 200kB 가량 떨어졌습니다. 이와 같이 프로세스 둘을 동시에 실행하는 경우, 동시 순간 메모리 사용량의 급격한 변화가 일어나기 때문에, 스와핑의 위험성이 조금 줄어들 수 있습니다.

더 잘 할 수 있겠죠? Massif의 간단한 실험 출력에서 g_strdup이 새 주요 원인임이 나타납니다.

명령: ./swell-foop

== 0 =========================== Heap allocation functions accounted for 87.6% of measured spacetime

Called from: 7.7% : 0x5A32A5: g_strdup (in /usr/lib/libglib-2.0.so.0.400.6)

7.6% : 0x43BC9F: (within /usr/lib/libgdk-x11-2.0.so.0.400.9)

6.9% : 0x510B3C: (within /usr/lib/libfreetype.so.6.3.7)

5.2% : 0x2A4A6B: __gconv_open (in /lib/tls/libc-2.3.3.so)

좀 더 가까이 살펴보면 여러 곳에서 호출했음이 나타납니다.

== 1 =========================== Context accounted for 7.7% of measured spacetime 0x5A32A5: g_strdup (in /usr/lib/libglib-2.0.so.0.400.6)

Called from: 1.8% : 0x8BF606: gtk_icon_source_copy (in /usr/lib/libgtk-x11-2.0.so.0.400.9)

1.1% : 0x67AF6B: g_param_spec_internal (in /usr/lib/libgobject-2.0.so.0.400.6)

0.9% : 0x91FCFC: (within /usr/lib/libgtk-x11-2.0.so.0.400.9)

0.8% : 0x57EEBF: g_quark_from_string (in /usr/lib/libglib-2.0.so.0.400.6)

and 155 other insignificant places

최적화를 열심히 한 결과 반환 결과가 줄어들었음이 보입니다. 그래프에서 몇가지 가능한 접근 방안을 귀띔해줍니다: "other" 및 "heap admin" 부분이 좀 넓습니다. 즉, 여러 곳에서 자잘한 할당이 여러 번 일어났습니다. 이 문제를 해결하는건 좀 어렵지만 한데 모아둘 수 있다면 제각각의 할당 용량이 커질 수 있으며 "heap admin" 제반 처리량을 줄일 수 있습니다.

# 경고

살펴봐야 할 몇가지 부분이 있습니다. 우선 공간시간은 백분율로만 나타나며, 전체 프로그램 크기를 비교하여 메모리 사용량이 가치가 있는지 판단해야합니다. 그래프의 수직축은 메모리 사용량을 킬로바이트 단위로 나타내며, 이 판단에 도움을 줍니다.

두번째로, Massif에서는 프로그램에서 사용하는 메모리만 보여줍니다. pixmap과 같은 자원은 X 서버에서 쥐고 있으며 Massif에서는 신경쓰지 않습니다. 스웰 푸프 예제에서, 실제로는, 클라이언트 측의 픽셀 버퍼와 서버 측의 픽셀 매핑으로 메모리 소모 추세를 옮겼을 뿐입니다. 꼼수를 썼긴 하지만 어쨌거나 성능상 이득을 얻었습니다. X 서버의 이미지 데이터를 유지하면 그래픽 루틴을 더 빠르게 동작할 수 있게 하며, 자체 프로세스 통신을 줄일 수 있습니다. 또한, 픽셀 매핑을 통해 gdk_pixbuf에서 사용하는 32비트 RGBA 형식보다 보통 훨씬 작은 자체 그래픽 형식으로 저장합니다. 픽셀 매핑과 다른 X 자원의 동작 결과를 측정하려면 xrestop 프로그램을 사용하십시오.