티스토리 뷰

러스트의 등장과 성공으로 메모리 관리, 스택과 힙 등 이슈가 새로 중요하게 조명받고 있습니다. 러스트 공부에 도움이 되었으면 합니다.

프로그램과 메모리

프로그램은 CPU의 레지스터에 메모리에 있는 데이터를 가져와서 계산하고 바꾸고 다시 저장하는 과정을 통해 수행됩니다. 이러한 load와 store가 컴퓨터의 기본 동작입니다.

그런데 컴퓨터에서 수행중인 프로그램은 아주 많으므로 컴퓨터(OS) 관점에서 볼 때 여러 개 프로그램이 필요한 만큼 메모리를 넉넉하게 줄 수는 없습니다. 그래서 컴퓨터는 각 프로그램이 사용하는 메모리를 제한하고 종류를 나누어 규칙에 따라 사용하도록 하고 있습니다. 그래서 메모리는 정적, 스택, 힙 세 종류로 나누어 집니다. 

정적 메모리는 코드나 데이터가 저장되는 영역입니다. 프로그램이 로드될 때 미리 정해진 크기 만큼 주어지고 프로그램 코드와 필요한 데이터들을 거기에 올려줍니다. 정적 메모리에 저장되는 데이터는 보통 정적변수, 전역변수, 코드에 있는 리터럴값 등이고, 이 메모리는 관리의 대상이 아닙니다. 프로그램 시작과 함께 할당되고 끝나면 소멸될 것이므로 프로그램의 실행 중에는 변동이 없습니다.

프로그램이 실행되는 동안 변화무쌍하게 바뀌는 것은 스택과 힙 영역입니다. 

스택 메모리

프로그램은 주어진 스택 영역 안에서 함수가 호출될 때마다 함수에 필요한 만큼의 메모리를 쌓습니다. 이것이 push/pop 방식이어서 스택 메모리라는 이름이 붙었습니다. 프로그램은 함수 단위로 실행됩니다. 함수가 다른 함수를 호출하고 또 호출하면서 호출 스택이 자라나고 반환될 때 줄어드는 과정을 프로그램과 호출스택에서 설명했습니다. 여기서는 프로그램의 관점에서 스택의 객체가 언제 할당되고 해지되는가를 살펴보겠습니다. 

함수가 호출되면 해당하는 스택 영역이 쌓입니다. 여기에는 지역변수, 매개변수, 기타 함수의 수행에 필요한 메모리 영역이 할당됩니다. 즉 스택 영역에 있는 각 메모리 부분은 프로그램에서는 지역변수나 매개변수에 대응합니다. 이들 변수를 통해 그 메모리에 값을 읽고 쓰게 되는데, 함수가 끝나면 이들 변수와 함께 그 메모리 영역도 사라지게 됩니다. 그래서 스택 메모리는 관리가 매우 쉽습니다. 할당도 해지도 함수와 함께 자동으로 이루어지므로 프로그램에서 할 일은 없습니다. 

스택 메모리를 사용하는데 제약이 몇가지 있습니다. 스택은 함수 호출할 때 자동으로 쌓여야 하므로 컴파일러가 미리 크기를 계산할 수 있어야 합니다. 컴파일러가 크기를 알지 못하는 객체는 스택에 쌓일 수 없습니다. 그래서 C 언어의 지역변수 배열은 크기가 상수여야 합니다. 또한 함수가 끝난 후에도 필요한 데이터라면 스택에 저장할 수 없습니다. 반환값으로 돌려주거나 전역변수에 저장하는 등의 방법으로 함수가 끝난 후에도 남아있도록 옮겨주어야 합니다.

힙 메모리

힙 메모리는 스택과 달리 프로그램이 필요할 때 할당하고 다 쓰면 돌려주는 메모리입니다. 다른 프로그램과 같이 나누어 쓸 수 있는 영역이라고 해서 공유 메모리라고도 합니다. C++이나 자바에서 프로그램이 힙에 메모리를 할당받는 것은 new를 통해 가능합니다. 이 때 프로그램은 크기를 정확히 써서 요청하게 되고 그렇게 받은 메모리에 데이터를 저장하므로서 객체를 생성하게 됩니다. 이때 힙 객체의 위치가 변수에 저장됩니다. C/C++에서는 힙 객체의 주소를 포인터에 저장하고 자바에서는 변수가 참조를 가진다 라고 합니다. 이러한 변수의 주소 또는 참조가 스택에 저장됩니다. 

힙메모리는 공유하는 영역이므로 다 쓰면 돌려주어야 합니다. 또한 스택보다는 훨씬 크기 때문에 이미지나 동영상처럼 많은 메모리를 필요로 하는 것은 힙에 저장해야 합니다. 그리고 이러한 큰 메모리 객체는 프로그램의 실행 중에 계속 필요한 것이 아니고 전체 프로그램의 실행 시간 중에 아주 일부에서만 사용하게 됩니다. 예를 들어 브라우저는 대부분의 경우 텍스트 중심의 화면을 보여주다가 동영상 플레이할 때만 그 메모리를 필요로 하게 될 것입니다. 그래서 프로그램이 실행되는 중에 다쓴 메모리를 어떻게 돌려주느냐의 문제가 중요해 집니다. 

힙 메모리의 수동 해지

C++은 delete를 통해 다 쓴 메모리를 해지합니다. 즉 프로그래머가 판단하여 이제 이 메모리 객체는 더이상 필요하지 않는 시점에 그 메모리를 돌려주게 됩니다. C/C++ 프로그램에서 다쓴 메모리를 해지하지 않고 두는 것을 메모리 누출(memory leak)라고 합니다. 흔히 드는 예로 핵발전소의 열감지 카메라가 있습니다. 핵발전소의 핵연료가 가열되면 폭발하므로 열감지 카메라가 1초마다 열을 감지해서 문제가 있으면 알람을 보내는 프로그램이 있다고 생각해 보죠. 이 프로그램이 1초마다 동작하는 함수에서 메모리를 할당하고 제대로 해지를 안 해서 몇 바이트라도 남았다면? 1초마다 몇 바이트씩 메모리를 잠식하게 됩니다. 그럼 오래지 않아 이 프로그램은 메모리 부족으로 멈추겠지요?

이것을 C/C++ 프로그램에서는 메모리 리크라고 합니다. 요즘은 메모리 리크를 분석해 주는 개발환경도 있고 많이 좋아졌지만 언제 해지할 것인가를 프로그래머가 판단하는 것은 큰 부담입니다. 더이상 필요하지 않을 줄 알고 해지했는데 복잡한 프로그램의 동작에서 나중에 생각지 못한 함수 부분이 불려질 수도 있고 쓰이지 않을 거라고 생각한 변수가 사용될 수 있습니다. 그러면 댕글링참조로 프로그램은 멈추게 됩니다.

메모리 해지를 안하면 메모리 리크, 너무 빨리 하면 댕글링참조... 양쪽이 절벽인데 이에 대한 C/C++의 답은? 니가 잘 알아서 해라 입니다. 

자바의 가비지 콜렉션

C/C++ 프로그램에서는 힙 객체를 할당받아 사용하다가 해지하는 것이 댕글링 참조를 일으켜 프로그램이 죽는 가장 큰 원인이 되었습니다. 또한 해지하지 않아서 생기는 메모리 부족도 문제가 되므로 프로그래머가 메모리 해지를 위해 신경써야 하는 것이 너무 많고 어렵습니다. 그래서 자바는 프로그램의 안전성을 앞세워서 아예 해지를 하지 않는 방법을 생각했습니다. 힙 메모리는 어차피 OS가 관리하는 건데 프로그램이 그걸 해지하느라 고생하지 말고 쓰고 싶은대로 쓰고 놔두면 OS 비슷한 가비지 콜렉터라고 하는 것이 알아서 메모리를 청소해 주겠다는 것입니다.

그럼 어떻게 쓰레기를 찾아서 해지를 해 줄 것인가? 위에서 힙 객체를 할당받으면 그 참조를 스택의 변수에 저장한다고 했죠? 그러니까 가리키는 변수가 없으면(그 지역변수의 함수가 끝나거나 범위를 벗어남) 쓰레기가 되는 것입니다. 그런데 문제는 객체가 또 다른 객체를 가리킬 수도 있다는 점입니다. 객체간의 참조 관계는 힙에 있는 객체들 간의 그래프 형태가 됩니다. 그러면 그 그래프 상에서 스택에 연결되어 있지 않은 모든 객체는 쓰레기가 됩니다. 말이 쉽지 프로그램이 가지고 있는 힙의 모든 객체들 간에 그래프를 모두 훑어야 하는 일이 됩니다. 이런 어려운 일을 GC가 해내고 있는 거죠.

GC의 장점은 프로그래머에게 힙객체의 해지 부담을 갖지 않게 해주는 것입니다. 메모리 좀 넉넉하게 쓰는 대신 개발자의 시간과 노력을 아껴 더 유용한 곳에 쓰게 하는 것이 좋겠다는 것이지요. 이것이 많은 개발자들이 C/C++보다 자바나 파이썬처럼 GC 사용 언어를 선호하는 이유라고 할 수 있습니다. 

그럼 GC에 비해 C/C++처럼 직접 프로그래머가 고생하면서 게다가 안전성까지 위협받는 방법이 아직도 선택되는 이유는 무엇일까요? 그 이유는 성능입니다. GC 방식이 가지는 성능 상의 문제를 생각해 보겠습니다.

힙 객체의 할당은 시스템 호출을 통해 OS에게 요청해야 합니다. 이것 자체가 상당한 성능상 부담을 가져옵니다.

다음은 접근할 때를 생각해 보겠습니다. 자바나 파이썬은 변수가 참조하는 모든 객체를 힙에 생성합니다. (크기를 아는 기본타입 변수는 제외) C/C++은 배열이나 객체를 스택에 할당할 수 있게 해줍니다. 스택 객체는 변수가 사용될 때 바로 값을 읽고 쓸 수 있습니다. 현재 수행 중인 함수의 스택 영역은 캐싱되어 있고 데이터 접근이 빠릅니다. 반면 힙 객체는 참조든 주소든 그것을 스택에서 레지스터로 가져오고 그 주소로 다시한번 메모리를 접근하여 데이터를 가져와야 합니다. 이것을 간접접근이라고 합니다. 메모리 접근이 두번 발생하는 것이지요. 프로그램의 성능은 메모리 접근의 회수로 결정된다고 볼 수 있습니다. 위 그림에서 CPU와 메모리 간에 화살표 부분을 왔다갔다 하는 것은 CPU의 사이클 입장에서 보면 너무 긴 시간입니다. 그러므로 힙 객체는 스택 객체보다 훨씬 더 접근 시간이 길다라고 얘기할 수 있습니다. 

또한 GC는 프로그램이 실행되는 중 어느 시점에 프로그램을 정지시키고 돌아야 합니다. 그리고 이건 시스템 영역이라 프로그램으로서는 언제 도는지 알지 못하고 예상할 수도 없습니다. 일반적으로 GC는 메모리가 부족할 때 동작합니다. 수시로 돌기에는 너무나 부하가 큰 기능이죠. 사양이 낮은 컴퓨터에서는 프로그램이 메모리가 부족해서 점점 느려지다가 어느 순간 프로그램이 멈추고 GC가 도는 것을 느낄 수 있습니다. 

또하나 문제는 GC 방식은 메모리를 제깍제깍 해지하는 언어에 비해 메모리 사용량이 늘어날 수밖에 없습니다. 앞에서 예를 들었던 동영상 플레이 프로그램처럼 아주 많은 메모리를 짧은 시간 사용해야 하는 프로그램에서는 GC 언어는 좋은 선택이 아닙니다. 원자력 발전소의 열감지 카메라 프로그램도 마찬가지죠. 즉 계산량이 많고 성능을 보장해야 하고 메모리 제약이 있는 프로그램은 GC 언어로 작성하기에 적합하지 않습니다. 

그래서 비GC 언어의 성능의 장점은 살리면서 프로그래머의 부담과 안전성의 위협은 없애겠다는 것이 러스트의 메모리 관리의 야심찬 자동 해지입니다. 프로그래머의 부담을 없애고 댕글링참조의 가능성도 없애고 컴파일러가 자동으로 메모리를 즉시 모두 해지해 주겠다고 합니다. GC와 비GC 언어의 이러한 양극단 사이에서 고민해야 했던 상황에서 이것은 정말 놀라운 이야기입니다. It's too good to believe!!

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함