파이썬의 모든 것은 객체다. 당연하게 들릴 수 있으나 객체 지향 언어라고 하려 모든것이 객체인건 아니다. 예를 들어 자바의 거의 모든것은 객체지만, 원시 자료형 (Primitive Type)은 아니다. 그러나 파이썬은 숫자도 객체다.
>>> 42..__add__(3) # 점을 두개 붙어야 호출 가능하다 45.0
파이썬의 객체는 PyObject로 이뤄져 있다. PyObject는 Include 폴더의 object.h에 다음과 같이 정의 되어 있다.
typedef struct _object {
_PyObject_HEAD_EXTRA
Py_ssize_t ob_refcnt;
struct _typeobject *ob_type;
} PyObject;이중 _PyObject_HEAD_EXTRA매크로는 디버깅에만 사용되므로 무시하자. 따라서 PyObject는 다음의 구조체다.
typedef struct _object {
Py_ssize_t ob_refcnt;
struct _typeobject *ob_type;
} PyObject;파이썬의 주 메모리 관리는 RC로 이뤄진다. RC는 객체가 얼마나 사용되는지 레퍼런스 카운트에 기록한다. 객체가 사용되는 만큼 레퍼런스 카운트를 1씩 증가/감소하며, 레퍼런스 카운트가 0이 되면 메모리에서 해제한다.
#define Py_INCREF(op) ( \
_Py_INC_REFTOTAL _Py_REF_DEBUG_COMMA \
((PyObject *)(op))->ob_refcnt++)
#define Py_DECREF(op) \
do { \
PyObject *_py_decref_tmp = (PyObject *)(op); \
if (_Py_DEC_REFTOTAL _Py_REF_DEBUG_COMMA \
--(_py_decref_tmp)->ob_refcnt != 0) \
_Py_CHECK_REFCNT(_py_decref_tmp) \
else \
_Py_Dealloc(_py_decref_tmp); \
} while (0)
((PyObject *)(op))->ob_refcnt++;
PyObject *_py_decref_tmp = (PyObject *)(op);
if (--(_py_decref_tmp)->ob_refcnt == 0)
_Py_Dealloc(_py_decref_tmp);우선 PyObject의 ob_refcnt 처럼 레퍼런스 카운트를 저장할 공간이 추가적으로 필요하고, 변수를 할당할때도 추가적으로 레퍼런스 카운트를 관리해야 한다.
if A != True:
pass
PyObject *T = Py_True;
Py_INCREF(T);
if (PyObject_RichCompareBool(A, T, Py_NE)) {
}
Py_DECREF(T);변수 A와 True를 비교하기 위해 임시 변수 T를 만들었고, T는 Py_True값을 참조 한다. 아쉽게도 단순히 참조에서 끝나는게 아니라 필요에 따라 Py_INCREF와 Py_DECREF가 호출 된다.
모든 해제된 메모리는 필요 없는 메모리지만, 모든 필요없는 메모리가 해제되는건 아니다.
class Foo:
child = None
name = None
def __init__(self, name):
self.name = name
def __del__(self):
print('Foo("{}")가 메모리에서 제거 되었습니다'.format(self.name))
a = Foo("a") # Foo("a")의 ref count는 1
b = Foo("b") # Foo("b")의 ref count는 1
a.child = b # a.child가 Foo("b")를 참조하므로 Foo("b")의 레퍼런스 카운트는 1 증가하여 2가 된다
b.child = a # b.child가 Foo("a")를 참조하므로 Foo("a")의 레퍼런스 카운트는 1 증가하여 2가 된다
a = 0 # a가 더이상 Foo("a")를 참조하지 않으므로 레퍼런스 카운트는 1 감소해서 1 이다
b = 0 # b가 더이상 Foo("b")를 참조하지 않으므로 레퍼런스 카운트는 1 감소해서 1 이다
# Foo("a")와 Foo("b")모두 접근할 수 없지만, 레퍼런스 카운트가 남아 있으므로 해재되지 않는다`a.child와 b.child의 참조때문에 올라간 레퍼런스 카운트가 존재해 a와 b가 더이상 child에 접근 못 하더라도 해제가 불가능 하다.
이러한 이유로 파이썬은 RC만을 사용하지 않고, 추가적으로 GC도 사용한다.
파이썬의 GC는 gcmodule.c에 정의 되어 있다.
class Foo:
child = None
name = None
def __init__(self, name):
self.name = name
def __del__(self):
print('Foo("{}")가 메모리에서 제거 되었습니다'.format(self.name))
a = Foo("a") # a의 ref count는 1
b = Foo("b") # b의 ref count는 1
b.child = a # a의 ref count는 2
a.child = b # b의 ref count는 2
a = 0 # a의 ref count는 1 감소해서 1 이다 (b.child에 참조가 남아있다)
b = 0 # b의 ref count는 1 감소해서 1 이다 (a.child에 참조가 남아있다)
import gc
gc.collect() # GC를 직접 호출해보자 (원래는 필요한 경우 자동으로 호출 된다)위의 코드를 실행하면 gc.collect()가 더이상 접근할 수 없는 객체를 해재하는 것을 볼 수 있다.
특별한 설정을 하지 않는다면 파이썬은 자동으로 필요할때 gc를 호출해 RC로 해재할 수 없는 객체를 해제해 준다.
GC도 사용하므로 파이썬의 메모리 관리는 아쉽게도 RC하나만 사용하는 것에 비해 추가적인 비용이 든다.
파이썬의 GC는 순환참조를 만들 가능성이 있는 컨테이너 객체를 대상으로 모든 객체를 검사하진 않는다.
실제로, GC는 파이썬 2.0 버전부터 도입 되었으며 당시 자료를 보면 이전 버전에 비해 약 4%의 성능 저하의 원인으로 생각한다고 한다.
이런 이유로 성능에 여유가 없거나, 순환참조를 피할 자신이 있다면 gc.disable()로 GC를 꺼도 된다. 대표적으로 인스타그램은 GC를 사용하지 않는다고 한다
순환참조로 인한 메모리 누수를 피하는 또다른 방법은 약한참조가 있다.
쉽게 설명하면 참조는 하되, 레퍼런스 카운트는 올리지 않는 것이다. 파이썬은 weakref모듈을 통해 약한 참조를 사용할 수 있다.
CPython은 멀티 스레드를 효율적으로 사용하지 못한다.
한 파이썬 프로세스는 한번에 한 스레드만 사용하며, 이를 GIL (Global interpreter lock) 이라고 부른다.
CPython에서 GIL을 제거하려는 시도는 종종 있지만, 아직까지는 GIL이 남아 있다. 그 이유중 하나가 바로 RC 때문이다.
PyPy는 2017년 8월 블로그글 Let's remove the Global Interpreter Lock를 통해 GIL을 제거할 수 있음을 알렸다.
PyPy의 블로그 글에 따르면 CPython에서 GIL을 제거하기 힘든 두가지 이유는 다음과 같다.
- how do we guard access to mutable data structures with locks and
- what to do with reference counting that needs to be guarded.
멀티스레드에서 발생하는 문제중 하나는 여러 스레드가 동시에 한 변수를 수정하려는 문제이다.
멀티스레드를 사용해본 경험이 있다면 lock guard와 mutex등을 보거나 사용해본 점이 있을 것이다.
그렇다면 스레드간 공유하는 객체는 읽기만 가능하고, 수정은 못하게 한다면 되지 않을까?
좋은 접근이지만 RC를 사용하면 쉽지많은 않은 일이다.
import sys
class Foo:
pass
a = Foo()
print(sys.getrefcount(a))
def read_foo(foo):
print(sys.getrefcount(foo)
read_foo(a) # read_foo의 매개변수 foo가 a를 참조 있으므로 Foo의 레퍼런스 카운트는 증가하였다
# read_a 함수가 끝나면 레퍼런스 카운트는 다시 줄어든다
print(sys.getrefcount(a))getrefcount는 객체의 레퍼런스 카운트 갯수를 구하는 함수다.
우리가 생각한 레퍼런스 카운트 횟수보다 좀 더 많을 텐데 문서의 내용에 따르면, getrefcount함수를 실행하기 위해 매개변수로 참조 카운트가 증가 하여 그렇다.
내용으로 돌아가면 read_foo는 a의 내용을 읽기만 하고 수정하진 않았지만, 매개변수등 참조가 일어 날때마나 obj_refcnt는 증감 한다. 여러 스레드가 동시에 참조를 만든다면 동시수정과 다름 없는 상태가 된다.
추가로 RC를 사용하고도 이 문제를 피할 방법 자체는 있다. 예를 들어 std::atomic이 있다(C++ 문서). atomic은 DB의 Atomic 성질 처럼 순서대로 진행하는걸 보장하는데, 즉 두 스레드가 거의 동시에 레퍼런스 카운트를 변화 하려고 하면, 먼저 요청한 스레드의 요청대로 한 obj_refcnt를 변화시키고, 그 다음 스레드 요청을 처리한다.
대표적인 예로 Rust의 Rc와 ARc가 있다. Rust 자체는 무비용 추상화로 메모리를 관리하지만, 원한다면 RC로 관리되는 객체를 만들 수 있다. 이 경우 싱글 스레드면 Rc를 멀티 스레드면 ARc를 사용하면 되는데, ARc는 레퍼런스 카운트가 atomic 하게 동작한다.
Atomic을 보장하는 것은 추가적인 오버헤드가 있다는 이야기다. 실제로 ARc 문서를 보면, 원자성 보장은 일반 메모리 접근보다 비용이 많이 들고, 싱글스레드라면 비용을 줄이기 위에 Rc를 쓸 수 있다고 언급하고 있다.
현재 파이썬의 대부분은 싱글 스레드 환경이며, 멀티스레드를 위해 싱글스레드 저하를 선택할 것인가는 논쟁의 여지가 있다. 현재로는 파이썬 커뮤니티는 멀티스레드 사용을 위해 싱글스레드 성능이 저하 되는 해결을 받아 들이고 싶어 하지 않는다. 파이썬 위키의 GIL 항목을 인용 하면
The BDFL has said he will reject any proposal in this direction that slows down single-threaded programs.
개인적으로도 멀티스레드를 위해 싱글스레드 성능저하가 심각하다면 받아 들이고 싶지 않다.
실제로 파이썬에 atomic 레퍼런스 카운트를 사용하게되면 약 23%의 성능저하를 보인다고 한다.
단순한 값으로 생각할 수 있는 None도 하나의 객체며 여러곳에서 공유하고 있다. 어려운 문제다.