개인적으로 회고하는 내용을 담은 글로서 부족할 수 있고 틀릴 수 있습니다.
내용에 대한 피드백은 언제나 환영입니다.
지난 글에서 메모리 누수 탐지 연장도구들을 알아보았다.
그럼 이제 본격적으로 탐지하고 해결해보자.
의심 되는 구간 시작 시점에 한번 메모리량을 측정하고,
구간 끝 시점에 다시 한번 메모리량을 측정해서
그 차이를 구하면 된다.
제일 난감한 상황이다.
메모리 누수의 첫번째 목표는 메모리 할당 해제 되지 않은 객체를 해제 해주는 것이다.
동적할당으로 늘어난 메모리를 OS가 회수해가도록 프로세스 내에서 할당해제 해주는건데
로그를 살펴보니 예상치 못한 구간에서 메모리 증가가 발생한다.
로그 내용은 DvbSub 네임스페이스의 create_region() 함수 시작하고 끝나는 동안
RSS는 584812 kB가 되었고, 이는 34844 kB 증가 했다는 거다.
아래는 실제 코드가 아닌 샘플 코드다.
enum
{
MAX_WIDTH = 7680,
MAX_HEIGHT = 4320,
}
struct region{
uint8_t bit[MAX_WIDTH * MAX_HEIGHT];
}
DvbSub::create_region(){
region_t *r = new region_t;
myLog(ObjectName(), ObjectId(), "mem: new setting 15 START");
int beforeMemUsage = getProcSelfStatusRSS();
for (size_t i = 0; i < MAX_WIDTH * MAX_HEIGHT; ++i){
r->bit[i] = 15;
}
int afterMemUsage = getProcSelfStatusRSS();
myLog(ObjectName(), ObjectId(), "mem: new setting 15 END");
myLog("mem: RES %d kB (%d)", afterMemUsage, afterMemUsage - beforeMemUsage);
}
메모리 증가가 일어날거라면 새로운 구조체를 동적할당 할 때 일어날 것인데 (물론 동적할당할 때도 증가가 있지만 할당 크기가 작아 영향이 미미하다)
for-loop을 돌고 구조체 멤버의 배열에다가 값을 초기화 & 할당할 때도 일어난다?
메모리 할당이 없는데?
강제로 이벤트를 발생시켜 할당해제한 후 다시 할당할 때에도
같은 구간에서 메모리가 늘어난다.
두가지 경우의 수다.
1. 마침 다른 스레드에서 메모리 할당하는 구간인데 타이밍이 절묘하게 겹친 경우.
2. 메모리 파편화로 큰 크기의 배열을 할당할 때, 페이지 단위의 메모리를 찾지 못하는 경우.
응 둘다 아니야
배열
은 data | static 영역에 저장.
std::vector
는 heap 영역에 저장.
배열이 저장되는 영역은 static한 메모리 구역으로서 컴파일타임에 할당되어
프로그램 종료 때까지 남아있다.
따라서 프로그램 런타임 동안 동적으로 할당/해제할 수 없다.
반면, heap 영역은 동적인 메모리 구역으로서
프로그램 실행 중에 OS에 의해 동적으로 할당되고
사용 후 해제될 수 있다.
그리고 배열이 큰 크기 (7680*4320)의 배열이라서 값이 초기화 될 때
Paging
으로 인한 성능 저하가 발생할 수 있다.
큰 크기의 배열을 초기화 하려니 추가 메모리가 필요하고
이 때 메모리 사용량이 증가하는 것이다.
로그에서 32400 kB가 증가하는것으로 나오는데
이는
7680 * 4320 = 33,177,600 (
33,177 kB로 전혀 다른 숫자가 나오지만)
33177600 / 1024 = 32400 KB (KB의 단위는 1024 bytes라는 사실)
계산을 통해 초기화 구간에서 메모리가 증가한다는 것을 알 수 있다.
정적 메모리가 해제되지 않을 이유는 없지만,
정적으로 할당 된 메모리를
동적으로 할당되도록 바꿔서 명시적으로 해제가 되도록 테스트해보려고 한다.
동적으로 해제할 수 있는 vector
로 바꾸면 되겠다.
enum
{
MAX_WIDTH = 7680,
MAX_HEIGHT = 4320,
}
struct region_t{
region (int width, int height)
{
bit = std::vector<uint8_t> (width * height, 0);
}
std::vector<uint8_t> bit; //배열 to vector
}
DvbSub::create_region(){
region_t *r = new region_t (MAX_WIDTH, MAX_HEIGHT);
myLog(ObjectName(), ObjectId(), "mem: new setting START");
int beforeMemUsage = getProcSelfStatusRSS();
//초기화는 생성자에서 해줘요
int afterMemUsage = getProcSelfStatusRSS();
myLog(ObjectName(), ObjectId(), "mem: new setting END");
myLog("mem: RES %d kB (%d)", afterMemUsage, afterMemUsage - beforeMemUsage);
}
바꾸는 김에, 초기화를 애초에 구조체 생성자에서 하도록 했다.
제발 늘어나지 마라, 제발 늘어나지 마라..
오 안 늘어났네...!
했다면 하이파이브 (나도 그랬기 때문에)
사실 초기화를 구조체 생성자에서 하기 때문에 늘어날 이유가 없다.
그니까 안 봐도 되는 로그를 보고 좋아했던거였다.
봐야하는 구조체 생성자 로그를 확인해본다.
늘어나지 않는다!
메모리 증가가 사라진건 아니지만, 증가량이 확실히 줄었다.
이 말인 즉슨, 어딘가 또 메모리가 새어 나가고 있다는 거다. 끝나지 않는 고통
소량의 메모리 증가는 다음 글에서 다뤄보도록 하겠다.
그럼에도 불구하고, 배열
을 vector
로 바꾼 것은 큰 소득이었다.
배열
은 연속되는 공간에 저장되는 특성 때문에
연속된 메모리 블락을 관리하기 위한 오버헤드가 발생하면서
메모리 사용량이 증가한 것으로 보인다.
반면 vector
은 배열보다 성능이 나쁘지만,
동적으로 할당되는 특성으로
메모리 내부 파편화를 줄여준다.
하물며, 동적으로 바뀌는 상황에 맞춰
바뀔 수 있는 크기로 할당/해제 해줘야 하는
vector
자료구조가 적절한 선택이다.
예상과는 달리, 이슈는 큰 크기의 배열을 정적 메모리 할당하면서 발생하는 문제였다.
이 글의 적확한 제목은 사실 <동적메모리 할당을 안했는데 왜 RSS가 늘어나?>이다.
정적 메모리도 프로그램 실행 중에 실제 페이지로 매핑되면서 RSS를 증가시킬 수 있기 때문이다.
메모리 누수를 잡는다는 것을 new
연산과 malloc
함수 호출 부분만 고려해야 한다고 생각했었다.
스택 영역의 변수들은 scope 벗어나면 해제되니까.
그러나 큰 크기의 배열이라는 변수가 있었다.
동적 메모리 할당 외에도 정적 메모리 할당 방식 역시 RSS 증가의 원인이 될 수 있다.
또한, 큰 크기의 배열을 할당 할 때에 메모리 소모가 심하다는 것,
데이터 영역의 메모리는 런타임 동안 해제할 수가 없다는 것을 고려해야 한다.