본문 바로가기

python

파이썬의 메모리 관리

파이썬의 메모리 관리

가장 아름다운 하나의 답이 존재한다. - 파이썬의 디자인 철학

파이썬의 메모리 관리 / 할당은 어떤 내부 동작으로 되는 것일까? 하는 의문에서 자료를 찾아보았다. 파이콘에서 파이썬의 메모리 관리에 대한 영상이 있어 보게 되었고 그 영상의 내용을 정리해봤다.

메모리 할당에는 두가지 타입이 있다.

 

1. 정적 메모리 할당

프로그램 컴파일 시 메모리가 할당된다. ex) C / C++

컴파일이란? → 고급 언어로 작성된 프로그램을 컴퓨터가 실행할 수 있는 프로그램으로 변환하는 방식을 의미

메모리 할당 시 타입이 정해져 있다.

“스택”은 정적 할당을 구현하는 데 사용된다. 이 경우 메모리를 재사용할 수 없다.

참조는 스택에 생성된다. 메소드와 변수는 스택 메모리에 저장된다. 메서드가 리턴될 때마다 자동으로 제거된다.

 

2. 동적 메모리 할당

프로그램 런타임에 메모리가 할당된다.

“힙”은 동적 할당을 구현하는데 사용된다. 필요하지 않은 메모리를 비우고 재사용할 수 있다.

힙은 모든 객체와 모든 값을 저장한다. 오브젝트 및 인스턴스 변수는 힙 메모리에 저장된다.

 

 

파이썬의 메모리 할당

파이썬은 C언어로 구현된 고급 프로그래밍 언어

파이썬은 모든것이 객체이다. 파이썬 메모리 관리의 기초는 동적 메모리 할당.

파이썬의 객체 타입에는 mutable과 immutable 두 가지 타입이 존재한다.

 

mutable

  • 변경 가능한 객체
  • 객체 값 변경 시 메모리 재할당 없음(set, list, dict)

 

파이썬의 id 함수를 이용하면 힙에 있는 객체의 주소를 알 수 있다.

mutable이라는 변수에 'Hello' 문자열이 들어있는 리스트를 할당했다. 그 후 mutable에 'World' 문자열을 추가한다. 그 결과 메모리 주소 값이 변하지 않았다. 리스트는 변경 가능한 객체라서 객체 값 변경 시 메모리 재할당이 없기 때문이다.

위의 코드를 그림으로 나타내면 다음과 같다.

 

 

힙에 있는 변수 mutable는 ['Hello', 'World'] 값을 참조하고 있다.

 

 

immutable

  • 변경 불가능한 객체
  • 객체 값 변경 시 메모리 재할당(int, float, srting, turple)

 

변수 immutable에 1000을 할당한다. 그 후 1을 더한다. 그 결과 1000을 할당했을 때 메모리 주소값과 1을 더하고 난 후의 주소 값이 다르다는 것을 알 수 있다. int형은 객체 값 변경 시 메모리 재할당이 이루어지기 때문이다.

 

 

위의 코드를 그림으로 나타내면 다음과 같다.

힙에 있는 변수 immutable은 1000을 참조하고 있다가 객체 값 변경 시 기존에 참조하고 있던 주소값을 릴리즈 후 다른 메모리 주소 값을 참조한다.

 

 

 

모든 인스턴스에서 동일한 메모리 주소 공유하고 있는 키워드들

True
False
None
Ellipsis # 인자를 생략할 때 쓰는 특수 키워드 (...)
Notimplemented

위 다섯개의 객체는 무조건 한 개의 오브젝트만을 유지하고 있다. 즉, 모든 파이썬 프로그램에서 똑같은 메모리 주소를 공유하고 있다. 파이썬 공식 레퍼런스에는 위 5개의 객체를 묶어서 특별히 이름 짓지는 않았다.

 

 

파이썬 메모리 어딘가에 1000을 할당하면 각각의 주소값이 다르게 나오는 것을 알 수 있다.

하지만 True의 주소값은 모두 동일하게 나오는 것을 볼 수가 있다.

파이썬 내부적으로 퍼포먼스 상향이나 관리 용이의 이유로 채택하고 있다.

 

A == None

A is None 

그래서 특정 객체의 값이 None인지 비교할 때는 값을 비교하는 '=='보다 메모리 주소를 비교하는 is를 사용하는 것이 더 빠르다.

 

 

파이썬에서 기본적으로 garbage collection(가비지 컬렉션) 과 reference counting(레퍼런스 카운팅)을 통해 할당된 메모리를 관리한다.

Reference count

특정 메모리 주소를 참조하는 곳(변수, 상수)의 수

0이 되었을 때 객체는 메모리에서 해제되고 더 이상 참조가 없는 객체가 된다. 가비지 컬렉터는 힙을 돌아다니며 이러한 것들을 메모리에서 객체의 할당을 해제시킨다.

 

 

 

Garbage Collection

필요 없는 메모리를 자동으로 해제시킨다.

파이썬의 GC는 Generation으로 나눠서 관리하고 있다. 각 generation마다 변수가 들어갈 수 있는 최대 threshold가 존재하고 있다. 가장 낮은 Generation부터 GC를 돌리기 시작한다. 즉, 최근에 할당된 값들부터 GC를 돌린다. 그 이유는 가장 최근에 할당된 변수가 가장 쓸모가 없기 때문이다.

 

예를 들어서 반복문에서 변수 i를 한 번 사용한 뒤 더 이상 사용할 확률이 낮다. 파이썬 내부적으로 예전에 생성한 변수보다는 최근에 생성된 변수가 더 쓰레기값이 될 확률이 높다고 판단하기 때문에 최근에 생성된 변수부터 GC를 돌린다.

for i in range(1, 10) # 이 때 임시변수 i가 생성된다.

 

 

파이썬에서 GC 관리를 위해 gc라는 모듈을 제공하고 있다. 이 gc 모듈을 이용해서 GC를 키거나 끌 수도 있고 여러 가지 옵션을 줄 수도 있다. generation 0에는 가장 최근에 할당된 변수들이 저장되어 있고 generation 2에는 가장 오래된 변수들이 저장되어 있다.

 

gc.gethreshold()

 

위의 함수를 이용하면 threshold에 저장되는 값의 제한을 줄 수 있다. 예를 들기 위해 threshold의 값을 3으로 제한했다.

 

변수 A에 'alpha'라는 값을 할당하면 GC generation에는 변수 A가 아닌 'alpha'라는 값이 저장된다.

 

generation 0에 3개의 저장공간이 꽉 차있는 상태에서 d = 'delta'를 할당하면 0 세대에 있던 값들은 1세대로 올라가서 저장되고 새로운 값이 0세대에 저장된다.

 

내부 동작

여기서 내부 동작을 살펴보자면 0세대에는 저장공간이 꽉 차있어서 더 이상 저장이 불가능하다. 이때 'PyObject_GC_Malloc'이라는 함수에서 'collect_generation'이라는 함수를 호출한다. 이 함수는 제일 오래된 세대부터 각 세대에 값이 몇 개 들어있는지 체크한다. 체크 후 초과될 세대를 찾는다. 위에서는 0세대의 저장공간이 가득 차 있으므로 0세대에 GC를 돌리라고 명령한다. 0세대의 값들을 모두 불러온 뒤 값들의 레퍼런스 카운트를 체크하기 시작한다. 레퍼런스 카운트가 0일 경우 그 값을 메모리에서 지워버리고 0이 아닌 값들은 바로 위 세대로 올린다. 0세대에 있던 alpha, bravo, charle는 레퍼런스 카운트가 1 이상이기 때문에 1세로 올라가고 새로운 값 delta가 0세대에 저장된다.

성능 향상을 위한 팁

a = 'My'
b = 'World'
c = 'Hello' + a + b + '!'  

위 코드 같은 경우 'Hello'에 a를 더한 뒤 메모리 어딘가에 저장하고 'Hello' + a에 b를 더한 뒤 다른 새로운 메모리에 저장하고 'Hello' + a + b에 '!'를 더한 뒤 또 새로운 메모리에 저장한다. 때문에 성능 저하가 일어난다.

a = f'Hello {a} {b}!' 

파이썬에서 제공하는 문자열 포맷팅을 이용하면 훨씬 더 효율적이고 빠르게 동작한다.

참고

https://blog.winterjung.dev/2018/02/18/python-gc

https://www.youtube.com/watch?v=UwGHc6A0Jq8&list=PLZPhyNeJvHRnoO_m1hH78j0JRj8LgUICN&index=6

'python' 카테고리의 다른 글

set과 frozenset  (0) 2022.04.07
dict & defaultdict  (0) 2022.04.06
제너레이터 함수  (0) 2022.03.06
리스트 컴프리헨션(2)  (0) 2022.03.02
map과 filter  (0) 2022.03.01