파이썬의 좋은 점은 파이썬의 모든 것이 객체라는 것
= 동적 메모리 할당이 파이썬 메모리 관리의 기초
Python 메모리 관리자는 Python의 메모리 할당을 관리
Python 메모리 관리자는 "블록"이라는 메모리 청크를 관리합니다.
동일한 크기의 블록 모음이 "풀"을 구성
풀은 힙 = 64 풀에 할당 된 256kB 메모리 덩어리 인 Arena에서 생성
객체가 파손되면 메모리 관리자는이 공간을 동일한 크기의 새 객체로 채 웁니다.
메소드와 변수는 스택 메모리에 작성
오브젝트 및 인스턴스 변수는 힙 메모리에 작성
변수와 함수가 반환 되 자마자 죽은 개체는 가비지 수집됩니다.
Python 메모리 관리자가 반드시 메모리를 운영 체제로 다시 릴리스 할 필요는 없으며 대신 메모리가 Python 인터프리터로 다시 리턴됩니다.
파이썬에는 작은 객체 할당자가있어 추후 사용을 위해 메모리를 할당합니다.
장기 실행 프로세스에서 사용되지 않는 메모리의 증분 예약이있을 수 있습니다.
파이썬은 C 또는 C++과 같이 프로그래머가 직접 메모리를 관리하지 않고 레퍼런스 카운트(Reference Counts)와 가비지 콜렉션(Automatic Garbage Collection)에 의해 관리
파이썬은 내부적으로 malloc()와 free()를 많이 사용하기 때문에 메모리 누수의 위험이 있습니다.
이런 이슈가 있기 때문에 파이썬은 메모리를 관리하기 위한 전략으로 레퍼런스 카운트를 사용합니다.
레퍼런스 카운트 전략이란 파이썬의 모든 객체에 카운트를 포함하고, 이 카운트는 객체가 참조될 때 증가하고, 참조가 삭제될 때 감소시키는 방식으로 작동됩니다. 이때 카운터가 0이 되면 메모리가 할당이 삭제됩니다.
Cpython 코드로 보는 내부 동작
PyObject(파이썬 객체)
Py_INCREF : 레퍼런스 카운트 증가
Py_DECREF : 레퍼런스 카운트 감소
/* 파이썬의 객체 형태 */
typedef struct _object {
_PyObject_HEAD_EXTRA
Py_ssize_t ob_refcnt; /* 레퍼런스 카운트 */
struct _typeobject *ob_type;
} PyObject;
/* ob_refcnt를 증가시킵니다. */
static inline void _Py_INCREF(PyObject *op)
{
_Py_INC_REFTOTAL;
op->ob_refcnt++;
}
/* ob_refcnt 0일때 _Py Dealloc(op)을 사용하여 메모리 할당을 제거합니다. */
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);
}
}
sys 라이브러리의 getrefcount()를 통해 파라미터로 전달된 객체의 레퍼런스 카운트 확인할 수 있습니다.
import sys
class RefExam():
def __init__(self):
print('create object')
a = RefExam()
print(f'count {sys.getrefcount(a)}')
b = a
print(f'count {sys.getrefcount(a)}')
c = a
print(f'count {sys.getrefcount(a)}')
c = 0
print(f'count {sys.getrefcount(a)}')
b = 0
print(f'count {sys.getrefcount(a)}')
"""
OUT PUT:
count 2 # 여기서 2가 출력되는 이유는 getrefcount()의 파라미터값으로 임시 참조되기 때문에 예상과 다르게 1이 아닌 2가 출력
count 3
count 4
count 3
count 2
"""
class RefExam():
def __init__(self):
print('create object')
def __del__(self): # 메모리 할당이 삭제되는 시점에서 실행되는 메서드
print(f'destroy {id(self)}')
a = RefExam()
a = 0
print('end .....')
"""
OUT PUT:
create object
destroy 3112733520336
end .....
"""
# a 변수에 0을 재할당할 떄 __del__이 실행되고 마무리 하는 것을 볼 수 있음
# me 프로퍼티에 자기 자신을 할당합니다.
class RefExam():
def __init__(self):
print('create object')
self.me = self
def __del__(self):
print(f'destroy {id(self)}')
a = RefExam()
a = 0
print('end .....')
"""
OUT PUT:
create object
end .....
destroy 2110595412432
"""
# ‘end …..’를 출력하고 __del__()이 실행되는 걸 확인
# a 변수에 새로운 값을 할당해도 a.me 프로퍼티에 자기 자신을 참조하고 있어 레퍼런스 카운트가 남아있기 때문에 이런 현상이 발생
# 이렇게 되면 레퍼런스 카운트가 0에 도달할 수 없고 할당된 메모리를 삭제할 수 없어 메모리 누수가 발생
# 파이썬은 이 문제를 가비지 콜렉션으로 해결합니다.
파이썬은 객체 관리를 위한 영역을 3가지로 나뉨. 이 영역을 세대(generation)라고 함
파이썬은 세대를 초기화 할 때 _PyGC_Intialize
메소드 호출 (https://github.com/python/cpython/blob/bf8162c8c45338470bbe487c8769bba20bde66c2/Modules/gcmodule.c#L129)
임계값 활용 방법
#define NUM_GENERATIONS 3 /* 3세대로 관리 */
// ...
#define GEN_HEAD(state, n) (&(state)->generations[n].head)
// ...
void
_PyGC_Initialize(struct _gc_runtime_state *state)
{
state->enabled = 1; /* automatic collection enabled? */
#define _GEN_HEAD(n) GEN_HEAD(state, n)
struct gc_generation generations[NUM_GENERATIONS] = {
/* PyGC_Head, threshold, count */
\{\{(uintptr_t)_GEN_HEAD(0), (uintptr_t)_GEN_HEAD(0)\}, 700, 0\}, /** 0세대 초기화 */
\{\{(uintptr_t)_GEN_HEAD(1), (uintptr_t)_GEN_HEAD(1)\}, 10, 0\}, /** 1세대 초기화 */
\{\{(uintptr_t)_GEN_HEAD(2), (uintptr_t)_GEN_HEAD(2)\}, 10, 0\}, /** 2세대 초기화 */
};
for (int i = 0; i < NUM_GENERATIONS; i++) {
state->generations[i] = generations[i];
};
// ...
}
import gc
print(gc.get_threshold())
print(gc.get_count())
"""
OUTPUT:
(700, 10, 10)
(18, 7, 8) // 현재 count상태를 확인하는 것이기 때문에 출력값이 다를 수 있다.
static PyObject *
_PyObject_GC_Alloc(int use_calloc, size_t basicsize)
{
struct _gc_runtime_state *state = &_PyRuntime.gc;
PyObject *op;
PyGC_Head *g;
size_t size;
if (basicsize > PY_SSIZE_T_MAX - sizeof(PyGC_Head)) /* 메모리 할당 */
return PyErr_NoMemory();
size = sizeof(PyGC_Head) + basicsize;
if (use_calloc)
g = (PyGC_Head *)PyObject_Calloc(1, size);
else
g = (PyGC_Head *)PyObject_Malloc(size);
if (g == NULL)
return PyErr_NoMemory();
assert(((uintptr_t)g & 3) == 0); // g must be aligned 4bytes boundary
g->_gc_next = 0;
g->_gc_prev = 0;
state->generations[0].count++; /* number of allocated GC objects */ /* 0세대 증가 */
if (state->generations[0].count > state->generations[0].threshold && /* 임계값 비교 */
state->enabled && /* 사용여부 */
state->generations[0].threshold && /* 임계값 설정 여부 */
!state->collecting && /* 수집중 여부 */
!PyErr_Occurred()) {
state->collecting = 1; /* 수집 상태 활성화 */
collect_generations(state); /* 모든 세대 검사 메소드 */
state->collecting = 0;
}
op = FROM_GC(g);
return op;
}
임계값 검사
이는 가비지 컬렉션 성능 향상을 위한 전략으로 새로 생성된 객체(long_lived_pending)의 수가 기존의 살아남았던 객체(long_lived_total)의 25%를 기준으로 기준치를 초과했을 때 전체 콜렉션이 실행됩니다. 자세한 내용은 pycore_pymem.h문서의 NOTE 주석을 통해 확인할 수 있습니다.
조건문 조건에 만족하는 세대를 collect_with_callback 호출에 파라미터값으로 전달합니다.
collect_with_callback 함수에서 GC의 핵심인 collect를 호출합니다. collect는 내부에서 콜렉션 대상 이하의 세대 카운트를 초기화하고, 도달 가능(reachable)한 객체와 도달할 수 없는(unreachable) 객체를 분류합니다. 그리고 분류된 도달할 수 없는 객체들을 메모리에서 삭제
이 과정은 먼저 레퍼런스 카운트(RC)를 gc_refs에 복사합니다. 그리고 객체에서 참조하고 있는 다른 컨테이너 객체를 찾아 참조되고 있는 컨테이너 객체의 gc_refs를 감소시킵니다. (TIP. 순환 참조는 컨테이너 객체에서 발생할 수 있는 이슈입니다)
즉, 다른 컨테이너 객체에 참조되고 있는 수 A와 현재 레퍼런스 카운트 B를 빼서 B - A > 0 일 경우 도달 가능한 객체(reachable)가 되고, 0 일 때 도달할 수 없는 객체(unreachable)로 분류합니다.
이후 도달 가능한 객체들은 다음 세대 리스트와 병합되고, 도달할 수 없는 객체들은 메모리에서 제거됩니다. 이런 메커니즘을 순환 참조 알고리즘 이라고 합니다
static Py_ssize_t
collect_generations(struct _gc_runtime_state *state)
{
Py_ssize_t n = 0;
for (int i = NUM_GENERATIONS-1; i >= 0; i--) { /** 마지막 세대부터 확인 */
if (state->generations[i].count > state->generations[i].threshold) {
if (i == NUM_GENERATIONS - 1
&& state->long_lived_pending < state->long_lived_total / 4) /** 새 객체 수가 기존 객체 수의 25%를 초과하면 전체 콜렉션 실행 */
continue;
n = collect_with_callback(state, i);
break;
}
}
return n;
}
/** ==================*/
static Py_ssize_t
collect_with_callback(struct _gc_runtime_state *state, int generation)
{
assert(!PyErr_Occurred());
Py_ssize_t result, collected, uncollectable;
invoke_gc_callback(state, "start", generation, 0, 0);
result = collect(state, generation, &collected, &uncollectable, 0);
invoke_gc_callback(state, "stop", generation, collected, uncollectable);
assert(!PyErr_Occurred());
return result;
}
# 안좋은예: + 로 string 잇기
mymsg=[‘line1’,’line2’]
for msg in mymsg :
word += msg+ ' '
# 더 나은 선택
mymsg=[‘line1’,’line2’]
‘ ’.join(mymsg)
# 안좋은 예
msg = ’hello’ + mymsg + ’world’
# 더 나은 선택
msg = f'hello {mymsg} world'
생성기를 사용하면 한 번에 모든 항목이 아닌 한 번에 하나의 항목을 반환하는 함수를 만들 수 있습니다. 즉, 데이터 집합이 큰 경우 전체 데이터 집합에 액세스 할 때까지 기다릴 필요가 없습니다.
class 이터레이터이름:
def __init__(self, items) : # 반복할 자료
self.items = items
def __iter__(self) : # iterator 생성
return self._generator()
def _generator(self) :
for itm in self.items() :
yield itm
파이썬은 전역 변수보다 훨씬 효율적으로 지역 변수에 액세스합니다. 지역 변수에 함수를 할당 한 다음 사용하십시오.
myLocalFunc = myObj.func
for i in range(n) :
myLocalFunc(i)
# 안좋은 예
mylist =[]
for myword in oldlist :
mylist.append(myword.upper())
# 더 나은 방법
mylist = map(str.lower, oldlist)
# 안좋은 예
mylist= []
for shape in [True, False] :
for weight in (1,5) :
firstlist += function(shape, weight)
# 더 나은 방법
from itertools import product, chain
list(chain.from_iterable(function(shape, weight) for weight, shape in product([True, False], range(1, 5))))
__new__
를 덮어 쓰고 메타 클래스를 활용하면 Singleton 및 Flyweight 패턴을 적용 할 때 메모리 관리에 유용하고 안전합니다. class Singleton(type) :
_instances = {}
def __call__(cls, *argsm **kwargs) :
if cls not in cls._instances :
cls.instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
class ConfigDict(dict, metaclass=Singleton) :
def __init__(self) :
super().__init__(self,read_config_file())
@staticmethod
def read_config_file() :
config_file_path = sys.argv[-1]
if not config_file_path.endwith(".yml") :
raise ConfigDictError(message=f"yml file not passed into falsk app but {config_file_path} instead")
return yaml.load(open(str(config_file_path)), Loader=yaml.FullLoader)
cProfile 및 프로파일과 같은 프로파일링 모듈 사용
python -m cProfile [-o output_file][-s sort_order](-m module | myscript.py)