2019년 7월 19일 금요일

190719 python의 garbage collection

리누스 토발즈가 하는 말을 이것저것 찾아보다가, garbage collection을 사용하는 파이썬과 같은 언어는 프로그래머로 하여금 성능이 떨어지는 코드를 짜게끔 한다는 의견을 접했다.

그래서 파이썬의 garbage collection은 어떤식으로 이루어져 있길래 그런 말을 한 건지 궁금해졌다.

https://www.quora.com/What-does-Linus-Torvalds-think-of-Python


garbage collection

C나 C++에서는 프로그래머가 객체의 메모리 할당과 해제 시점을 직접 정해준다.

객체의 메모리 관리는 참조자(reference. 포인터에 해당)를 통해 이루어진다.

그런데 프로그램 흐름 상 reference가 원래 가리키던 객체를 벗어나 다른 객체를 가리키거나, 그 reference를 담고 있는 객체의 메모리가 해제되는 경우가 있다.

즉 reference를 잃은 객체들(Unreachable memory)이 발생하고, 이를 방치하는 것은 memory leak의 원인이 된다.

python garbage collector는 reference가 없는 객체들을 찾아 메모리를 해제하도록 표시해두는 역할을 수행한다.

https://ko.wikipedia.org/wiki/쓰레기수집(컴퓨터_과학)

https://www.decodejava.com/python-garbage-collection.htm


python garbage collection

파이썬에서는 reference counting이 0인 객체(객체를 가리키는 reference가 하나도 없는)에 대해 메모리를 해제하는 방식으로 관리를 한다.

그렇게 하려면 객체의 reference count 값을 주기적으로 업데이트해야 하는데, 이를 파이썬 gc 모듈의 gc.collect()에서 수행한다.

파이썬에서는 객체를 참조하는 모든 연산을 counting 하여 해당 객체의 count 변수 따위에 기록해둔다.

그런데 예를 들어 두 객체 간 순환 참조를 하고 있는 경우, 두 객체 중 하나가 사라지면

다른 객체는 reference counting 값이 0이 아닌데도 자동으로 reference를 잃는 상황이 발생한다.

즉 counting 중 의미 없는 값이 있는 것이고, 이 의미없는 counting 값을 정리하기 위해 gc.collect()를 수행하는 것이다.

gc.collect()는 객체들을 순회하며 순환참조를 하고 있는 객체들을 탐지하고, 이들의 reference count값을 내린다.

https://docs.python.org/3/glossary.html#term-reference-count

https://docs.python.org/3/glossary.html#term-garbage-collection

https://winterj.me/python-gc/#2-가비지-컬렉션의-작동-방식


generational hypothesis

gc.collect()의 호출은 미리 설정된 주기에 의해 이루어지는데,

비교적 생성된 객체에 대해서는 자주 순회하고 생성된지 오래된 객체에 대해서는 훨씬 적은 빈도로 순회한다.

이는 생성된지 얼마 안된 객체일수록 더 금방 쓸모없어진다는 generational hypothesis에 기반한 것이다.

https://plumbr.io/handbook/garbage-collection-in-java/generational-hypothesis


copy-on-write in python multiprocessing module

파이썬의 멀티프로세싱 모듈에서 부모 프로세스가 자식 프로세스를 fork 할 때,

기본적으로는 부모 프로세스에 있는 데이터는 pickle로 만들어 자식에게 전달하는 것으로 되어 있다.

하지만 자식 프로세스가 많아지고 데이터의 크기가 커지면 복사본이 너무 많이 생기기 때문에,

쓰기 요청 시에만 복사본이 생기는 copy-on-write 방식의 참조 가능한 페이지를 자식 프로세스에 전달하는 방식을 써서 중복 데이터를 방지할 수 있다.

https://stackoverflow.com/questions/38084401/leveraging-copy-on-write-to-copy-data-to-multiprocessing-pool-worker-process


copy-on-write

프로세스들끼리 같은 내용의 페이지(가상 메모리의 단위)를 사용해야 하는 경우가 있다.

같은 내용의 페이지라면 읽기 전용의 공유 메모리 형태로 만들어도 상관이 없지만, 각 프로세스마다 다른 쓰기 작업을 하기 위해선 개별 페이지가 필요하다.

그래서 같은 내용의 읽기 전용 페이지에 대해 어떤 프로세스가 쓰기 작업을 요청한다면 page fault가 발생되고,

시스템은 읽기/쓰기가 가능한 페이지를 해당 프로세스에 복사해 준다.

https://obvious.services.net/2011/01/history-of-copy-on-write-memory.html

https://en.wikipedia.org/wiki/Copy-on-write

http://jake.dothome.co.kr/mm-fault/

https://python.flowdas.com/library/multiprocessing.html


인스타그램 개발자들이 밝힌 python garbage collector의 문제점

인스타그램에서는 uWSGI라는걸 사용하는데, 이는 django를 multiprocessing으로 사용할 수 있게 해주어 많은 요청에 대응할 수 있게 해준다.

부모프로세스에서 fork()를 통해 자식프로세스를 생성하고, 자식프로세스에서 사용자의 요청을 처리하는 것 같다.

그런데 이 과정에서 자식프로세스들이 사용하는 공유메모리의 예상보다 사용률이 높지 않은것이 문제였다.

이는 파이썬 garbage collection에서 객체를 담은 링크드리스트를 순회하며 객체의 reference count를 업데이트할 때 링크드리스트 자체를 뒤섞기 때문에

쓰기 작업이 일어나, 읽기 전용 페이지에 page fault가 발생하여 copy-on-write도 같이 수행되기 때문이다. 어찌보면 이는 copy-on-read라고 할 수 있을 것이다.

그래서 인스타 팀에서는 gc.disable()을 통해 garbage collector의 작업을 중단하려 하였는데,

서드파티 등에서 gc.enable()을 호출하고 있었기 때문에 소용없는 짓이 되었고 대신 gc.collect()의 호출 주기 threshold를 무한대로 만들어버리는 gc.set_threshold(0)을 통해 garbage collector의 호출을 막아 공유메모리 사용량을 늘릴 수 있었다고 한다.

https://b.luavis.kr/python/dismissing-python-garbage-collection-at-instagram

https://winterj.me/python-gc/#2-가비지-컬렉션의-작동-방식 <- 링크드리스트를 뒤섞는 과정에 대해 설명한다.


인스타그램 개발자들의 추가 실험, 파이썬 3.7 반영 사항

사용량이 늘고 새로운 기능을 추가함에 따라 메모리 사용량 자체가 증가하였는데,

위 문단에서 언급한 방식은 gc 자체의 이점을 얻지 못하게 되어 gc를 쓰나 안쓰나 비슷한 메모리 사용량을 얻게 되었다고 한다.

그래서 객체를 담은 링크드리스트에서 공유객체만을 제외하고 gc가 작동되도록 변경하여 메모리 사용량을 대폭 줄일 수 있었다고 한다.

이는 파이썬 3.7에 반영되어 있다.

https://b.luavis.kr/python/cow-friendly-python-gc

댓글 없음:

댓글 쓰기