1. 파이썬의 메모리 관리 방법 - RC 1
1. 레퍼런스 카운팅 (RC)
1-1. 구조 및 내용 분석하기
파이썬은 레퍼런스 카운트 전략을 사용하여 모든 객체에 카운트를 포함합니다.
카운트는 객체가 참조될 때 증가하고, 참조가 삭제될 떄 감소 시키는 방법으로 동작합니다.
/* cpython/object.h */
typedef struct _object {
_PyObject_HEAD_EXTRA // 디버깅 시에만 사용됨
Py_ssize_t ob_refcnt;
PyTypeObject *ob_type; // 파이썬 객체에 대한 정보가 저장된다.
} PyObject;
/* ob_refcnt를 증가시킵니다. */
static inline void _Py_INCREF(PyObject *op)
{
#if defined(Py_REF_DEBUG) && defined(Py_LIMITED_API) && Py_LIMITED_API+0 >= 0x030A0000
// Stable ABI for Python 3.10 built in debug mode.
_Py_IncRef(op);
#else
// Non-limited C API and limited C API for Python 3.9 and older access
// directly PyObject.ob_refcnt.
#ifdef Py_REF_DEBUG
_Py_RefTotal++;
#endif
op->ob_refcnt++;
#endif
}
/* ob_refcnt 0일때 _Py Dealloc(op)을 사용하여 메모리 할당을 제거합니다. */
static inline void _Py_DECREF(
#if defined(Py_REF_DEBUG) && !(defined(Py_LIMITED_API) && Py_LIMITED_API+0 >= 0x030A0000)
const char *filename, int lineno,
#endif
PyObject *op)
{
#if defined(Py_REF_DEBUG) && defined(Py_LIMITED_API) && Py_LIMITED_API+0 >= 0x030A0000
// Stable ABI for Python 3.10 built in debug mode.
_Py_DecRef(op);
#else
// Non-limited C API and limited C API for Python 3.9 and older access
// directly PyObject.ob_refcnt.
#ifdef Py_REF_DEBUG
_Py_RefTotal--;
#endif
if (--op->ob_refcnt != 0) {
#ifdef Py_REF_DEBUG
if (op->ob_refcnt < 0) {
_Py_NegativeRefcount(filename, lineno, op);
}
#endif
}
else {
_Py_Dealloc(op);
}
#endif
}
위는 Cpython의 object.h의 모습입니다.PyObject
라는 이름을 가진 구조체를 한번 살펴봅시다.
공식 DOCS에 의하면 아래와 같습니다.
모든 객체 형은 이 형의 확장입니다. 이것은 파이썬이 객체에 대한 포인터를 객체로 취급하는 데 필요한 정보를 포함하는 형입니다. 일반적인 《릴리스》 빌드에는, 객체의 참조 횟수와 해당 형 객체에 대한 포인터만 포함됩니다. 실제로PyObject로 선언된 것은 없지만, 파이썬 객체에 대한 모든 포인터를PyObject*로 캐스트 할 수 있습니다.Py_REFCNT와Py_TYPE매크로를 사용하여 멤버에 액세스해야 합니다.
https://docs.python.org/ko/3/c-api/structures.html#c.Py_TYPE
공통 객체 구조체 — Python 3.9.5 문서
공통 객체 구조체 파이썬의 객체 형 정의에 사용되는 많은 구조체가 있습니다. 이 섹션에서는 이러한 구조체와 사용 방법에 관해 설명합니다. 기본 객체 형과 매크로 모든 파이썬 객체는 궁극적
docs.python.org
아직 정확한 사용예를 확인하지 않아 잘 모르지만.. 모든 파이썬 객체의 포인터로서의 작용을 하는 것 같습니다.
여기에서 중요한 것은 PY_REFCNT 인데, 이를 사용해서 파이썬은 메모리를 관리한다는 것을 알 수 있습니다.
위의 코드를 살펴 보면 파이썬에서 REF_CNT를 증감 시키는 모습을 볼 수 있는데
static inline void _Py_INCREF(PyObject *op)
{
op->ob_refcnt++;
}
위의 구조체를 살펴보면 PyObject로 들어온 인자 op의 refcnt를 증가 시키는 것을 알 수 있습니다.
다 사용한 메모리는 해제하는 _Py_DECREF는 아래와 같이 정의됩니다.
static inline void _Py_DECREF(const char *filename, int lineno,
PyObject *op)
{
(void)filename; /* may be unused, shut up -Wunused-parameter */
(void)lineno; /* may be unused, shut up -Wunused-parameter */
_Py_DEC_REFTOTAL;
if (--op->ob_refcnt != 0) {
#ifdef Py_REF_DEBUG
if (op->ob_refcnt < 0) {
_Py_NegativeRefcount(filename, lineno, op);
}
#endif
}
else {
_Py_Dealloc(op);
}
}
아직 자세한 내용을 몰라 단순히 참조가 끝난 오브젝트의 카운트를 하나 줄여준다 라고 이해했습니다.
1-2 . 직접 실행해보기
파이썬에서 레퍼런스 카운트를 직접 눈으로 확인하는 방법은sys.getrefcount(x)
에서 볼 수 있다.
import sys
test = "Hello"
sys.getrefcount(test)
위와 같은 코드를 실행해보면 파이썬 오브젝트인 문자열 "Hello"의 refcount가 2 라고 나오는 것을 볼 수 있는데,
getrefcount 에서 한번 더 test를 참조하기 때문에 실제 참조 수 보다 하나 더 크게 나온다고 합니다.
이번에는 PyIncrease와 Pydecrease를 파이썬 코드를 통해 조금 더 직관적으로 나타내보겠습니다.
from sys import getrefcount as rc
class RefCountTest:
def __init__(self):
pass
a = RefCountTest()
print(rc(a)) # 2
b = a
print(rc(a)) # 3
c = b
print(rc(a)) # 4
c = 0
print(rc(a)) # 3
b = 0
print(rc(a)) # 2
RefCountTest라는 클래스와 인스턴스 a를 생성하고 b와 로 a를 참조함으로서 레퍼런스 카운트의 값을 늘렸다.
이후 c와, b를 0으로 초기화 시키면서 레퍼런스 카운트는 감소한다.
이로써, 파이썬 메모리 관리기법 중 하나인 레퍼런스 카운팅에대한 소개를 마쳤습니다. 다음 포스트에서는 레퍼런스 카운팅의 약점인 순환참조와, 이를 보완해줄 가비지콜렉션에 대해 포스트 할 예정입니다.
👀언제든지 내용의 오류와 코드에 대한 지적은 감사하게 받아드리고 있습니다! 처음 접하는 내용이다보니 잘못 이해한 내용이 있을 수 있으므로, 댓글로 지적해주시면 감사하겠습니다!