Python은 메모리를 어떻게 관리할까?

토즐라·2022년 10월 6일
1

1D1Q

목록 보기
7/7
post-thumbnail

Question

Python은 메모리를 어떻게 관리할까? 🤔

00. 개요

메모리 관리란?

많은 프로그래밍 언어들은 특정 명령을 수행하기 위해 객체를 이용합니다. 이 객체는 빠른 접근성을 위해 메모리에 저장되어 쓰입니다. 따라서, 한정된 메모리에서 프로그래밍을 수행하려면, 필요할 때에 메모리를 확보하고, 이용이 끝나면 메모리를 free하는 메모리 관리가 필수적입니다.

C와 같은 초기의 프로그래밍 언어에서는 개발자가 메모리 관리를 직접 해주어야만 했습니다. 어떤 객체를 만들고 나면, alloc 등을 이용해 객체에 대한 메모리를 개발자가 직접 확보하고, 다 이용하고 나면 메모리를 다시 활용할 수 있도록 free시켜주어야 했죠. 이 과정은 두 가지 문제를 야기했습니다.

  1. 메모리를 free하는 것을 잊어버릴 때
    • 메모리를 이용하고 free하지 않으면, 메모리 누수가 발생합니다. 메모리 누수가 발생하면 프로그램이 실행될수록 너무 많은 메모리를 이용하게 되는 문제가 발생합니다.
  2. 메모리를 너무 빨리 free해버릴 때
    • 객체를 이용하는 중에 메모리를 free해버리면 프로그램이 충돌하거나 쓰레기 값을 변수에 이용하는 등의 문제가 발생합니다.

이러한 문제들 때문에 Python, Java와 같은 후발 프로그래밍 언어들은 Garbage Collector 를 이용해 자동으로 메모리를 관리하는 시스템을 채택하게 됩니다.

자동 메모리 관리

자동 메모리 관리를 채택한 프로그래밍 언어를 사용하면, 개발자들이 직접 메모리 관리를 하지 않아도 됩니다. 이의 보편적인 예시로는 프로그램이 객체의 참조 횟수를 기억하고 있다가, 참조 횟수가 0이 되면 메모리를 free하는 Reference Counting이 있습니다.

자동 메모리 관리를 하면, 앞서 언급한 실수로 인한 메모리 누수나 시스템 충돌을 방지할 수 있고, 개발자들로 하여금 메모리 관리에 신경을 쓰지 않고 개발할 수 있게 해 줘 개발 속도를 증진시킬 수 있다는 장점이 있습니다.

하지만, 이에 대한 tradeoff로 reference counting에 필요한 추가적인 메모리와 연산이 필요하다는 단점이 있고, 결정적으로 많은 프로그래밍 언어에서 메모리 관리를 할 때에 grabage collection 작업이 끝나기 전까지 전체 프로그램이 중단되는, stop-the-world 문제가 발생합니다.

그렇다면 Python은 어떠한 방식으로 메모리를 관리하고 있는지 차근차근 알아보겠습니다.


01. Python의 grabage collection

그렇다면 Python은 어떻게 메모리 관리를 할까요?

Python의 모든 것은 CPython이라는 C 언어의 struct로 구현되어 있습니다. 그리고 CPython은 다음 두 가지 방법으로 garbage collection을 수행합니다.

  • Refenence Counting
  • Generational garbage collection

이 두 가지를 방법을 하나씩 살펴보겠습니다.

Reference counting

Python의 주요한 grabage collecting 기법은 앞서 언급한 reference counting입니다. Python의 모든 객체는 PyObject라는 객체를 상속받는데, PyObject 객체는 다음과 같은 C 구조체로 구현되어 있습니다.

struct _object {
    _PyObject_HEAD_EXTRA
    Py_ssize_t ob_refcnt;
    PyTypeObject *ob_type;
};

여기서 ob_refcnt 변수가 바로 특정 객체가 얼마나 많이 참조되는지를 의미하는 변수입니다. 즉, Python의 모든 객체는 자기가 얼마나 참조되는지를 내부 변수를 통해 기억하고 있는 것입니다. ob_refcnt 는 객체가 참조될 때마다 1씩 늘어나고, 참조가 해제될 때마다 1씩 감소합니다. 0이 되면 Python은 해당 객체에 대한 메모리를 free합니다.

다른 언어와는 다르게, 소스코드로 따로 Python의 reference counting을 해제할 수 없습니다.

Generational garbage collection

Generational GC를 이해하기 위해 먼저 간단한 예시를 살펴보겠습니다.

dummy = [] # 1
dummy.append(dummy) # 2
del dummy # 3

위의 코드는 세 가지 단계로 이루어져 있습니다.
1. dummy란 list를 만들고 (ob_refcnt +=1)
2. dummy에 dummy를 원소로 추가하고 (ob_refcnt+=1)
3. dummy를 삭제했습니다. (ob_refcnt -= 1)


이 때, 우리는 dummy를 더 이상 이용할 수 없습니다. 하지만, 해당 객체의 ob_refcnt 변수가 0이 되지 않아, 프로그램은 이 객체가 할당된 메모리를 free하지 않습니다.

이러한 문제를 reference cycle이라고 하며, 이 문제는 reference counting으로는 해결할 수 없습니다. 이 문제를 해결하기 위해 Python은 generational GC를 이용합니다.


Generational GC에 대해서 먼저 이해해야 할 두 가지 주요 개념이 있습니다.

  1. Generation

    Python의 GC는 메모리의 모든 객체를 추적합니다. 새로운 객체는 1세대 GC에서 life를 시작합니다. Python이 이 세대에서 garbage collecting을 수행하고 객체가 살아남으면, 두 번째 세대로 넘어갑니다. Python의 GC는 총 3세대로 구성되어 있으며, 객체는 현재 세대의 garbage collecting 프로세스에서 살아남을 때마다 다음 세대로 넘어갑니다.

  1. Threshold

    각 세대마다 GC는 객체의 수에 대한 임계값(threshold)를 갖습니다. 만약, 객체의 수가 임계값을 넘어간다면, GC는 collection 프로세스를 수행합니다. 이 프로세스에서 살아남는 객체 역시 다음 세대로 넘어갑니다.

Generational GC를 이용하면, GC는 내부적으로 세대과 임계값을 기준으로 garbage collection 주기와 잔존 객체 수를 관리합니다. 여기서, 세대가 낮을 수록(만든지 얼마 안 된 객체일수록) 가비지 컬렉션은 자주 일어나게 되는데, 이는 만든지 얼마 되지 않은 객체가 오래된 객체보다 해제될 가능성이 훨씬 높다는 generational hypothesis에 근거합니다.

Reference counting과는 다르게, Generational GC는 임계값을 임의로 설정하는 등, 개발자에 의해 그 방식이 수정될 수 있습니다. 또, garbage collection 프로세스를 임의로 수행시킬수도 있고, 아예 garbage collection이 일어나지 않도록 막을 수도 있습니다.


02. Garbage Collection 튜닝

기본 원칙

가비지 콜렉터는 건들지 말 것!

Python의 철학은 ‘세부 사항을 신경쓰지 않음으로써 개발 생산성을 증진시키는 것’ 이므로, 일반적인 경우에, 가비지 콜렉팅 작업을 개발자가 꼼꼼이 신경쓸 이유는 없습니다. Garbage collecting 프로세스가 제대로 이루어지지 않아 발생하는 문제는 더 고사양의 메모리 등 향상된 컴퓨터 자원을 이용함으로써 해결할 수 있기 때문이죠.

그럼에도 불구하고 건들려면…?

Python에서 유일하게 수정이 가능한 GC는 generational GC입니다. 그렇다면, 이 기능을 수정해서 성능이 좋아진 예시는 없을까요?

세계 최대의 Django 기반 서비스중 하나인 Instagram은 generational GC를 사용하지 않습니다. Instagram은 single compute instance에서 여러 웹 어플리케이션의 인스턴스를 실행하는데, 이 인스턴스들은 하위 프로세스가 상위 프로세스의 메모리를 공유하는 master-slave 메커니즘을 사용합니다. Instagram 개발팀은 slave 프로세스가 생성된 직후 공유 메모리가 금격히 떨어지는 현상을 발견했는데, 이는 generational GC 때문이었습니다.

이에 따라 generational GC의 임계값을 0으로 떨어뜨려 작동하지 않게 만들었더니 slave 프로세스가 10% 더 효율적으로 실행되었습니다.

위와 같이 특수한 경우에는, Generational GC를 작동시키지 않는 것이 오히려 성능을 나아지게 할 수도 있습니다. 하지만, 이런 효과를 바라고 무작정 GC를 건드는 일은 지양해야 합니다.

Python에서 GC를 수동으로 관리하려는 경우, 먼저 응용 프로그램 성능 및 문제를 정확하게 파악하는 것이 중요합니다. 문제를 파악했는데, 위와 같은 GC 문제인게 명확하다면, 그 때 가서 GC를 건드는 것이 좋습니다.


정리

Python은 메모리를 어떻게 관리할까? 🤔

💡 파이썬은 Garbage Collector를 이용해 자동으로 메모리를 관리합니다.
💡 파이썬의 GC 기법으로는 Reference Counter, Generational GC가 있습니다.
💡 파이썬에서 일반적으로 GC를 수동으로 관리하는 것은 위험한 행동입니다!

Reference

https://stackify.com/python-garbage-collection/
https://www.quora.com/What-is-the-generational-hypothesis-in-the-context-of-garbage-collection
https://plumbr.io/handbook/garbage-collection-in-java/generational-hypothesis
https://rushter.com/blog/python-memory-managment/

profile
Work Hard, Play Hard 🔥🔥🔥

0개의 댓글