본문 바로가기

3.구현/VC++

Windows에서 메모리 사용량 확인하기

Windows에서 메모리 사용량을 정확히 확인하기는 매우 어렵다. 일반적으로 메모리라고 하면 물리적인 램을 생각하며, 메모리 사용량이라고 하면 물리적 램 사용량이라고 생각하는 경우가 많다. 물론 물리 메모리의 사용량이라고 생각하는게 맞겠지만, 실제로는 가상 메모리라는 것도 존재한다. 운영체제에서 사용하는 메모리량은 물리적 램 크기와는 다르게된다. 메모리 사용량에는 복잡 미묘한 부분이 많다.
아래는 필자가 프로그램 성능 측정을 위해서 찾았던 내용을 중점으로 정리하였다. 위도우즈 위주의 실행 로직에서 사용되는 실제 메모리 량이 얼마인지 측정하기 위한 목적이다.

작성자: http://ospace.tistory.com/,2013.04.20 (ospace114@empal.com)

Windows 메모리 모델

실제 메모리 정보를 얻기 전에 Windows의 메모리 모델을 살펴보도록 하자.
프로세스에서 사용하는 메모리는 크게 3 부분으로 나눌 수 있다.

  1. 스택 영역

    • 지역 변수나 매개 변수데이터를 임시로 사용하는 영역이다.
    • 함수호출에서 사용하는 반환 주소도 여기에 저장된다.
  2. 데이터 영역

    • 고정된 상수 값이나 전역 변수 등이 저장된다.
    • 그리고 동적으로 할당된 영역도 있다.
  3. 코드 영역

    • 실행되는 코드가 저장된다.
    • 읽기 전용으로 되어 있다.

위의 구조는 프로세스 실행에 사용하는 메모리 구분되는 영역이라고 보면 된다. 일반적으로 32bit 환경에서는 4GB가 최대 메모리 영역을 가지며 64bit 환경은 8GB이다. 실제 프로그램인 이중에 절반인 2GB만 사용할 수 있다. XP나 비스타는 3GB까지 가능하다. 나머지는 운영체제에서 필요로 하는 코드를 저장된다. 이 부분에 코드 영역과 스택 영역이 존재한다.
운영체에서 물리 메모리에 접근 가능한 메모리크기는 한정되어 있다. 그리고 실제 물리메모리에는 한계가 있다. 그러나 프로그램에서 사용하는 메모리량은 매우 많이 필요로 한다. 이를 위해 가상 메모리 개념이 도입되었다. 메모리 중에 자주 사용하지 않는 영역은 디스크에 저장하여 메모리에서 제거한다. 그러면 실제 사용가능한 메모리 공간은 늘어난다. 디스크는 물리 메모리보다 더 큰 용량을 가질 수 있다.
즉, 프로그램에서 사용가능한 논리 메모리 용량은 물리 메모리 용량보다 더 크다. 그렇기 때문에 필요한 부분만 물리 메모리에 올려서 사용하고 사용하지 않은 메모리는 디스크에 저장하는 가상 메모리를 사용한다.
그렇기 때문에 물리 메모리에 올려서 사용하는 메모리 공간이 워킹셋(WorkingSet)이라고 하고 디스크에 저장되는 메모리가 가상 메모리(Virtual Memory)가 된다.
이 워킹셋이 실제 사용중이 메모리이지만, 시스템 상황에 따라 워킹셋은 변경될 수 있다. 또한 프로그램 구성에 따사 워킹셋 관리 방식도 달라지게 된다.
이제 실제 메모리 사용량을 구해보자.

GlobalMemoryStatusEx()

  • Header: Windows.h
  • Library: winmm.lib

먼저 찾은 함수있다. 이 함수를 통해 다음과 같은 메모리 정보를 획득할 수 있다.

typedef struct _MEMORYSTATUSEX {
  DWORD     dwLength;
  DWORD     dwMemoryLoad;
  DWORDLONG ullTotalPhys;
  DWORDLONG ullAvailPhys;
  DWORDLONG ullTotalPageFile;
  DWORDLONG ullAvailPageFile;
  DWORDLONG ullTotalVirtual;
  DWORDLONG ullAvailVirtual;
  DWORDLONG ullAvailExtendedVirtual;
} MEMORYSTATUSEX, *LPMEMORYSTATUSEX;

획득 정보

  • dwLength은 초기 입력 값(구조체 크기)
  • dwMemoryLoad: 메모리 사용량(%)
  • ullTotalPhys: 실제 총 물리 메모리 크기
  • ullAvailPhys: 물리 메모리 중에 사용가능한 메모리
  • ullTotalPageFile: 총 페이지 파일 크기
  • ullAvailPageFile: 사용 가능한 페이지 파일 크기
  • ullTotalVirtual: 총 가상 메모리 크기
  • ullAvailExtendedVirtual: 확장 메모리 크기.

여기서 ullAvailPhys를 사용하여 메모리 사용량을 측정해보았다. 사용량 측정은 간단하다.

DWORDLONG GetAvailablePhysicalMemory() {
    MEMORYSTATUSEX mem;
    mem.dwLength = sizeof(MEMORYSTATUSEX);
    if (0==GlobalMemoryStatusEx(&mem)) {
        return 0;
    }
    return mem.ullAvailPhys;
}

//memory usage
DWORDLONG usage = GetAvailablePhysicalMemory();
//something
usage = usage - GetAvailablePhysicalMemory();

측정하고자 하는 범위 전에 값을 측정하고, 범위 끝에서 다시 측정한 값을 빼면 된다. 이전 보다 줄어든 유효 메모리량이 사용 메모리량이 된다.
근데, 이를 사용한 방법에 문제가 발생한다.
짧은 시간에 시스템에 별다른 작업이 없다면, 그나마 근사치 값을 얻을 수 있지다. 그러나, 시스템은 항상 다른 작업들과 복잡하게 동작중이고 측정 시간이 길어지면, 다른 프로그램이 메모리 사용량까지 더해진다.
즉, 신뢰할 만한 결과가 아니게 된다. 그렇기 때문에 어쩔 수 없이 해당 프로세스에서만 사용하는 메모리량 측정이 필요했다.
그래서 찾은 것이 GetProcessMemoryInfo()함수이다.

GetProcessMemoryInfo()

  • Header: psapi.h
  • Library: psapi.lib(PSAPI_VERSION=1)

이 함수를 통해 다음과 같은 정보를 얻을 수 있다.

typedef struct _PROCESS_MEMORY_COUNTERS {
    DWORD cb //구조체 크기
    DWORD PageFaultCount; //페이지 폴트 개수
    SIZE_T PeakWorkingSetSize; //최대 워킹셋 크기(byte)
    SIZE_T WorkingSetSize; //현재 워킹셋 크기(byte)
    SIZE_T QuotaPeakPagedPoolUsage; //최대 페이지 풀 사용량(byte)
    SIZE_T QuotaPagedPoolUsage; //현재 페이지 풀 사용량(byte)
    SIZE_T QuotaPeakNonPagedPoolUsage; //최대 넌페이지 풀 사용량(byte)
    SIZE_T QuotaNonPagedPoolUsage; //현재 넌페이즈 풀 사용량(byte)
    SIZE_T PagefileUsage; //할당된 메모리량(byte)
    SIZE_T PeakPagefileUsage; //최대 할당된 메모리량(byte)
} PROCESS_MEMORY_COUNTERS;

여기서 PeakWorkingSetSize를 사용했다. 주의 할 것은 PagefileUsage가 할당된 메모리량이라고 되어 있는 부분은 물리 메모리와 가상 메모리를 합한 값이다. 물론 실제 사용 중인 메모리 량을 측정하고 싶다면 PagefileUsage를 사용하면 되지만, 실제 물리 메모리 사용량에 근접한 값을 측정하고 싶다면 WorkingSetSize를 사용해야 한다.
이를 통해서 값을 측정해보자.

SIZE_T GetProcessWorkingSetSize() {
    PROCESS_MEMORY_COUNTERS pmc;
    if (!GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof(pmc))) {
        return 0;
    }
    return pmc.WorkingSetSize;
}

//memory usage
SIZE_T usage = GetProcessWorkingSetSize();
//something
usage = GetProcessWorkingSetSize() - usage;

측정하고자 하는 범위에서 WorkingSetSize의 물리 메모리 사용량이 증가되므로 처음에 측정한 값을 범위 끝에서 측정한 값에서 빼면 증가된 WorkingSetSize 값을 얻을 수 있다.
그나마 일정한 보다 정확한 물리 메모리 사용량을 얻을 수 있었다.

결론

시스템 전체의 사용량 측정을 하고 싶다면, GlobalMemoryStatusEx()이 유용할 것이다. 그러나 개별 프로세스의 정보를 측정하고 싶다면 GetProcessMemoryInfo()를 사용하면 된다. 물론 전체도 GetProcessMemoryInfo()를 사용하여 모든 프로세스 결과를 종합하면 되지만, 불필요한 작업을 할 필요는 없을 것이다. 그때 상황에 맞게 원하는 정보를 취하면 될 것이다.
Windows에서는 정확한 물리 메모리 사용량 측정은 매우 어렵다. 움직이는 타겟으로 그때 마다 가변적이고 실행 조건에 따라 변경될 수 있기 때문이다[4]. 지금은 기본 상태에서 길지 않은 제한된 측정을 수행했기 때문에 근사치 값을 얻을 수 있다고 판단하기 때문이다. 대용량과 장시간 측정하는 경우 좀더 많은 상황에 대한 조건을 고려해야할 것이다.
필자도 필요에 의해 정리하다보니 몰랐던 부분도 많았고, 그동안 관과했던 부분도 많았다. 좀더 객관적이고 정확한 수칙을 얻는게 쉽지 않은 일이다.
모두 즐프하세요.ospace.

참조

[1] [General/Windows] 윈도우 메모리 구조, JakeWOrld, 2007.09.27, http://furyheimdall.tistory.com/34

[2] 가상 메모리와 워킹셋의 개념, 태효, 2009.10.08, http://taehyo.egloos.com/3354579

[3] GlobalMemoryStatusEx function, MSDN, http://msdn.microsoft.com/en-us/library/windows/desktop/aa366589(v=vs.85).aspx

[4] The working set of an application is trimmed when its top-level window is minimized, http://support.microsoft.com/kb/293215

반응형