· 게임 개발 전용 프로그래밍 언어 개발일지 #2
· [약 스압?] 게임 개발 전용 프로그래밍 언어 개발일지 #3
GC 를 개발하기로 하였습니다.
애초에 C++ 개발을 하면서 생기는 메모리 누수도 많았는데, 여기에 추가로 가상 머신 heap에 생성되는 메모리까지 수거해야 했습니다.
누수가 나던 메모리 췍췍들.
우선 GC 에 대해 공부해야 했습니다. 책과 인터넷을 통해 공부하였으며, 가장 많이 참고가 된 책은 'JVM 밑바닥까지 파헤치기' 였습니다.
2장의 '자동 메모리 관리' 에서 구체적으로 JVM 과 그 이전에 구현된 가상 머신이 어떻게 메모리를 관리하였는지,
그리고 GC 의 전체적인 역사를 다루며 GC 에 대해 깊게 공부할 수 있었습니다.
GC 가 원리나 방식 같은걸 세세하게 나누면 복잡하고 많아서, 어떻게 구현해야 할지 막막했습니다.
가장 쉬워 보이는 방식은 reference counting 이었습니다. 이름 그대로 참조한 수를 카운팅 해서 참조 수가 0이 되면 메모리를 수거하는 방식으로,
c++ 의 스마트 포인터가 이러한 방식으로 작동됩니다.
하지만 서로가 서로를 참조하는 순환 참조의 경우, 영원히 서로의 참조 횟수가 1 로 고정되어, 메모리가 수거되지 않는 문제가 있습니다.
순환 참조와 같은 썩 좋지 못한 구조를 자주 만드는 제 처참한 프로그래밍 실력 덕분에 위와 같은 방식이 활용되는 순간 메모리 누수가 날게 뻔했습니다.
그렇다면 그 다음 방식으로 mark and sweep 방식이 있었습니다. 간단하게 설명해서 지금 당장 접근 가능한 메모리를 root memory 로 지정하고, 여기서 접근 가능한 메모리를 그래프 탐색하듯 탐색하여 표시합니다. (mark).
이렇게 표시된 메모리는 접근이 가능한 메모리이므로 남겨두고, 표시가 없는 heap 에 있는 메모리를 수거합니다. (sweep)
JVM 과 같은 VM 에서는 이러한 방식이 사용되어 왔다는 것을 알 수 있었습니다.
위와 같은 순환 참조에 대한 메모리 누수 문제는 없었지만, 수거하기 위해 표시 (mark) 하고 제거 (sweep) 하는 과정이 확실히 더 복잡하고 오래 걸리기 때문에
순간 프로그램이 멈추는 stop the world 문제 (잠깐 어플리케이션의 작동이 멈추는) 문제가 있다고 합니다.
이를 해결하기 위해 young 영역과 old 영역을 나누는 등의 다양한 방식으로 나름대로의 최적화를 통해 현재의 GC 가 되었다고 합니다.
우선 저의 프로그래밍 언어 chestnut 또한 이런 mark and sweep 방식을 활용하기로 하였으며,
성능 개선을 위한 추가적인 기능은 나중으로 미루고 우선은 누수가 일어나고 있는 메모리가 수거되는가를 중점적으로 생각해서 개발하였습니다.
다음 두 줄의 코드는 각각 gc nodes 에 생성된 메모리의 Node 를 추가하고, gc 의 counter 를 하나 올려줍니다.
gc counter 는 현재 동적 할당이 일어난 수를 의미합니다. 이 counter 가 일정 값 이상이 되면 gc 를 작동하게 하였습니다.
사실 이렇게 하는 게 맞는지는 모르겠는데, 뭐 어디 물어볼 곳이 없으니까 그냥 제 나름대로 이렇게 구현하였습니다.
우선 gc node 는 mark 단계에서 각각 접근 가능한 메모리를 파악하기 위해 tree 형태로 heap 의 기본적인 구조를 구현한 그래프 입니다.
다음과 같이 속성 값 (foo.bar = a) 변수가 저장될 때, child 에 Node가 추가됩니다.
이렇게 하면 속성 값을 통해 접근 가능한 메모리 끼리 연결되었습니다.
(킹림판 ON)
이제 이렇게 연결된 node 들을 기반으로 접근 가능한 메모리에 모두 mark 를 시켜줘야 합니다.
코딩 테스트를 하면서 그래프 탐색과 같은 알고리즘을 사용할 일이 많아 크게 어렵지 않게 코드를 작성할 수 있었습니다.
다음 코드는 root memory를 설정하는 코드입니다. root memory 는 위에서 설명한 것처럼 '당장 접근 가능한 메모리' 입니다.
1차적으로 단순하게 생각했을 때 가장 쉽게 접근 가능한 메모리를 우선 선별하고 이 root memory 를 바탕으로 mark 작업을 이어나갑니다.
다음 코드는 BFS를 활용한 mark 작업에 대한 코드입니다. 접근 가능한 메모리를 모두 marked_memories 에 추가합니다.
이렇게 mark 된 정보를 바탕으로 mark 되지 않은 메모리는 모두 수거하였으며, 이와 연결된 Node 또한 수거하였습니다.
생각 이상으로 큰 시행착오 없이 잘 작동하였습니다.
콘솔에 보이시는 것 처럼 접근 가능한 12개 정도의 메모리를 제외한 (아마 배열과 같은 화면에는 보이지 않는 메모리가 포함된 것 같습니다.) 대부분의 접근 불가능한 메모리를 지워주는 모습입니다.
아직 프로세스 메모리가 완전히 잡히지는 못하는 것으로 보아 C++ 자체에서 뭔가 메모리 누수가 나고 있는 것 같아 이건 빨리 잡아야 할 것 같습니다.
감사합니다.
궁금한데 로컬 변수에 할당할때 루트에 추가하는거 같은데 a = new A a = new B 이렇게 같은 변수에 중복 할당하면 어떻게 됨?
a 에 이미 저장되어 있던 A 객체의 주소는 사라지며, Node 간의 연결 또한 끊어지게 됩니다. 이렇게 되면 후에 Mark 단계에서 표시가 되지 않아 자동으로 수거 됩니다.
오~
메모리 누수는 valgrind 같은걸로 잡아보는게 어떰?
GC 가 두개 돌아가면 혹시라도 성능이 낮아질까 우선은 하나 하나 다 잡는 방향으로 가고 있습니다. 사실 처음 코드 짤 때 부터 최대한 동적 할당 줄이는 방향으로 가야 했는데, 너무 생각 없이 코드를 쓴 것 같아 살짝 후회하고 있습니다.
이 댓글은 게시물 작성자가 삭제하였습니다.
아니 k나단 블로
https://s.click.aliexpress.com/e/_DBR5yUp
알리 천원마트인데
어차피 똑같이 중국에서 물건 떼오는건데 굳이 비싸게 살 필요 없겠더라ㅋㅋ