메모리 정리 프로그램 자체를 검색하다가 찾아온 것이라면 다음에 이어지는 포스팅에 C로 표현한 핵심코드, 기존 프로그램 몇개에 대한 링크와 개인적 요구사항에 맞추어 새로 하나 만든 것이 있고, 이 포스팅은 working set자체를 어떻게 해석하는가에 중점이 맞추어져 있다는 것을 미리 밝혀둔다.

윈도우즈의 메모리를 관리/정리/최적화해준다는 프로그램들을 많이 보았을 것이다. 이런 프로그램들은 항상 그 유효성에 관해 논란이 있기 마련이다. 최근에 웹광고를 비켜가는 방법으로 사용하는 가짜 웹서버메모리 사용량을 줄였으면 좋겠다는 개선점 제안을 받고, 이것을 구현하는 과정에서 한동안 잊어버리고 있던 부분을 다시 찾아보고 정리해 볼 기회가 되었다.

메모리 정리 방법

메모리 정리 프로그램은 윈도우즈95가 시장을 지배하던 시절에도 있었다. 당시에 사용되던 방법은 RAM의 용량만큼 메모리 할당을 시도하는 것이었다. 메모리 정리와 전혀 관계없이 보이는 이 작업이 효과가 있는 듯이 보인 이유는 단 하나이다. 운영체제가 메모리 할당 요청에 응하면서 당장 쓰이지 않는 부분을 swap파일로 옮겨버린다. 그 결과로 이 프로그램이 메모리를 모두 돌려주고 종료하면, RAM이 텅 비어버린다. 이 원리를 이해하고 나면, 이 방식은 실제로 메모리를 정리하지 않는다는 것을 금방 알게된다. 자연스럽게 사기논란이 일어났고, 요즘은 초보 프로그래머가 아니면 이 방식을 쓰지 않는다.

다른 방식은 NT계통의 윈도우즈가 시장을 지배하면서 나타난 것으로, 윈도우즈가 내부적으로 사용하는 방법을 이용하는 것이다. 작업관리자로 메모리 사용량을 지켜보다 보면, 어떤 프로그램을 최소화하면 메모리 사용량이 확 줄어버리는 것을 본 적이 있을 것이다. 그것을 모든 프로그램에 적용하자는 것이 요즘 메모리 정리 프로그램의 기본 아이디어이다. 이는 윈도우즈 API로 제공되는 SetProcessWorkingSetSize()EmptyWorkingSet() 함수를 사용한다. 이 방법은 brhttpd를 개선하는데 사용된 방법이기도 하다.

자신의 프로그램의 메모리 사용량을 줄이기 위해서 이 방법을 사용하고자 한다면, 적절한 위치에

SetProcessWorkingSetSize(GetCurrentProcess(), -1, -1);
이렇게 한줄 집어 넣음으로써, 지금까지 사용하던 메모리중 당장 쓰는 것을 빼고는 다 털어버리게 한다. 그 다음에는 page fault 메카니즘을 통해 다시 필요한 부분을 메모리에 올린다. 당연한 얘기이지만, page fault는 프로그램의 실행속도에 영향을 주게 된다. 그럼에도 불구하고 .net환경에서 개발하는 사람에게는 메모리 사용량을 줄이는 방편으로 이 방법이 권장되기도 한다.

문제의 핵심: working set

윈도우즈 API를 사용해서 메모리를 정리한다고 해도 논란이 잦아들지는 않았다. 논란이 아직도 계속되고 있다는 것을 웅변하듯이, 메모리 정리 프로그램 중의 하나인 CleanMem은 홈페이지의 아래 절반을 자기 프로그램이 효과가 있다고 주장하는 것에 할애하고 있다. 이 프로그램은 프로세스마다 EmptyWorkingSet() 함수를 호출하는 매우 단순한 비주얼베이직 프로그램이다. 그러므로 윈도우즈가 효과가 있는 만큼 이 프로그램이 효과가 있다는 주장이 나름대로 설득력이 있다. 그러나, 이 프로그램이 효과가 없다는 주장도 마찬가지로 설득력이 있다. 그 이유는 working set에 대한 해석에 달려있다.

Working set은 MSDN페이지에 잘 설명되어 있듯이 현재 물리적 메모리(RAM)에 상주하고 있는 가상 메모리 부분이다. 작업관리자가 보여주는 메모리 사용량은 바로 이 working set의 양이다. Working set은 다시 공유 가능한 부분과 공유 불가능한 부분으로 나누어진다. 보통, 공유가 불가능한 부분은 현재 프로세스의 데이타이고, 공유가 가능한 부분은 DLL이나 프로세스간 통신을 위한 공유메모리 같이 말그대로 공유하도록 만들어진 부분이다. 공유가 가능한 부분의 대부분은 실제로 공유된다.

Working set의 구분을 보면 바로 떠오르는 의문은, 공유하고 있는 부분을 각각의 프로세스의 메모리 사용량으로 계산하는 것이 정확한 것인가 하는 질문이다. 예를 들어, kernel32.dll같이 모든 사용자 모드 프로그램이 공유할 수 밖에 없는 DLL은 프로세스의 갯수만큼 RAM을 사용하고 있다고 계산되는데, 실제로 점유하고 있는 RAM의 양은 DLL하나에만 해당한다. (그렇지 않다면 DLL이 아니다!) 프로세스가 오로지 하나만 돌아가고 있다면 이 계산은 맞지만, 그렇지 않다면 작업관리자가 보여주는 메모리 사용량은 과다계상되고 있는 것이다.

두번째로 생기는 의문은, working set을 다 비우고 처음부터 다시 구성한다면 과연 효율적으로 메모리 사용량을 재편할 수 있는가 하는 질문이다. 아까의 예를 계속하면, kernel32.dll은 어차피 메모리에 상주하고 있을 것이다. 그렇다면 프로세스 하나의 working set을 비우고 다시 (soft) page fault를 거쳐 kernel32.dll의 필요한 부분만 이 프로세스의 working set에 포함한다면, 메모리 사용량이 줄어들었다고 볼 수 있는가 하는 의문을 가지게 된다.

사례 분석

불행한 일이지만, 정답은 없다. 정답이 있었다면 애초에 논란이 생기지도 않았다. 결국 사용자로서 할 수 있는 일은, 자신이 주로 사용하는 프로그램들이 메모리 정리 프로그램의 효과를 볼 수 있는지 알아보고 결정하는 수 밖에 없다. 개발자라면 자신의 (메모리 누수가 없는) 프로그램의 working set을 분석해보고 SetProcessWorkingSetSize() 함수를 쓸 것인지 결정할 수 있다.

간단한 working set 분석의 예로 VMMap을 이용하여 brhttpd에서 메모리 정리 이전과 이후를 비교해보자. 이를 위해서 brhttpd의 자체 메모리 정리 기능은 사용하지 않도록 설정하고 실행한다.

[Settings]
MemCleanTimeOut=-1
이제 VMMap을 실행하여 brhttpd.exe를 선택하면 다음과 같은 화면이 보인다. 메모리 정리 이전의 VMMap 실행화면 이 화면은 메모리 정리 이전의 사용량을 보여준다. 화면에서 보이듯이 시스템 DLL이 차지하는 양이 꽤 많다. 이제 메모리 정리 프로그램이 하듯이 working set을 비운다. 메뉴에서 Empty Working Set을 선택하거나 Ctrl-E를 누른다. 그리고 나서 새롭게 재편된 메모리 사용량을 보기 위해 Refresh를 선택한다. 메뉴에서 Refresh를 선택하거나 F5를 누른다. 메모리 정리 이후의 사용량은 다음과 같은 화면으로 나타난다. 메모리 정리 이후의 VMMap 실행화면

그림에 나와있는 숫자만 읽어보면 전체 working set이 1680KB에서 188KB로 줄어들었으니 90%에 가까운 효율을 보이는 메모리 절약의 효과가 있다고 볼 수도 있다. 이것을 좀더 자세히 들여다 보기 위해, 메모리 사용량이 차이를 계산해보면 다음의 표와 같다.

EmptyWorkingSet() 함수로 절약한 메모리 양 (단위: KB)
TypeTotal WSPrivate WSShareable WSShared WS
Total-1,492-212-1,280-1,268
Image-1,260-104-1,156-1,144
Mapped File-480-48-48
Shareable-760-76-76
Stack-20-2000
Private Data-88-8800

이 표를 보면 메모리 절약의 효과의 대부분이 공유가능한 working set부분에서 나타났음을 볼 수 있다. 실제로 공유된 부분이 절감된 수치(오른쪽 마지막 열)를 보면 공유 가능한 부분은 실제 공유가 이루어지고 있었다. (12KB의 차이는 brhttpd가 한번만 실행되었기 때문에 그 실행파일 이미지는 공유할 기회가 없어서 생긴 것이다.)

공유가 되고 있었던 부분중에 절약된 부분을 알아보기 위해 VMMap의 아래쪽 창을 보면, 공유가 되고 있었던 Image 영역은 대부분 시스템 DLL이다. 여기서 절약된 1MB 남짓한 부분중에, brhttpd.exe 실행파일에 20KB를 할당했던 것을 필요한 부분 8KB만 남기고 털어버리는 12KB의 절약분을 제외하면, 실제로 RAM에서 없어졌을 것 같이 보이지 않는다. 다시 말하면, brhttpd가 쓰는 부분만 골라서 사용량을 계산했을뿐이지, 실제로 RAM에서 시스템 DLL을 내려서 메모리를 정리했을 가능성이 없다는 것이다. Mapped file이나 shareable 영역도 비슷한 얘기가 이어진다.

이렇게 생각해보면 working set을 비워서 얻게되는 실질적인 절약효과는 공유가 불가능한 working set에서 생긴다는 것을 알 수 있다. 이미 설명한 바와 같이 공유가 불가능한 working set은 프로그램의 데이타이다. 위의 표에서는 image영역, 스택 영역, 그리고 프로그램 데이타 영역에서 메모리 절약이 있다.

위의 예에서 image영역의 비공유 working set의 대부분이 사라진 것은 메모리 정리의 효과가 빛을 발하는 부분이다. 둘째로, 스택영역은 운영체제가 관리하는 부분이므로, 운영체제가 약간 여유를 준 부분을 빡빡하게 만드는 효과가 있다. 하지만, 원래 운영체제에서 메모리를 방만하게 관리하지 않으므로, 절약되는 바이트 수는 미미할 수 밖에 없다. 마지막으로, 순수한 프로그램 데이타에서 절약되는 부분은 working set을 비우는 것의 효과가 제일 중요한 부분이다. (여기에도 환경변수영역같이 운영체제가 생성하는 부분이 포함되기는 하지만, 그 크기는 상대적으로 작다.) 이 부분에는 프로그램을 짤때 메모리의 재사용을 미리 고려할 수 없어서 생기는 (어쩔 수 없는) 낭비도 포함된다. 사용자의 입장에서는 이 부분이 절약되는 것이 가장 기분 좋은 일이다.

brhttpd의 경우, 비공유 working set이 200KB정도 절약되는 것은, 정리 이전에 350KB남짓한 비공유 working set을 사용하고 있었다는 것에 비추어 보면 엄청난 양의 절약이라고 판단된다. 이 판단에 기초하여 working set을 줄일 수 있는 선택이 가능하도록 개선하였다. 그러나, 실제로 절약되는 부분이 200KB 정도에 불과하다면, 몇 GB짜리 RAM의 0.1%도 안되는 미미한 양이라고 생각할 수도 있다. 한편, 절약되는 200KB의 대부분은 빈 페이지를 보여주는 작업을 할 때에 다시 사용된다. 그래서, 메모리 정리에 관한 부분을 아예 신경쓰지 않도록 설정할 수도 있게 지원한다.

아직도 남은 논란

사용자의 입장에서 안 쓰는 프로그램 데이타 영역을 절약하는 것은 기분 좋은 일이라고 이미 말한바 있다. 그런데, 개발자라면 프로그램 데이타가 차지하는 working set이 많이 감소한다는 것을 다른 관점에서 봐야한다.

순수 프로그램 데이타가 상당히 크게 늘어나면서 당장 필요하지 않은 부분이 아주 많다면, 그것은 어디인가 메모리 누수가 있기 때문일 가능성이 매우 높다. 그렇지 않다면, 자체 캐쉬 알고리즘을 사용하고 있는데, 그 구현에 뭔가 헛점이 있다는 말이다. 그렇다면, 지금 당장은 임시방편으로 SetProcessWorkingSetSize() 함수를 이용해 메모리를 정리하더라도, 언젠가는 코드를 꼼꼼히 점검해봐야 하는 상황인 것이다.

한편, 이런 개발자의 관점은 사용자에게 새로운 시각을 제공한다. 프로그램 데이타 관리를 허술하게 해서 메모리 정리 프로그램을 따로 쓰게끔 만드는 응용프로그램을 쓸 것인가, 아니면 꼼꼼하게 짜여진 응용프로그램을 쓰고 메모리 정리 프로그램 같은 것은 잊어버릴 것인가 하는 질문을 해보는 것이 그것이다. 꼼꼼하게 짠 응용프로그램만 쓴다면 메모리 정리 프로그램을 써봐야 별 효과가 없을 것이고, 그렇게 되면 또다시 논란에 불을 지피는 묘한 피드백이 생긴다.

Posted by movsd
,