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
,从开始,每当它们被填充时,它们的大小都会发生变化

然后它迭代你的值,计算它们是否在哈希表中;如果没有,它们将同时添加到哈希表和向量中。(这是向量可能增长的地方;由于大小提示,哈希表不需要增长。)

最后,一个NumPy
ndarray
指向向量

所以,呃,我想你会看到向量大小在某些阈值下翻了四倍(如果我深夜的数学正确的话

>>> [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%的桶,否则大小将加倍(+)
后果是什么?让我们来看一个有1023个元素的数组:

SCOPE void kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets)
...
  • 哈希映射在开始时将有1024个bucket(大于元素数的最小二次幂),但仅足够容纳约800个元素(即1024个中的77%)
  • 添加约800个元素后,大小将调整为2048个元素(下一次幂为2),这意味着峰值消耗将3072(需要同时使用新旧阵列)
对于包含1025个元素的数组,会发生什么情况

  • 散列映射在开始时将有2048个存储桶(比元素数大两个的最小幂),并且仅足够容纳约1600个元素(即2048年的77%)
  • 不会进行重新灰化,峰值消耗将保持在2048,因此需要更少的内存
阵列大小每增加一倍,就会出现这种内存消耗模式。这是我们观察到的

对较小影响的解释:

  • 内存消耗的每一秒跳跃并不伴随着减少:唯一的元素存储在向量中,向量的大小每一秒都会增加一倍。元素必须被复制,这样两倍的内存被提交/使用,从而隐藏了哈希映射的较小使用
  • 在这两者之间,消耗量呈线性增加:随着元素添加到unique的向量中,越来越多的内存页被提交:used
    np。resize
    zero不提交它的额外内存(至少在Linux上)

下面是一个小实验,它表明,
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