PEP 445 – Python 메모리 할당자를 사용자 정의하기 위한 새 API 추가
- Author:
- Victor Stinner <vstinner at python.org>
- BDFL-Delegate:
- Antoine Pitrou <solipsis at pitrou.net>
- Status:
- Final
- Type:
- Standards Track
- Created:
- 15-Jun-2013
- Python-Version:
- 3.4
- Resolution:
- Python-Dev message
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 PEP는 Python 메모리 할당자를 사용자 정의하기 위한 새로운 애플리케이션 프로그래밍 인터페이스(API)를 제안합니다. 이 PEP를 따라야 하는 구현은 CPython뿐이지만, 다른 구현도 호환되도록 하거나 비슷한 방식을 재사용할 수 있습니다.
근거
사용 사례:
- Python을 내장하면서 Python 메모리를 애플리케이션 메모리와 분리하거나, Python 사용에 최적화된 다른 메모리 할당자를 사용하려는 애플리케이션
- 메모리가 적고 CPU가 느린 임베디드 장치에서 실행되는 Python 효율을 높이거나 장치의 모든 메모리에 접근하기 위해, 또는 두 가지 목적 모두를 위해 사용자 정의 메모리 할당자를 사용할 수 있습니다.
- 메모리 할당자용 디버그 도구:
- 메모리 사용량 추적(메모리 누수 찾기)
- 메모리 할당 위치(Python 파일 이름과 줄 번호)와 메모리 블록 크기 확인
- 버퍼 언더플로, 버퍼 오버플로, Python 할당자 API의 오용 감지(Redesign Debug Checks on Memory Block Allocators as Hooks 참고)
MemoryError예외 처리를 테스트하기 위해 메모리 할당을 강제로 실패시키기
제안
새 함수와 구조체
- GIL이 필요 없는(GIL을 쥐고 있지 않아도 되는) 새 메모리 할당자를 추가합니다:
void* PyMem_RawMalloc(size_t size)void* PyMem_RawRealloc(void *ptr, size_t new_size)void PyMem_RawFree(void *ptr)- 새로 할당된 메모리는 어떤 식으로도 초기화되지 않습니다.
- 0바이트를 요청하면 가능한 경우
PyMem_Malloc(1)을 대신 호출한 것처럼 NULL이 아닌 고유한 포인터를 반환합니다.
- 새
PyMemAllocator구조체를 추가합니다.:typedef struct { /* user context passed as the first argument to the 3 functions */ void *ctx; /* allocate a memory block */ void* (*malloc) (void *ctx, size_t size); /* allocate or resize a memory block */ void* (*realloc) (void *ctx, void *ptr, size_t new_size); /* release a memory block */ void (*free) (void *ctx, void *ptr); } PyMemAllocator;
- Python 할당자 도메인을 선택하기 위한 새
PyMemAllocatorDomain열거형을 추가합니다. 도메인:PYMEM_DOMAIN_RAW:PyMem_RawMalloc(),PyMem_RawRealloc()및PyMem_RawFree()PYMEM_DOMAIN_MEM:PyMem_Malloc(),PyMem_Realloc()및PyMem_Free()PYMEM_DOMAIN_OBJ:PyObject_Malloc(),PyObject_Realloc()및PyObject_Free()
- 메모리 블록 할당자를 가져오고 설정하는 새 함수를 추가합니다:
void PyMem_GetAllocator(PyMemAllocatorDomain domain, PyMemAllocator *allocator)void PyMem_SetAllocator(PyMemAllocatorDomain domain, PyMemAllocator *allocator)- 새 할당자는 0바이트를 요청받으면 NULL이 아닌 고유한 포인터를 반환해야 합니다
PYMEM_DOMAIN_RAW도메인에서는 할당자가 스레드 안전해야 합니다. 할당자가 호출될 때 GIL을 보유하고 있지 않기 때문입니다.
- 새
PyObjectArenaAllocator구조체를 추가합니다.:typedef struct { /* user context passed as the first argument to the 2 functions */ void *ctx; /* allocate an arena */ void* (*alloc) (void *ctx, size_t size); /* release an arena */ void (*free) (void *ctx, void *ptr, size_t size); } PyObjectArenaAllocator;
- pymalloc이 사용하는 아레나 할당자를 가져오고 설정하는 새 함수를 추가합니다:
void PyObject_GetArenaAllocator(PyObjectArenaAllocator *allocator)void PyObject_SetArenaAllocator(PyObjectArenaAllocator *allocator)
- 메모리 할당자가
PyMem_SetAllocator()로 교체되었을 때 메모리 할당자에 디버그 검사를 다시 설치하는 새 함수를 추가합니다:void PyMem_SetupDebugHooks(void)- 모든 메모리 블록 할당자에 디버그 훅을 설치합니다. 이 함수는 여러 번 호출할 수 있지만, 훅은 한 번만 설치됩니다.
- Python이 디버그 모드로 컴파일되지 않았다면 이 함수는 아무 일도 하지 않습니다.
- size가
PY_SSIZE_T_MAX보다 크면 메모리 블록 할당자는 항상 NULL을 반환합니다. 이 검사는 내부 함수를 호출하기 전에 수행됩니다.
Note
pymalloc 할당자는 수명이 짧은 512바이트 미만의 객체에 최적화되어 있습니다. 이 할당자는 “아레나(arena)”라고 부르는 256 KB 고정 크기의 메모리 매핑을 사용합니다.
기본적으로 할당자는 다음과 같이 설정됩니다:
PYMEM_DOMAIN_RAW,PYMEM_DOMAIN_MEM:malloc(),realloc(),free()를 사용합니다. 0바이트를 요청하면malloc(1)을 호출합니다.PYMEM_DOMAIN_OBJ: 512바이트보다 큰 할당에는PyMem_Malloc()으로 대체하는 pymalloc 할당자- pymalloc 아레나 할당자: Windows에서는
VirtualAlloc()과VirtualFree(), 사용할 수 있는 경우mmap()과munmap(), 그 밖의 경우malloc()과free()
메모리 블록 할당자의 디버그 검사를 훅으로 재설계
Python 2.3부터 Python은 디버그 모드에서 메모리 할당자에 대해 여러 가지 검사를 구현합니다:
- 새로 할당된 메모리는
0xCB바이트로 채워지고, 해제된 메모리는0xDB바이트로 채워집니다. - API 위반을 감지합니다. 예:
PyMem_Malloc()으로 할당된 메모리 블록에 대해PyObject_Free()를 호출하는 경우 - 버퍼 시작 전에 쓰는 것(버퍼 언더플로)을 감지합니다.
- 버퍼 끝 뒤에 쓰는 것(버퍼 오버플로)을 감지합니다.
Python 3.3에서는 매크로를 사용하여 PyMem_Malloc(), PyMem_Realloc(), PyMem_Free(), PyObject_Malloc(), PyObject_Realloc(), PyObject_Free()를 교체하는 방식으로 검사를 설치합니다. 새 할당자는 더 큰 버퍼를 할당하고, 버퍼 언더플로, 버퍼 오버플로, 해제 후 사용을 감지하기 위한 패턴을 기록합니다(해제 후 사용은 버퍼를 0xDB 바이트로 채워 감지합니다). 이 할당자는 메모리를 할당할 때 원래의 PyObject_Malloc() 함수를 사용합니다. 따라서 PyMem_Malloc()과 PyMem_Realloc()은 간접적으로 PyObject_Malloc()과 PyObject_Realloc()을 호출합니다.
이 PEP는 디버그 검사를 디버그 모드에서 기존 할당자에 거는 훅으로 재설계합니다. 훅이 없을 때의 호출 추적 예:
PyMem_RawMalloc()=>_PyMem_RawMalloc()=>malloc()PyMem_Realloc()=>_PyMem_RawRealloc()=>realloc()PyObject_Free()=>_PyObject_Free()
훅이 설치된 경우의 호출 추적(디버그 모드):
PyMem_RawMalloc()=>_PyMem_DebugMalloc()=>_PyMem_RawMalloc()=>malloc()PyMem_Realloc()=>_PyMem_DebugRealloc()=>_PyMem_RawRealloc()=>realloc()PyObject_Free()=>_PyMem_DebugFree()=>_PyObject_Free()
그 결과, 이제 PyMem_Malloc()과 PyMem_Realloc()은 디버그 모드에서 PyObject_Malloc()과 PyObject_Realloc()을 호출하는 대신 릴리스 모드와 디버그 모드 모두에서 malloc()과 realloc()을 호출합니다.
PyMem_SetAllocator()로 메모리 할당자를 하나 이상 교체한 경우, 새 할당자 위에 디버그 훅을 다시 설치하려면 PyMem_SetupDebugHooks() 함수를 호출해야 합니다.
더 이상 malloc()을 직접 호출하지 않음
PyObject_Malloc()은 크기가 512바이트 이상이면 malloc() 대신 PyMem_Malloc()으로 대체하고, PyObject_Realloc()은 realloc() 대신 PyMem_Realloc()으로 대체합니다.
malloc() 직접 호출은 PyMem_Malloc()으로 대체되며, GIL을 보유하지 않은 경우에는 PyMem_RawMalloc()으로 대체됩니다.
zlib이나 OpenSSL 같은 외부 라이브러리는 PyMem_Malloc() 또는 PyMem_RawMalloc()을 사용하여 메모리를 할당하도록 구성할 수 있습니다. 라이브러리의 할당자를 (객체 단위가 아니라) 전역으로만 교체할 수 있다면, Python이 애플리케이션에 내장된 경우에는 교체해서는 안 됩니다.
“메모리 사용량 추적” 사용 사례에서는 정확한 보고서를 얻기 위해 외부 라이브러리에서 할당한 메모리도 추적하는 것이 중요합니다. 이러한 할당은 클 수 있으며(예를 들어 MemoryError 예외를 일으킬 수 있음), 추적하지 않으면 메모리 사용량 보고서에서 누락되기 때문입니다.
예시
사용 사례 1: 메모리 할당자 교체, pymalloc 유지
메모리 블록마다 2바이트, pymalloc 아레나마다 10바이트를 낭비하는 더미 예제:
#include <stdlib.h>
size_t alloc_padding = 2;
size_t arena_padding = 10;
void* my_malloc(void *ctx, size_t size)
{
int padding = *(int *)ctx;
return malloc(size + padding);
}
void* my_realloc(void *ctx, void *ptr, size_t new_size)
{
int padding = *(int *)ctx;
return realloc(ptr, new_size + padding);
}
void my_free(void *ctx, void *ptr)
{
free(ptr);
}
void* my_alloc_arena(void *ctx, size_t size)
{
int padding = *(int *)ctx;
return malloc(size + padding);
}
void my_free_arena(void *ctx, void *ptr, size_t size)
{
free(ptr);
}
void setup_custom_allocator(void)
{
PyMemAllocator alloc;
PyObjectArenaAllocator arena;
alloc.ctx = &alloc_padding;
alloc.malloc = my_malloc;
alloc.realloc = my_realloc;
alloc.free = my_free;
PyMem_SetAllocator(PYMEM_DOMAIN_RAW, &alloc);
PyMem_SetAllocator(PYMEM_DOMAIN_MEM, &alloc);
/* leave PYMEM_DOMAIN_OBJ unchanged, use pymalloc */
arena.ctx = &arena_padding;
arena.alloc = my_alloc_arena;
arena.free = my_free_arena;
PyObject_SetArenaAllocator(&arena);
PyMem_SetupDebugHooks();
}
사용 사례 2: 메모리 할당자 교체, pymalloc 재정의
수명이 짧고 512바이트보다 작은 객체의 할당에 최적화된 전용 할당자가 있다면, pymalloc을 재정의할 수 있습니다(PyObject_Malloc() 교체).
메모리 블록마다 2바이트를 낭비하는 더미 예제:
#include <stdlib.h>
size_t padding = 2;
void* my_malloc(void *ctx, size_t size)
{
int padding = *(int *)ctx;
return malloc(size + padding);
}
void* my_realloc(void *ctx, void *ptr, size_t new_size)
{
int padding = *(int *)ctx;
return realloc(ptr, new_size + padding);
}
void my_free(void *ctx, void *ptr)
{
free(ptr);
}
void setup_custom_allocator(void)
{
PyMemAllocator alloc;
alloc.ctx = &padding;
alloc.malloc = my_malloc;
alloc.realloc = my_realloc;
alloc.free = my_free;
PyMem_SetAllocator(PYMEM_DOMAIN_RAW, &alloc);
PyMem_SetAllocator(PYMEM_DOMAIN_MEM, &alloc);
PyMem_SetAllocator(PYMEM_DOMAIN_OBJ, &alloc);
PyMem_SetupDebugHooks();
}
새 할당자가 pymalloc 아레나를 더 이상 사용하지 않으므로 이를 교체할 필요가 없습니다.
사용 사례 3: 메모리 블록 할당자에 훅 설정
모든 메모리 블록 할당자에 훅을 설정하는 예제:
struct {
PyMemAllocator raw;
PyMemAllocator mem;
PyMemAllocator obj;
/* ... */
} hook;
static void* hook_malloc(void *ctx, size_t size)
{
PyMemAllocator *alloc = (PyMemAllocator *)ctx;
void *ptr;
/* ... */
ptr = alloc->malloc(alloc->ctx, size);
/* ... */
return ptr;
}
static void* hook_realloc(void *ctx, void *ptr, size_t new_size)
{
PyMemAllocator *alloc = (PyMemAllocator *)ctx;
void *ptr2;
/* ... */
ptr2 = alloc->realloc(alloc->ctx, ptr, new_size);
/* ... */
return ptr2;
}
static void hook_free(void *ctx, void *ptr)
{
PyMemAllocator *alloc = (PyMemAllocator *)ctx;
/* ... */
alloc->free(alloc->ctx, ptr);
/* ... */
}
void setup_hooks(void)
{
PyMemAllocator alloc;
static int installed = 0;
if (installed)
return;
installed = 1;
alloc.malloc = hook_malloc;
alloc.realloc = hook_realloc;
alloc.free = hook_free;
PyMem_GetAllocator(PYMEM_DOMAIN_RAW, &hook.raw);
PyMem_GetAllocator(PYMEM_DOMAIN_MEM, &hook.mem);
PyMem_GetAllocator(PYMEM_DOMAIN_OBJ, &hook.obj);
alloc.ctx = &hook.raw;
PyMem_SetAllocator(PYMEM_DOMAIN_RAW, &alloc);
alloc.ctx = &hook.mem;
PyMem_SetAllocator(PYMEM_DOMAIN_MEM, &alloc);
alloc.ctx = &hook.obj;
PyMem_SetAllocator(PYMEM_DOMAIN_OBJ, &alloc);
}
Note
메모리 할당자를 교체하지 않으므로 PyMem_SetupDebugHooks()를 호출할 필요가 없습니다. 메모리 블록 할당자에 대한 디버그 검사는 시작 시 자동으로 설치됩니다.
성능
이 PEP의 구현(이슈 #3329)은 Python 벤치마크 모음에서 눈에 띄는 오버헤드가 없습니다.
Python 벤치마크 모음 결과(-b 2n3): 일부 테스트는 1.04배 빠르고, 일부 테스트는 1.04배 느립니다. pybench 마이크로벤치마크 결과: 전체적으로 “+0.1%” 느립니다(차이는 -4.9%에서 +5.6% 사이).
벤치마크의 전체 출력은 이슈 #3329에 첨부되어 있습니다.
기각된 대안
메모리 할당자를 가져오고 설정하는 더 구체적인 함수
원래는 할당자 도메인마다 함수 한 쌍씩을 두는, 더 많은 C API 함수 집합이 제안되었습니다:
void PyMem_GetRawAllocator(PyMemAllocator *allocator)void PyMem_GetAllocator(PyMemAllocator *allocator)void PyObject_GetAllocator(PyMemAllocator *allocator)void PyMem_SetRawAllocator(PyMemAllocator *allocator)void PyMem_SetAllocator(PyMemAllocator *allocator)void PyObject_SetAllocator(PyMemAllocator *allocator)
이 대안은 더 구체적인 함수로는 범용 코드를 작성할 수 없기 때문에 기각되었습니다: 메모리 할당자 도메인마다 코드를 중복해서 작성해야 합니다.
PyMem_Malloc()이 기본적으로 PyMem_RawMalloc()을 재사용하도록 하기
PyMem_Malloc()이 기본적으로 PyMem_RawMalloc()을 호출한다면, PyMem_SetAllocator(PYMEM_DOMAIN_RAW, alloc)를 호출할 때 PyMem_Malloc()도 간접적으로 패치될 것입니다.
이 대안은 PyMem_SetAllocator()가 도메인에 따라 다르게 동작하게 되기 때문에 기각되었습니다. 항상 같은 방식으로 동작해야 오류가 덜 발생합니다.
새 PYDEBUGMALLOC 환경 변수 추가하기
메모리 블록 할당자에 대한 디버그 검사를 활성화하는 새 PYDEBUGMALLOC 환경 변수를 추가하자는 제안이 있었습니다. 이 환경 변수는 C 코드를 전혀 작성하지 않고도 PyMem_SetupDebugHooks()를 호출하는 것과 같은 효과를 냈을 것입니다. 또 다른 장점은 릴리스 모드에서도 디버그 검사를 활성화할 수 있다는 점입니다: 디버그 검사는 항상 컴파일에 포함되지만, 환경 변수가 존재하고 비어 있지 않을 때만 활성화됩니다.
이 대안은 새 환경 변수가 Python 초기화를 더욱 복잡하게 만들기 때문에 기각되었습니다. PEP 432는 CPython 시작 절차를 단순화하려고 합니다.
매크로를 사용하여 사용자 정의 가능한 할당자 얻기
기본 구성에서 오버헤드가 없도록, 사용자 정의 가능한 할당자는 구성 옵션이나 매크로로 활성화하는 선택적 기능이 됩니다.
이 대안은 매크로를 사용하면 새 할당자와 할당자 훅을 사용하기 위해 확장 모듈을 다시 컴파일해야 하기 때문에 기각되었습니다. Python이나 확장 모듈을 다시 컴파일할 필요가 없으면 실제로 디버그 훅을 더 쉽게 사용할 수 있습니다.
C 파일 이름과 줄 번호 전달하기
메모리 할당의 C 파일 이름과 줄 번호를 얻기 위해 __FILE__과 __LINE__을 사용하는 매크로로 할당자 함수를 정의합니다.
수정된 PyMemAllocator 구조체를 사용하는 PyMem_Malloc 매크로의 예:
typedef struct {
/* user context passed as the first argument
to the 3 functions */
void *ctx;
/* allocate a memory block */
void* (*malloc) (void *ctx, const char *filename, int lineno,
size_t size);
/* allocate or resize a memory block */
void* (*realloc) (void *ctx, const char *filename, int lineno,
void *ptr, size_t new_size);
/* release a memory block */
void (*free) (void *ctx, const char *filename, int lineno,
void *ptr);
} PyMemAllocator;
void* _PyMem_MallocTrace(const char *filename, int lineno,
size_t size);
/* the function is still needed for the Python stable ABI */
void* PyMem_Malloc(size_t size);
#define PyMem_Malloc(size) \
_PyMem_MallocTrace(__FILE__, __LINE__, size)
GC 할당자 함수도 패치해야 할 것입니다. 예를 들어, _PyObject_GC_Malloc()은 많은 C 함수에서 사용되므로 서로 다른 타입의 객체가 같은 할당 위치를 갖게 됩니다.
이 대안은 각 할당자에 파일 이름과 줄 번호를 전달하면 API가 더 복잡해지기 때문에 기각되었습니다: 각 할당자 함수에 컨텍스트 인자(ctx) 하나만이 아니라 새로운 인자 3개(ctx, filename, lineno)를 전달해야 합니다. GC 할당자 함수까지 수정해야 하는 것은 작은 이득에 비해 너무 많은 복잡성을 더합니다.
GIL이 필요 없는 PyMem_Malloc()
Python 3.3에서 Python을 디버그 모드로 컴파일하면, PyMem_Malloc()은 GIL을 쥐고 있어야 하는(스레드 안전하지 않은) PyObject_Malloc()을 간접적으로 호출합니다. 그렇기 때문에 PyMem_Malloc()은 GIL을 쥔 상태에서 호출해야 합니다.
이 PEP는 PyMem_Malloc()을 변경합니다: 이제 PyObject_Malloc() 대신 항상 malloc()을 호출합니다. 따라서 PyMem_Malloc()에서 “GIL을 쥐고 있어야 한다”는 제약을 제거할 수 있습니다.
이 대안은 GIL을 쥐지 않은 상태에서 PyMem_Malloc()을 호출할 수 있게 하면 자체 할당자나 할당자 훅을 설정하는 애플리케이션이 망가질 수 있기 때문에 거부되었습니다. GIL을 쥐고 있으면 사용자 정의 할당자를 개발하기가 편리합니다. 다른 스레드를 신경 쓸 필요가 없기 때문입니다. 디버그 할당자 훅에도 편리합니다. Python 객체를 안전하게 검사할 수 있고, 보고에 C API를 사용할 수 있습니다.
게다가 메모리 할당자에서 PyGILState_Ensure()를 호출하면 예상치 못한 동작이 발생하며, 특히 Python 시작 시와 새 Python 스레드 상태를 생성할 때 그렇습니다. 사용자 정의 할당자에게서 GIL을 획득해야 하는 책임을 덜어 주는 것이 더 좋습니다.
PyMem_RawMalloc()을 추가하지 않기
malloc()을 PyMem_Malloc()으로 대체하되, GIL을 쥐고 있는 경우에만 대체합니다. 그렇지 않으면 malloc()을 그대로 유지합니다.
일부 Python 함수에서는 GIL을 쥐지 않은 상태로 PyMem_Malloc()이 사용됩니다. 예를 들어, Python의 main() 함수와 Py_Main() 함수는 GIL이 아직 존재하지 않는데도 PyMem_Malloc()을 호출합니다. 이 경우 PyMem_Malloc()은 malloc()(또는 PyMem_RawMalloc())으로 대체됩니다.
이 대안은 메모리 사용량을 정확하게 보고하려면 PyMem_RawMalloc()이 필요하기 때문에 거부되었습니다. 디버그 훅을 사용하여 메모리 사용량을 추적할 때, malloc()을 직접 호출하여 할당한 메모리는 추적할 수 없습니다. PyMem_RawMalloc()은 훅을 걸 수 있으므로, GIL을 쥐지 않은 상태로 할당한 메모리를 포함하여 Python이 할당한 모든 메모리를 추적할 수 있습니다.
기존 디버그 도구를 사용하여 메모리 사용 분석하기
메모리 사용을 분석하는 기존 디버그 도구는 많이 있습니다. 몇 가지 예로는 Valgrind, Purify, Clang AddressSanitizer, failmalloc 등이 있습니다.
문제는 메모리 포인터와 관련된 Python 객체를 찾아 그 타입이나 내용을 읽는 것입니다. 또 다른 문제는 메모리 할당의 출처를 찾는 것입니다. C 백트레이스는 대개 쓸모가 없으며(__FILE__과 __LINE__을 사용하는 매크로와 같은 이유입니다. Pass the C filename and line number를 참고하십시오), Python 파일 이름과 줄 번호(또는 Python 트레이스백)가 더 유용합니다.
기존 도구는 이러한 정보를 수집하기 위해 Python 내부를 인트로스펙션할 수 없으므로 이 대안은 거부되었습니다. GIL을 쥔 상태로 호출되는 할당자에 훅을 설정할 수 있으면 Python 내부에서 유용한 데이터를 많이 수집할 수 있습니다.
msize() 함수 추가하기
PyMemAllocator 및 PyObjectArenaAllocator 구조체에 함수를 하나 더 추가합니다.:
size_t msize(void *ptr);
이 함수는 메모리 블록이나 메모리 매핑의 크기를 반환합니다. 함수가 구현되지 않았거나 포인터를 알 수 없는 경우(예: NULL 포인터) (size_t)-1을 반환합니다.
Windows에서는 _msize()와 VirtualQuery()를 사용하여 이 함수를 구현할 수 있습니다.
이 함수는 메모리 사용량을 추적하는 훅을 구현하는 데 사용할 수 있습니다. 할당자의 free() 메서드는 메모리 블록의 주소만 받지만, 메모리 사용량을 갱신하려면 메모리 블록의 크기가 필요합니다.
추가 msize() 함수는 이를 구현하는 플랫폼이 거의 없기 때문에 거부되었습니다. 예를 들어, GNU libc를 사용하는 Linux는 메모리 블록의 크기를 얻는 함수를 제공하지 않습니다. msize()는 현재 Python 소스 코드에서 사용되지 않습니다. 이 함수는 메모리 사용 추적에만 사용될 뿐이며, API를 더 복잡하게 만들 것입니다. 디버그 훅이 이 함수를 내부적으로 구현할 수 있으므로, PyMemAllocator 및 PyObjectArenaAllocator 구조체에 추가할 필요가 없습니다.
컨텍스트 인자 없음
할당자 함수의 시그니처를 단순화하고 컨텍스트 인자를 제거합니다:
void* malloc(size_t size)void* realloc(void *ptr, size_t new_size)void free(void *ptr)
할당자 훅은 PyMem_SetAllocator()와 PyObject_SetAllocator(), 나아가 PyMem_SetRawAllocator()에도 재사용될 가능성이 높지만, 훅은 할당자에 따라 서로 다른 함수를 호출해야 합니다. 컨텍스트는 같은 사용자 정의 할당자나 훅을 서로 다른 Python 할당자에 재사용하는 편리한 방법입니다.
C++에서는 컨텍스트를 사용하여 this를 전달할 수 있습니다.
외부 라이브러리
메모리 할당자를 사용자 정의하는 데 쓰이는 API의 예입니다.
Python이 사용하는 라이브러리:
- OpenSSL: CRYPTO_set_mem_functions()로 메모리 관리 함수를 전역으로 설정합니다
- expat: parserCreate()에는 인스턴스별 메모리 핸들러가 있습니다
- zlib: zlib 1.2.8 Manual, 불투명 포인터를 전달합니다
- bz2: bzip2 and libbzip2, version 1.0.5, 불투명 포인터를 전달합니다
- lzma: LZMA SDK - How to Use, 불투명 포인터를 전달합니다
- lipmpdec: 불투명 포인터를 사용하지 않습니다(고전적인 malloc API)
기타 라이브러리:
- glib: g_mem_set_vtable()
- libxml2: xmlGcMemSetup(), 전역
- Oracle의 OCI: Oracle Call Interface Programmer’s Guide, Release 2 (9.2), 불투명 포인터를 전달합니다
이 PEP의 새로운 ctx 매개변수는 zlib 및 Oracle OCI 라이브러리의 API에서 착안했습니다.
메모리 할당자에 훅을 거는 데 다른 접근 방식을 사용하는 GNU libc: Memory Allocation Hooks도 참고하십시오.
메모리 할당자
C 표준 라이브러리는 잘 알려진 malloc() 함수를 제공합니다. 그 구현은 플랫폼과 C 라이브러리에 따라 다릅니다. GNU C 라이브러리는 “Doug Lea’s Malloc”(dlmalloc)을 기반으로 수정한 ptmalloc2를 사용합니다. FreeBSD는 jemalloc을 사용합니다. Google은 gperftools의 일부인 tcmalloc을 제공합니다.
malloc()은 힙과 메모리 매핑이라는 두 종류의 메모리를 사용합니다. 메모리 매핑은 보통 큰 할당(예: 256 KB보다 큰 할당)에 사용되는 반면, 힙은 작은 할당에 사용됩니다.
UNIX에서 힙은 brk()와 sbrk() 시스템 호출로 처리되며, 연속적입니다. Windows에서 힙은 HeapAlloc()으로 처리되며, 불연속적일 수 있습니다. 메모리 매핑은 UNIX에서는 mmap()이, Windows에서는 VirtualAlloc()이 처리하며, 불연속적일 수 있습니다.
메모리 매핑을 해제하면 메모리가 즉시 시스템에 반환됩니다. UNIX에서 힙 메모리는 해제된 블록이 힙의 끝에 위치한 경우에만 시스템에 반환됩니다. 그렇지 않으면 해제된 메모리 뒤에 위치한 모든 메모리까지 해제되었을 때에만 메모리가 시스템에 반환됩니다.
힙에 메모리를 할당하기 위해 할당자는 빈 공간을 재사용하려고 시도합니다. 충분히 큰 연속 공간이 없으면, 필요한 크기보다 더 많은 빈 공간이 있더라도 힙을 확장해야 합니다. 이 문제를 “메모리 단편화”라고 합니다: 시스템이 보는 메모리 사용량이 실제 사용량보다 높아집니다. Windows에서 HeapAlloc()은 비어 있는 연속 메모리가 충분하지 않으면 VirtualAlloc()으로 새 메모리 매핑을 생성합니다.
CPython에는 512바이트보다 작은 할당을 위한 pymalloc 할당자가 있습니다. 이 할당자는 수명이 짧은 작은 객체에 최적화되어 있습니다. 이 할당자는 256 KB의 고정 크기를 가진 “아레나(arena)”라는 메모리 매핑을 사용합니다.
다른 할당자:
- Windows는 저단편화 힙(Low-fragmentation Heap)을 제공합니다.
- Linux 커널은 슬랩 할당을 사용합니다.
- glib 라이브러리에는 Memory Slice API가 있습니다: 같은 크기의 메모리 청크 그룹을 효율적으로 할당하는 방법입니다.
이 PEP를 사용하면 애플리케이션의 메모리 사용 방식(할당 횟수, 할당 크기, 객체의 수명 등)에 따라 어떤 메모리 할당자를 사용할지 정확히 선택할 수 있습니다.
링크
메모리 할당과 관련된 CPython 이슈:
- 이슈 #3329: 메모리 할당자를 사용자 정의하는 새 API 추가
- 이슈 #13483: 메모리 아레나 할당에 VirtualAlloc 사용
- 이슈 #16742: PyOS_Readline이 GIL을 해제하고 스레드 안전하지 않은 PyOS_StdioReadline을 호출함
- 이슈 #18203: malloc() 호출을 PyMem_Malloc() 또는 PyMem_RawMalloc()으로 대체
- 이슈 #18227: zlib이나 OpenSSL 같은 외부 라이브러리에서 Python 메모리 할당자 사용
Python 애플리케이션의 메모리 사용량을 분석하는 프로젝트:
Copyright
This document has been placed into the public domain.