파이썬의 메모리 관리
- 파이썬의 좋은 점은 파이썬의 모든 것이 객체라는 것
- 객체가 더 이상 필요하지 않으면 Python 메모리 관리자가 자동으로 객체에서 메모리를 회수
- = 동적 메모리 할당이 파이썬 메모리 관리의 기초
- Python 메모리 관리자는 Python의 메모리 할당을 관리
- 모든 파이썬 객체와 데이터 구조를 포함하는 개인 힙이 있습니다.
- Python 메모리 관리자에는 객체 별 할당자가있어 int, string 등과 같은 특정 객체에 대해 메모리를 명확하게 할당
- 그 아래에서 원시 메모리 할당자는 운영 체제의 메모리 관리자와 상호 작용하여 개인 힙에 공간이 있는지 확인
- Python 메모리 관리자는 "블록"이라는 메모리 청크를 관리합니다.
- 동일한 크기의 블록 모음이 "풀"을 구성
- 풀은 힙 = 64 풀에 할당 된 256kB 메모리 덩어리 인 Arena에서 생성
- 객체가 파손되면 메모리 관리자는이 공간을 동일한 크기의 새 객체로 채 웁니다.
- 메소드와 변수는 스택 메모리에 작성
- 메소드와 변수가 작성 될 때마다 스택 프레임이 작성됩니다.
- 이러한 프레임은 메소드가 리턴 될 때마다 자동으로 제거됩니다.
- 오브젝트 및 인스턴스 변수는 힙 메모리에 작성
- 변수와 함수가 반환 되 자마자 죽은 개체는 가비지 수집됩니다.
- Python 메모리 관리자가 반드시 메모리를 운영 체제로 다시 릴리스 할 필요는 없으며 대신 메모리가 Python 인터프리터로 다시 리턴됩니다.
- 파이썬에는 작은 객체 할당자가있어 추후 사용을 위해 메모리를 할당합니다.
- 장기 실행 프로세스에서 사용되지 않는 메모리의 증분 예약이있을 수 있습니다.
- 파이썬은 C 또는 C++과 같이 프로그래머가 직접 메모리를 관리하지 않고 레퍼런스 카운트(Reference Counts)와 가비지 콜렉션(Automatic Garbage Collection)에 의해 관리
레퍼런스 카운트(Reference Counts)
- 파이썬은 내부적으로 malloc()와 free()를 많이 사용하기 때문에 메모리 누수의 위험이 있습니다.
- 이런 이슈가 있기 때문에 파이썬은 메모리를 관리하기 위한 전략으로 레퍼런스 카운트를 사용합니다.
- 레퍼런스 카운트 전략이란 파이썬의 모든 객체에 카운트를 포함하고, 이 카운트는 객체가 참조될 때 증가하고, 참조가 삭제될 때 감소시키는 방식으로 작동됩니다. 이때 카운터가 0이 되면 메모리가 할당이 삭제됩니다.
- Cpython 코드로 보는 내부 동작
- PyObject(파이썬 객체)
- ob_refcnt(레퍼런스 카운트)
- Py_INCREF : 레퍼런스 카운트 증가
- Py_DECREF : 레퍼런스 카운트 감소
- _Py_Dealloc(op) : 레퍼런스 카운트가 0 이 되었을 경우 메모리 할당 삭제
/* 파이썬의 객체 형태 */ 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); } }
- PyObject(파이썬 객체)
- 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에 도달할 수 없고 할당된 메모리를 삭제할 수 없어 메모리 누수가 발생
# 파이썬은 이 문제를 가비지 콜렉션으로 해결합니다.
가비지 콜렉션(Automatic Garbage Collection)
- 설명에 들어가기에 앞서 알아두셔야 하는 것은 레퍼런스 카운트도 가비지 콜렉션이(GC)라고 부릅니다. 이를 구분하기 위해서 순환 참조 이슈를 해결하기 위해 구현한 가비지 콜렉션을 ‘Automatic garbage collection’이라고 부릅니다.(Python Doc 1.10.Reference Counts)
- 파이썬에서는 Cyclic Garbage Collection을 지원합니다. 이는 순환 참조 이슈를 해결하기 위해 존재하며, 참조 주기를 감지하여 메모리 누수를 예방합니다.
- Generational Hypothesis - 가비지 콜렉션의 가설기반
- 대부분의 객체는 생성되고 오래 살아남지 못하고 곧바로 버려지는 것
- 젊은 객체가 오래된 객체를 참조하는 상황은 드물다
- 이를 통해 유추할 수 있는 것
- 메모리에 존재하는 객체를 오래된 객체(old)와 젊은 객체(young)로 나눌 수 있는데, 대부분의 객체는 생성되고 곧바로 버려지기 때문에 젊은 객체가 비교적 더 많이 존재
- 젊은 객체에 대부분의 객체가 존재하니, 가비지 컬렉터가 작동 빈도수를 높여 젊은 객체 위주로 관리해주는 것
세대관리
- 파이썬은 객체 관리를 위한 영역을 3가지로 나뉨. 이 영역을 세대(generation)라고 함
- 파이썬은 세대를 초기화 할 때 _PyGC_Intialize 메소드 호출 (https://github.com/python/cpython/blob/bf8162c8c45338470bbe487c8769bba20bde66c2/Modules/gcmodule.c#L129)
- 코드를 초기화할 때 임계 값(threshold)을 각 700, 10, 10으로 초기화하고 카운트(count)를 0, 0, 0으로 초기화
- 이를 python 런타임 환경에서는 gc.get_threshold()와 gc.get_count() 를 통해 가능
- 임계값 활용 방법
- 객체가 생성될 때 0세대의 카운트 값이 증가
- 증가될 때 0세대의 카운트와 임계값을 비교하여 만약 카운트가 임계 값보다 클 때 쓰레기 수집을 실행하고 0세대는 초기화
- 0세대의 살아남은 객체는 다음 1세대로 옮겨지고 1세대의 카운트(count)는 1 증가합니다. 이런 방식으로 젊은 세대(young)에서 임계 값이 초과하면 오래된 세대(old)로 위임하는 방식으로 3세대 영역으로 관리
#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상태를 확인하는 것이기 때문에 출력값이 다를 수 있다.
좀더 자세히 들여다보기
- 파이썬은 객체가 생성될 때 _PyObject_GC_Alloc 메소드를 호출
- generations[0].count (0세대)를 증가
- 이후 현재 상태를 확인(우측 주석 확인)하여 조건에 충족하지 않을 때 collect_generations을 호출
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;
}
효율적인 파이썬 코드의 모범사례
리스트 원소를 출력할 때 join 하기
# 안좋은예: + 로 string 잇기
mymsg=[‘line1’,’line2’]
for msg in mymsg :
word += msg+ ' '
# 더 나은 선택
mymsg=[‘line1’,’line2’]
‘ ’.join(mymsg)
문자열에 + 연산자 피하기
# 안좋은 예
msg = ’hello’ + mymsg + ’world’
# 더 나은 선택
msg = f'hello {mymsg} world'
제네레이터(Generaotr) 사용하기
생성기를 사용하면 한 번에 모든 항목이 아닌 한 번에 하나의 항목을 반환하는 함수를 만들 수 있습니다. 즉, 데이터 집합이 큰 경우 전체 데이터 집합에 액세스 할 때까지 기다릴 필요가 없습니다.
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)
itertools를 사용하여 원치 않는 루프 제거
# 안좋은 예
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 덮어 쓰기 및 메타 클래스 활용
- __new__를 덮어 쓰고 메타 클래스를 활용하면 Singleton 및 Flyweight 패턴을 적용 할 때 메모리 관리에 유용하고 안전합니다.
- 예를 들어 다음은 Yaml 파일을 읽는 dict 객체의 예입니다.
- 메타 클래스는 일단 정의되면 싱글 톤 디자인 패턴이므로 시스템의 어느 곳으로나 가져 와서 다시 정의 할 수 있으며 인터프리터는 초기 객체 만 가리 킵니다.
- 메모리 공간을 줄이고 안전을 보장합니다. 팀의 다른 개발자가 아무리 주니어에 있더라도 중복 된 객체는 발생하지 않으므로 시스템의 한 부분에서 dict를 변경하지 못하게하고 다른 부분에서 다른 dict를 참조하지 않습니다.
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)
'개발 > python' 카테고리의 다른 글
파이썬 클래스와 인스턴스 이해하기: 개념, 예제 코드 및 활용 방안 소개 (0) | 2023.07.08 |
---|---|
파이썬 데코레이터의 원리 및 활용법: 코드 가독성과 효율성 향상 시키기 (0) | 2023.07.08 |
파이썬 GIL(Global Interpreter Lock) 이해하기: 병렬처리에 관한 성능 제한과 해결 (0) | 2023.07.08 |
파이썬 제너레이터와 이터레이터: 차이점, 장단점, 사용법 (0) | 2023.07.08 |
[파이썬] Python coding convention (파이썬 코딩 컨벤션) (0) | 2022.04.18 |
[파이썬] 코딩테스트용 문법 정리 (0) | 2022.01.04 |