python 管理 allocation/freeing 大变量的策略是什么?
What is python's strategy to manage allocation/freeing of large variables?
作为 this question 的后续,在 (C)Python.
中似乎有不同的 allocation/deallocation 小变量和大变量策略
更准确地说,对象大小似乎有一个边界,超过该边界,分配的对象使用的内存可以返回给OS。低于此大小,内存不会还给 OS。
引用 Numpy 释放内存策略的答案:
The exception is that for large single allocations (e.g. if you create a multi-megabyte array), a different mechanism is used. Such large memory allocations can be released back to the OS. So it might specifically be the non-numpy parts of your program that are producing the issues you see.
的确,这两种分配策略很容易展示。例如:
- 第一种策略:不将内存还给 OS
import numpy as np
import psutil
import gc
# Allocate array
x = np.random.uniform(0,1, size=(10**4))
# gc
del x
gc.collect()
# We go from 41295.872 KB to 41295.872 KB
# using psutil.Process().memory_info().rss / 10**3; same behavior for VMS
=> 没有内存还给 OS
- 第二种策略:释放的内存返回给OS
做同样的实验,但使用更大的数组:
x = np.random.uniform(0,1, size=(10**5))
del x
gc.collect()
# We go from 41582.592 KB to 41017.344 KB
=>内存释放到OS
似乎使用第二种策略分配了大约大于 8*10**4
字节的对象。
所以:
- 是否记录了此行为? (分配策略发生变化的确切边界是什么?)
- 这些策略的内部原理是什么(假设使用
mmap
/munmap
将内存释放回 OS)
- 这是 100% 由 Python 运行时完成的,还是 Numpy 有特定的处理方式? (numpy doc提到了在内存分配器之间切换的
NPY_USE_PYMEM
)
你观察到的不是CPython的策略,而是你的CPython版本的C-运行时内存分配器的策略是使用。
当 CPython allocates/deallocates 内存通过 malloc/free
时,它不直接与底层 OS 通信,而是与内存分配器的具体实现通信。就我而言 Linux,它是 the GNU Allocator。
GNU 分配器有不同的所谓的区域,内存不会返回到 OS,而是保留下来以便可以重复使用而无需与 OS 通信。但是,如果请求大量内存(无论 "large" 的定义如何),分配器不会使用来自 arenas 的内存,而是从 OS 请求内存,因此可以直接返回到 OS,一旦 free
被调用。
CPython 有自己的内存分配器 - pymalloc,它建立在 C-运行 时间分配器之上。它针对生活在特殊领域的小物体进行了优化;与底层 C-运行 时间分配器相比,creating/freeing 这些对象的开销更少。但是,大于 512 字节的对象不使用此区域,而是由 C-运行time-allocator 直接管理。
numpy 数组的情况更加复杂,因为不同的内存分配器用于元数据(如形状、数据类型和其他标志)和实际数据本身:
- 对于元数据
PyArray_malloc
,使用 CPython 的内存分配器(即 pymalloc)。
- 对于数据本身,使用了
PyDataMem_NEW
,它直接利用了底层的C-运行时间功能:
NPY_NO_EXPORT void *
PyDataMem_NEW(size_t size)
{
void *result;
result = malloc(size);
...
return result;
}
我不确定,这个设计背后的确切想法是什么:显然有人想从 pymalloc 的小对象优化中获益,而对于数据,这种优化永远不会起作用,但是可以使用 PyMem_RawMalloc
而不是 malloc
。也许目标是能够将 numpy 数组包装在由 C 例程分配的内存周围并接管内存的所有权(但这在某些情况下不起作用,请参阅我在本文末尾的评论 post)。
这解释了您观察到的行为:对于数据(其大小根据传入的大小参数而变化)使用了 PyDataMem_NEW
,它绕过了 CPython 的内存分配器并且您会看到 C-运行time 的分配器的原始行为。
应尽量避免混合使用不同的 allocations/deallocations 例程 PyArray_malloc
/PyDataMem_NEW'/
mallocand
PyArray_free/
PyDataMem_FREE/
free`:即使它在手头的 OS+Python 版本上工作,它也可能会因其他组合而失败。
例如在 Windows 上,当使用不同的编译器版本构建扩展时,一个可执行文件可能具有来自不同 C-运行 时代的不同内存分配器,并且 malloc/free
可能与不同的 C 内存分配器通信,这可能导致难以追踪错误。
作为 this question 的后续,在 (C)Python.
中似乎有不同的 allocation/deallocation 小变量和大变量策略
更准确地说,对象大小似乎有一个边界,超过该边界,分配的对象使用的内存可以返回给OS。低于此大小,内存不会还给 OS。
引用 Numpy 释放内存策略的答案:
The exception is that for large single allocations (e.g. if you create a multi-megabyte array), a different mechanism is used. Such large memory allocations can be released back to the OS. So it might specifically be the non-numpy parts of your program that are producing the issues you see.
的确,这两种分配策略很容易展示。例如:
- 第一种策略:不将内存还给 OS
import numpy as np
import psutil
import gc
# Allocate array
x = np.random.uniform(0,1, size=(10**4))
# gc
del x
gc.collect()
# We go from 41295.872 KB to 41295.872 KB
# using psutil.Process().memory_info().rss / 10**3; same behavior for VMS
=> 没有内存还给 OS
- 第二种策略:释放的内存返回给OS
做同样的实验,但使用更大的数组:
x = np.random.uniform(0,1, size=(10**5))
del x
gc.collect()
# We go from 41582.592 KB to 41017.344 KB
=>内存释放到OS
似乎使用第二种策略分配了大约大于 8*10**4
字节的对象。
所以:
- 是否记录了此行为? (分配策略发生变化的确切边界是什么?)
- 这些策略的内部原理是什么(假设使用
mmap
/munmap
将内存释放回 OS) - 这是 100% 由 Python 运行时完成的,还是 Numpy 有特定的处理方式? (numpy doc提到了在内存分配器之间切换的
NPY_USE_PYMEM
)
你观察到的不是CPython的策略,而是你的CPython版本的C-运行时内存分配器的策略是使用。
当 CPython allocates/deallocates 内存通过 malloc/free
时,它不直接与底层 OS 通信,而是与内存分配器的具体实现通信。就我而言 Linux,它是 the GNU Allocator。
GNU 分配器有不同的所谓的区域,内存不会返回到 OS,而是保留下来以便可以重复使用而无需与 OS 通信。但是,如果请求大量内存(无论 "large" 的定义如何),分配器不会使用来自 arenas 的内存,而是从 OS 请求内存,因此可以直接返回到 OS,一旦 free
被调用。
CPython 有自己的内存分配器 - pymalloc,它建立在 C-运行 时间分配器之上。它针对生活在特殊领域的小物体进行了优化;与底层 C-运行 时间分配器相比,creating/freeing 这些对象的开销更少。但是,大于 512 字节的对象不使用此区域,而是由 C-运行time-allocator 直接管理。
numpy 数组的情况更加复杂,因为不同的内存分配器用于元数据(如形状、数据类型和其他标志)和实际数据本身:
- 对于元数据
PyArray_malloc
,使用 CPython 的内存分配器(即 pymalloc)。 - 对于数据本身,使用了
PyDataMem_NEW
,它直接利用了底层的C-运行时间功能:
NPY_NO_EXPORT void *
PyDataMem_NEW(size_t size)
{
void *result;
result = malloc(size);
...
return result;
}
我不确定,这个设计背后的确切想法是什么:显然有人想从 pymalloc 的小对象优化中获益,而对于数据,这种优化永远不会起作用,但是可以使用 PyMem_RawMalloc
而不是 malloc
。也许目标是能够将 numpy 数组包装在由 C 例程分配的内存周围并接管内存的所有权(但这在某些情况下不起作用,请参阅我在本文末尾的评论 post)。
这解释了您观察到的行为:对于数据(其大小根据传入的大小参数而变化)使用了 PyDataMem_NEW
,它绕过了 CPython 的内存分配器并且您会看到 C-运行time 的分配器的原始行为。
应尽量避免混合使用不同的 allocations/deallocations 例程 PyArray_malloc
/PyDataMem_NEW'/
mallocand
PyArray_free/
PyDataMem_FREE/
free`:即使它在手头的 OS+Python 版本上工作,它也可能会因其他组合而失败。
例如在 Windows 上,当使用不同的编译器版本构建扩展时,一个可执行文件可能具有来自不同 C-运行 时代的不同内存分配器,并且 malloc/free
可能与不同的 C 内存分配器通信,这可能导致难以追踪错误。