Following system colour scheme - Python 增強提案 Selected dark colour scheme - Python 增強提案 Selected light colour scheme - Python 增強提案

Python 增強提案

PEP 445 – 新增新的 API 以自定義 Python 記憶體分配器

作者:
Victor Stinner <vstinner at python.org>
BDFL 委託
Antoine Pitrou <solipsis at pitrou.net>
狀態:
最終版
型別:
標準跟蹤
建立日期:
2013年6月15日
Python 版本:
3.4
決議:
Python-Dev 訊息

目錄

摘要

本 PEP 提出了新的應用程式程式設計介面(API)以自定義 Python 記憶體分配器。唯一需要符合本 PEP 的實現是 CPython,但其他實現可以選擇相容,或重用類似的方案。

基本原理

用例

  • 嵌入 Python 的應用程式希望將 Python 記憶體與應用程式記憶體隔離,或者希望使用針對其 Python 用途最佳化的不同記憶體分配器
  • Python 執行在記憶體不足和 CPU 緩慢的嵌入式裝置上。可以使用自定義記憶體分配器來提高效率和/或訪問裝置的所有記憶體。
  • 記憶體分配器的除錯工具
    • 跟蹤記憶體使用情況(查詢記憶體洩漏)
    • 獲取記憶體分配的位置:Python 檔名和行號,以及記憶體塊的大小
    • 檢測緩衝區下溢、緩衝區溢位和濫用 Python 分配器 API(參見將記憶體塊分配器上的除錯檢查重新設計為鉤子
    • 強制記憶體分配失敗以測試 MemoryError 異常的處理

提案

新函式和結構

  • 新增新的 GIL-free(無需持有 GIL)記憶體分配器
    • void* PyMem_RawMalloc(size_t size)
    • void* PyMem_RawRealloc(void *ptr, size_t new_size)
    • void PyMem_RawFree(void *ptr)
    • 新分配的記憶體將不會以任何方式初始化。
    • 如果可能,請求零位元組會返回一個不同的非 NULL 指標,就像呼叫了 PyMem_Malloc(1) 一樣。
  • 新增新的 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;
    
  • 新增新的 PyMemAllocatorDomain 列舉以選擇 Python 分配器域。域
    • 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)
    • 新分配器在請求零位元組時必須返回一個不同的非 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。在呼叫內部函式之前進行檢查。

注意

pymalloc 分配器針對小於 512 位元組且生命週期短的物件進行了最佳化。它使用固定大小為 256 KB 的記憶體對映,稱為“競技場”。

下面是預設情況下分配器的設定方式

  • PYMEM_DOMAIN_RAW, PYMEM_DOMAIN_MEM: malloc(), realloc()free(); 請求零位元組時呼叫 malloc(1)
  • PYMEM_DOMAIN_OBJ: pymalloc 分配器,對於大於 512 位元組的分配,回退到 PyMem_Malloc()
  • 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() 現在在釋出模式和除錯模式下都呼叫 malloc()realloc(),而不是在除錯模式下呼叫 PyObject_Malloc()PyObject_Realloc()

當至少一個記憶體分配器被 PyMem_SetAllocator() 替換時,必須呼叫 PyMem_SetupDebugHooks() 函式,以便在新分配器之上重新安裝除錯鉤子。

不再直接呼叫 malloc()

PyObject_Malloc() 回退到 PyMem_Malloc() 而不是 malloc(),如果大小大於或等於 512 位元組,並且 PyObject_Realloc() 回退到 PyMem_Realloc() 而不是 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);
}

注意

不需要呼叫 PyMem_SetupDebugHooks(),因為記憶體分配器沒有被替換:記憶體塊分配器上的除錯檢查在啟動時自動安裝。

效能

此 PEP 的實現(問題 #3329)對 Python 基準測試套件沒有明顯開銷。

Python 基準測試套件 (http://hg.python.org/benchmarks) (-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 環境變數,以啟用記憶體塊分配器上的除錯檢查。它將與呼叫 PyMem_SetupDebugHooks() 具有相同的效果,而無需編寫任何 C 程式碼。另一個優點是即使在釋出模式下也可以啟用除錯檢查:除錯檢查將始終被編譯進去,但只有當環境變數存在且非空時才啟用。

這個替代方案被拒絕了,因為新的環境變數會使 Python 初始化更加複雜。PEP 432 試圖簡化 CPython 的啟動序列。

使用宏獲取可自定義的分配器

為了在預設配置下沒有開銷,可定製的分配器將是一個可選功能,透過配置選項或宏啟用。

此替代方案被拒絕,因為使用宏意味著必須重新編譯擴充套件模組才能使用新的分配器和分配器鉤子。無需重新編譯 Python 和擴充套件模組使得除錯鉤子在實踐中更易於使用。

傳遞 C 檔名和行號

使用 __FILE____LINE__ 將分配器函式定義為宏,以獲取記憶體分配的 C 檔名和行號。

修改後的 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 更加複雜:向每個分配器函式傳遞 3 個新引數(ctx、filename、lineno),而不是僅僅一個上下文引數(ctx)。還需要修改 GC 分配器函式,這會增加太多複雜性,而收益卻很小。

GIL-free PyMem_Malloc()

在 Python 3.3 中,當 Python 以除錯模式編譯時,PyMem_Malloc() 間接呼叫 PyObject_Malloc(),這需要持有 GIL(它不是執行緒安全的)。這就是為什麼 PyMem_Malloc() 必須在持有 GIL 的情況下呼叫。

本 PEP 更改了 PyMem_Malloc():它現在總是呼叫 malloc() 而不是 PyObject_Malloc()。因此,可以從 PyMem_Malloc() 中移除“必須持有 GIL”的限制。

此替代方案被拒絕,因為允許在不持有 GIL 的情況下呼叫 PyMem_Malloc() 可能會破壞設定其自己的分配器或分配器鉤子的應用程式。持有 GIL 對於開發自定義分配器很方便:無需關心其他執行緒。對於除錯分配器鉤子也很方便:Python 物件可以安全地檢查,並且 C API 可以用於報告。

此外,在記憶體分配器中呼叫 PyGILState_Ensure() 會產生意外行為,尤其是在 Python 啟動和建立新的 Python 執行緒狀態時。最好將獲取 GIL 的責任從自定義分配器中解放出來。

不新增 PyMem_RawMalloc()

malloc() 替換為 PyMem_Malloc(),但僅在持有 GIL 的情況下。否則,保持 malloc() 不變。

在某些 Python 函式中,PyMem_Malloc() 在沒有持有 GIL 的情況下使用。例如,Python 的 main()Py_Main() 函式呼叫 PyMem_Malloc(),而 GIL 尚不存在。在這種情況下,PyMem_Malloc() 將被替換為 malloc()(或 PyMem_RawMalloc())。

此替代方案被拒絕,因為 PyMem_RawMalloc() 對於準確的記憶體使用報告是必需的。當使用除錯鉤子跟蹤記憶體使用情況時,無法跟蹤直接呼叫 malloc() 分配的記憶體。PyMem_RawMalloc() 可以被鉤住,因此可以跟蹤 Python 分配的所有記憶體,包括在不持有 GIL 的情況下分配的記憶體。

使用現有除錯工具分析記憶體使用情況

有許多現有的除錯工具可用於分析記憶體使用情況。一些例子:ValgrindPurifyClang AddressSanitizerfailmalloc 等。

問題是如何檢索與記憶體指標相關的 Python 物件以讀取其型別和/或內容。另一個問題是檢索記憶體分配的來源:C 回溯通常無用(與使用 __FILE____LINE__ 的宏的理由相同,參見傳遞 C 檔名和行號),Python 檔名和行號(甚至 Python 回溯)更有用。

此替代方案被拒絕,因為經典工具無法內省 Python 內部以收集此類資訊。能夠在持有 GIL 的情況下設定分配器鉤子可以收集大量來自 Python 內部的有用資料。

新增一個 msize() 函式

PyMemAllocatorPyObjectArenaAllocator 結構新增另一個函式

size_t msize(void *ptr);

此函式返回記憶體塊或記憶體對映的大小。如果函式未實現或指標未知(例如:NULL 指標),則返回 (size_t)-1。

在 Windows 上,此函式可以使用 _msize()VirtualQuery() 實現。

該函式可用於實現跟蹤記憶體使用情況的鉤子。分配器的 free() 方法只獲取記憶體塊的地址,而更新記憶體使用情況需要記憶體塊的大小。

額外的 msize() 函式被拒絕,因為只有少數平臺實現了它。例如,帶 GNU libc 的 Linux 不提供獲取記憶體塊大小的函式。msize() 目前未在 Python 原始碼中使用。該函式只用於跟蹤記憶體使用,並使 API 更加複雜。除錯鉤子可以在內部實現該函式,無需將其新增到 PyMemAllocatorPyObjectArenaAllocator 結構中。

沒有上下文引數

簡化分配器函式的簽名,移除上下文引數

  • 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 使用的庫

其他庫

本 PEP 中新的 ctx 引數受到 zlib 和 Oracle OCI 庫 API 的啟發。

另請參閱 GNU libc: 記憶體分配鉤子,它使用不同的方法來鉤住記憶體分配器。

記憶體分配器

C 標準庫提供了眾所周知的 malloc() 函式。它的實現取決於平臺和 C 庫。GNU C 庫使用修改後的 ptmalloc2,基於“Doug Lea 的 Malloc”(dlmalloc)。FreeBSD 使用 jemalloc。Google 提供了 tcmalloc,它是 gperftools 的一部分。

malloc() 使用兩種記憶體:堆和記憶體對映。記憶體對映通常用於大型分配(例如:大於 256 KB),而堆用於小型分配。

在 UNIX 上,堆由 brk()sbrk() 系統呼叫處理,並且是連續的。在 Windows 上,堆由 HeapAlloc() 處理,並且可以是不連續的。記憶體對映在 UNIX 上由 mmap() 處理,在 Windows 上由 VirtualAlloc() 處理,它們可以是不連續的。

釋放記憶體對映會立即將記憶體返還給系統。在 UNIX 上,只有當釋放的塊位於堆的末尾時,堆記憶體才會返還給系統。否則,只有當釋放的記憶體之後的所有記憶體也被釋放時,記憶體才會返還給系統。

為了在堆上分配記憶體,分配器會嘗試重用空閒空間。如果沒有足夠大的連續空間,即使有比所需大小更多的空閒空間,堆也必須擴大。這個問題被稱為“記憶體碎片化”:系統看到的記憶體使用量高於實際使用量。在 Windows 上,如果連續空閒記憶體不足,HeapAlloc() 會使用 VirtualAlloc() 建立新的記憶體對映。

CPython 有一個 pymalloc 分配器,用於小於 512 位元組的分配。此分配器針對生命週期短的小物件進行了最佳化。它使用固定大小為 256 KB 的記憶體對映,稱為“競技場”。

其他分配器

本 PEP 允許根據應用程式對記憶體的使用情況(分配次數、分配大小、物件生命週期等)精確選擇使用的記憶體分配器。


來源: https://github.com/python/peps/blob/main/peps/pep-0445.rst

最後修改時間: 2025-02-01 08:59:27 GMT