Python 熊猫的好奇记忆消耗
在分析我的算法的内存消耗时,我惊讶地发现,对于较小的输入,有时需要更多的内存 这一切归结为Python 熊猫的好奇记忆消耗,python,algorithm,performance,pandas,Python,Algorithm,Performance,Pandas,在分析我的算法的内存消耗时,我惊讶地发现,对于较小的输入,有时需要更多的内存 这一切归结为pandas.unique()的以下用法: 使用N=6*10^7需要3.7GB峰值内存,但使用N=8*10^7“仅”3GB 扫描不同的输入大小会生成以下图形: 出于好奇和自我教育:如何解释N=5*10^7,N=1.3*10^7周围违反直觉的行为(即输入大小越小,内存越大) 以下是在Linux上生成内存消耗图的脚本: pandas_unique_test.py: 显示_memory.py: 运行_perf
pandas.unique()
的以下用法:
使用N=6*10^7
需要3.7GB
峰值内存,但使用N=8*10^7
“仅”3GB
扫描不同的输入大小会生成以下图形:
出于好奇和自我教育:如何解释N=5*10^7
,N=1.3*10^7
周围违反直觉的行为(即输入大小越小,内存越大)
以下是在Linux上生成内存消耗图的脚本: pandas_unique_test.py: 显示_memory.py: 运行_perf_test.sh: 现在:
sh run_perf_tests.sh 2>&1 | python show_memory.py
让我们看看
表示它是“基于哈希表的唯一”
它调用以获取数据的正确哈希表实现,即
哈希表初始化为size\u hint
=值向量的长度。这意味着调用kh\u resize\u DTYPE(表,大小提示)
这些函数已定义(模板化)
它似乎为存储桶分配了(size\u hint>>5)*4+(size\u hint)*8*2
字节的内存(可能更多,可能更少,我可能会离开这里)
然后,被称为
它分配一个空的Int64Vector
,从开始,每当它们被填充时,它们的大小都会发生变化
然后它迭代你的值,计算它们是否在哈希表中;如果没有,它们将同时添加到哈希表和向量中。(这是向量可能增长的地方;由于大小提示,哈希表不需要增长。)
最后,一个NumPyndarray
指向向量
所以,呃,我想你会看到向量大小在某些阈值下翻了四倍(如果我深夜的数学正确的话
>>> [2 ** (2 * i - 1) for i in range(4, 20)]
[
128,
512,
2048,
8192,
32768,
131072,
524288,
2097152,
8388608,
33554432,
134217728,
536870912,
2147483648,
8589934592,
34359738368,
137438953472,
...,
]
希望这能给我们一些启示:)@AKX answers解释了为什么内存消耗会在跳跃中增加,但没有解释为什么它会随着元素的增加而减少——这个答案填补了这个空白
pandas
使用khash
-地图查找独特元素。创建哈希映射时,数组中的元素数:
但是,问题是“地图中将有n个值”:
然而,卡什地图将其理解为(而不是我们需要一个放置n
元素的地方):
以下是关于卡什地图中桶数的两个重要实现细节:
- 桶的数量是
- 最多可以占用77%的桶,否则大小将加倍(+)
SCOPE void kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets)
...
- 哈希映射在开始时将有1024个bucket(大于元素数的最小二次幂),但仅足够容纳约800个元素(即1024个中的77%)
- 添加约800个元素后,大小将调整为2048个元素(下一次幂为2),这意味着峰值消耗将3072(需要同时使用新旧阵列)
- 散列映射在开始时将有2048个存储桶(比元素数大两个的最小幂),并且仅足够容纳约1600个元素(即2048年的77%)
- 不会进行重新灰化,峰值消耗将保持在2048,因此需要更少的内存
- 内存消耗的每一秒跳跃并不伴随着减少:唯一的元素存储在向量中,向量的大小每一秒都会增加一倍。元素必须被复制,这样两倍的内存被提交/使用,从而隐藏了哈希映射的较小使用
- 在这两者之间,消耗量呈线性增加:随着元素添加到unique的向量中,越来越多的内存页被提交:used
zero不提交它的额外内存(至少在Linux上)np。resize
下面是一个小实验,它表明,
np.zero(…)
不会提交内存,而是只保留内存:
import numpy as np
import psutil
process = psutil.Process()
old = process.memory_info().rss
a=np.zeros(10**8)
print("commited: ", process.memory_info().rss-old)
# commited: 0, i.e. nothign
a[0:100000] = 1.0
print("commited: ", process.memory_info().rss-old)
# commited: 2347008, more but not all
a[:] = 1.0
print("commited: ", process.memory_info().rss-old)
# commited: 799866880, i.e. all
注意:
a=np.full(10**8,0.0)
将直接提交内存。每次内存使用量大幅增加时,步骤的大小都会加倍,这与内存分配算法的工作方式相同,并且在网络冲突中会出现指数退避。这只是一个观察。感谢您的分析,但不知何故,我不知道该如何解释这种奇怪的行为(即,较小输入的内存较少),例如,序列中的所有值都在增长。我是否遗漏了你答案中的关键点,这可以解释为什么?你可能什么都没遗漏。我知道答案无论如何都不是一个完整的答案。。。无论如何,这是我的代码——我想重现结果,我看到了相同的内存使用模式。
sh run_perf_tests.sh 2>&1 | python show_memory.py
>>> [2 ** (2 * i - 1) for i in range(4, 20)]
[
128,
512,
2048,
8192,
32768,
131072,
524288,
2097152,
8388608,
33554432,
134217728,
536870912,
2147483648,
8589934592,
34359738368,
137438953472,
...,
]
def unique(values):
...
table = htable(len(values))
...
cdef class {{name}}HashTable(HashTable):
def __cinit__(self, int64_t size_hint=1):
self.table = kh_init_{{dtype}}()
if size_hint is not None:
size_hint = min(size_hint, _SIZE_HINT_LIMIT)
kh_resize_{{dtype}}(self.table, size_hint)
SCOPE void kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets)
...
import numpy as np
import psutil
process = psutil.Process()
old = process.memory_info().rss
a=np.zeros(10**8)
print("commited: ", process.memory_info().rss-old)
# commited: 0, i.e. nothign
a[0:100000] = 1.0
print("commited: ", process.memory_info().rss-old)
# commited: 2347008, more but not all
a[:] = 1.0
print("commited: ", process.memory_info().rss-old)
# commited: 799866880, i.e. all