프로그래밍/파이썬 분석

1. 파이썬의 메모리 관리 방법 - RC 1

Cycrypt0 2021. 6. 19. 00:58
728x90

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_REFCNTPy_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으로 초기화 시키면서 레퍼런스 카운트는 감소한다.

 

 

 

이로써, 파이썬 메모리 관리기법 중 하나인 레퍼런스 카운팅에대한 소개를 마쳤습니다. 다음 포스트에서는 레퍼런스 카운팅의 약점인 순환참조와, 이를 보완해줄 가비지콜렉션에 대해 포스트 할 예정입니다.

 

 

 

👀언제든지 내용의 오류와 코드에 대한 지적은 감사하게 받아드리고 있습니다! 처음 접하는 내용이다보니 잘못 이해한 내용이 있을 수 있으므로, 댓글로 지적해주시면 감사하겠습니다!

728x90