파이썬 또한 프로그래밍 언어이기 때문에 컴퓨터 메모리 (RAM) 상에 데이터를 저장하고 (write) 읽는 (read) 작업을 수행해야합니다. 파이썬 프로그램이 수행된다면 데이터를 저장할 메모리 공간을 할당받고 더 이상 쓰이지 않는 메모리 공간을 해제하는 작업이 필요하다는 것이죠. 그렇다면 파이썬 프로그램에서 물리 메모리 상의 특정 공간까지 데이터 read/write는 어떤 방식으로 이루어질까요?
파이썬 코드가 컴퓨터 상에서 실제로 수행되기 위해서는 컴퓨터가 이해할 수 있는 특정한 종류의 언어로 먼저 변환되어야 합니다. 파이썬은 C 언어로 구현된 CPython으로 구현되어 있고 CPython은 파이썬 코드를 컴퓨터가 이해할 수 있는 어셈블리 언어와 비슷한 bytecode로 파이썬 코드를 컴파일합니다. Bytecode는 CPython 인터프리터에서 사용되는 파이썬 프로그램의 내부 표현 양식으로 파이썬 프로그램 실행 시 생성되는 .pyc 파일이나 __pycache__ 폴더가 이에 해당됩니다. (.pyc 파일은 컴파일된 bytecode를 캐쉬하여 다음 프로그램 실행이 더 빠르게 하는 용도입니다.) 컴파일된 bytecode는 순수히 소프트웨어로만 구성된 파이썬 가상머신 (virtual machine) 에서 실행되게 됩니다. 참고로 파이썬 구현체는 C로 이루어진 CPython 이외에도 닷넷 bytecode로 컴파일하는 IronPython, 자바 bytecode로 컴파일하는 Jython 등이 있습니다.
따라서 CPython 안에서 파이썬의 memory management나 객체가 C 언어로 구현되어있는데, CPython은 파이썬의 모든 객체를 PyObject 라고하는 C 구조체 스타일로 저장합니다. (C 언어에서 구조체란 객체지향프로그래밍 언어에서 속성만 존재하는 클래스와 비슷한 개념으로 서로 다른 데이터 타입을 묶는 용도로 사용됩니다.) 파이썬의 모든 객체의 기본이 되는 PyObject에는 1) 객체가 얼마나 참조되었는지를 카운트하는 ob_refcnt와, 2) 타입 구조체를 향하는 ob_type 포인터로 구성되어있고 각 객체는 메모리 공간 할당을 위한 allocator와 공간 해제를 위한 deallocator를 개별적으로 가지고 있습니다. 또한 각 객체가 참조되는 횟수인 reference counter가 0이 되면 deallocator 함수가 호출되어 메모리를 해제합니다.
만약 서로 다른 두 개의 프로그램이 같은 메모리 공간에 접근하는 상황이라면 어떻게 될까요? 특히 여러 쓰레드를 사용하는 경우에는 쓰레드는 컴퓨터 메모리 자원을 공유하므로 특정 메모리 공간에 여러 쓰레드가 동시에 접근하여 결과가 엉망이 되는 경우가 발생할 수 있습니다. 따라서 파이썬에서는 GIL (Global Interpreter Lock) 이라고 하는 단일 쓰레드만 공유 자원에 접근할 수 있게 제한함으로써 다른 쓰레드가 현재 쓰레드 수행을 방해하지 못하게 합니다. 따라서 CPython이 메모리를 관리할 때 GIL이 다른 쓰레드로부터 메모리가 더렵혀지지 않도록 안전하게 보호해주는 역할을 하게 됩니다.
CPython's memory management
운영체제 (OS)는 물리적인 메모리를 추상화하여 파이썬 프로그램을 포함한 어플리케이션이 접근할 수 있는 virtual memory를 생성합니다. Figure 1과 같이 파이썬 프로그램이 사용할 virtual memory가 할당되고 CPython은 object 메모리 공간에 allocator를 사용해서 새로운 객체마다 메모리 공간을 할당합니다.
Components
CPython의 메모리 할당은 Figure 2와 같이 1) Arena, 2) Pool, 3) Block 이라고 하는 3가지 구성요소로 이루어져 있습니다. Arena는 OS가 사용하는 고정된 길이의 연속적인 메모리 청크로 보통 256 KB 정도입니다. Arena는 Pool로 이루어져 있는데 하나의 Pool은 4KB 이고 같은 크기를 가진 Block들로 이루어져 있습니다. Block의 최소크기는 8 바이트로 예를 들어 42 바이트 크기의 데이터가 요청되면 48 바이트 크기의 Block이 할당됩니다.
Pools
Pool은 단일 크기로 이루어진 Block들로 이루어져있으며 각 pool은 같은 block 크기를 가진 다른 pool들과 이중연결리스트로 연결되어 요구되는 block 크기에 맞는 공간을 빠르게 찾을 수 있도록 합니다. 또한, pool은 데이터가 할당된 정도에 따라 used/full/empty 상태로 구분될 수 있는데 각 block 크기마다 usedpools 리스트에 used 상태인 pool을 담고 freepools 리스트에는 empty 상태인 pool을 담습니다. 예를 들어 8 바이트의 메모리가 필요하다면 8 바이트 block 크기의 usedpools 리스트를 검색하고 적절한 pool이 없다면 freepools 리스트에서 하나의 pool을 선택하여 8 바이트 데이터를 저장합니다. 이때 선택된 pool은 8 바이트 block 크기의 usedpools 리스트에 추가되겠죠. 반대로 full 상태의 pool에서 메모리가 해제되면 used 상태로 전환되고 맞는 block 크기의 usedpools 리스트에 추가됩니다.
Blocks
Block 또한 pool과 마찬가지로 1) 할당된 적이 없는 unallocated, 2) 전에 할당이 되었지만 데이터가 더 이상 쓰이지 않아 해제되고 OS에 반납되지 않은 free, 3) 데이터가 존재하는 allocated의 3가지 상태가 존재합니다. Figure 3과 같이 pool에는 free block을 가리키는 freeblock 포인터가 존재하며 이는 단일연결리스트로 구성되어 있습니다. 따라서 freeblock 리스트에는 사용가능한 free block들이 담겨져있고 요구되는 block 크기가 freeblock 리스트의 크기보다 클 경우 unallocated block이 새로 할당되게됩니다. 또한 garbage collector에 의해 특정 block이 해제된다면 freeblock 리스트의 첫 번째 원소로 담기게 됩니다.
Arenas
Arena는 pool들을 담고 있으며 pool, block과 달리 상태가 존재하지 않습니다. 대신 이중연결리스트로 free pool의 숫자가 적은 순서대로 구성된 usable_arena가 Figure 4와 같이 존재합니다. 따라서 가용공간이 제일 적은 arena부터 우선적으로 사용되게 되는데요, 이는 block과 달리 arena의 경우는 실제로 모든 pool이 비워질경우 메모리 공간 해제 시 OS에 해당 메모리가 반환되기 때문입니다.
참조
'Computer > Python' 카테고리의 다른 글
Decorator 에서 함수 디폴트 인자 파악 방법 (0) | 2021.07.20 |
---|---|
namedtuple 인스턴스 확인 (0) | 2021.07.19 |
파이썬과 객체 (0) | 2021.07.14 |
파이썬의 Namespace와 Scope (0) | 2021.07.13 |
파이썬의 매개변수 전달 방식 (0) | 2021.07.13 |