pandas.unique()的奇怪内存消耗

时间:2018-07-23 19:19:14

标签: python algorithm performance pandas

在分析算法的内存消耗时,我感到惊讶的是,有时对于较小的输入,需要更多的内存。

这全部归结为pandas.unique()的以下用法:

import numpy as np
import pandas as pd
import sys

N=int(sys.argv[1])

a=np.arange(N, dtype=np.int64)
b=pd.unique(a)

使用N=6*10^7时,它需要3.7GB峰值内存,但是使用N=8*10^7“仅” 3GB时。

扫描不同的输入大小会产生下图:

enter image description here

出于好奇和自我教育:如何解释N=5*10^7N=1.3*10^7周围的违反直觉的行为(即,更多的内存用于更小的输入大小)?


以下是在Linux上生成内存消耗图表的脚本:

pandas_unique_test.py

import numpy as np
import pandas as pd
import sys

N=int(sys.argv[1])    
a=np.arange(N, dtype=np.int64)
b=pd.unique(a)

show_memory.py

import sys
import matplotlib.pyplot as plt   
ns=[]
mems=[]
for line in sys.stdin.readlines():
    n,mem = map(int, line.strip().split(" "))
    ns.append(n)
    mems.append(mem)
plt.plot(ns, mems, label='peak-memory')
plt.xlabel('n')
plt.ylabel('peak memory in KB')
ymin, ymax = plt.ylim()
plt.ylim(0,ymax)
plt.legend()
plt.show()

run_perf_test.sh

WRAPPER="/usr/bin/time -f%M" #peak memory in Kb
N=1000000
while [ $N -lt 100000000 ]
do
   printf "$N "
   $WRAPPER python pandas_unique_test.py $N
   N=`expr $N + 1000000`
done 

现在:

sh run_perf_tests.sh  2>&1 | python show_memory.py

2 个答案:

答案 0 :(得分:7)

让我们看看...

pandas.unique说它是“基于哈希表的唯一”。

它调用this function来获取数据的正确哈希表实现,即htable.Int64HashTable

哈希表初始化为size_hint =值向量的长度。这意味着kh_resize_DTYPE(table, size_hint)被呼叫。

定义(模板化)here in khash.h的那些功能。

似乎为存储桶分配了(size_hint >> 5) * 4 + (size_hint) * 8 * 2字节的内存(可能更多,也许更少,我可能不在这里)。

然后,调用HashTable.unique()

quadruple开始,它分配一个空的Int64Vector,它们看起来像128的大小一样。

然后遍历您的值,找出它们是否在哈希表中;如果不是,则将它们添加到哈希表和向量中。 (这是向量可能增长的地方;由于大小提示,哈希表不需要增长。)

最后,使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,
    ...,
]

希望这对事情有所启发:)

答案 1 :(得分:0)

@AKX答案解释了为什么内存消耗在跳转中增加,但没有解释,为什么它可能随着元素的增加而减少-这个答案填补了空白。

pandas使用khash映射来查找唯一元素。创建哈希映射后,数组is used as a hint中的元素数:

def unique(values):
    ...
    table = htable(len(values))
    ...

但是,meaning of the hint是“映射中将有n个值”:

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)

但是khash地图将其理解为we need at least n buckets(并且我们不需要n元素的位置)

SCOPE void kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets)
...

关于khash-map中存储桶数的两个重要实现细节:

  • 存储桶数为power of 2
  • 最多可容纳77%的存储桶,否则大小将增加一倍(here + here

会有什么后果?让我们看一下包含1023个元素的数组:

  • 散列图在开始时将具有1024个存储桶(比元素数大两个的最小乘方),但是仅足够用于ca。 800个元素(即1024个元素中的77%)。
  • 添加ca后。 800个元素,大小将调整为2048个元素(下一个2的幂),这意味着峰值消耗将为 3072 (需要同时使用旧阵列和新阵列)。 / li>

具有1025个元素的数组会发生什么?

  • 散列图在开始时将具有2048个存储桶(比元素数大两个的最小乘方),并且足够用于ca。 1600个元素(即2048个元素的77%)。
  • 不会进行任何哈希处理,峰值消耗将保持在 2048 ,因此需要较少的内存。

每增加一倍数组大小,就会发生这种内存消耗模式。这就是我们观察到的。

说明以产生较小的效果:

  • 每秒钟内存消耗的跳跃并没有减少:唯一元素存储在向量quadruples its size中(每秒钟翻一番就可见)。必须复制元素,这样就两次提交/使用内存,从而隐藏了哈希映射的较小用途。
  • 消耗之间的线性增长:随着元素被添加到唯一向量中,越来越多的内存页面被提交:用过的np.resize将没有提交的附加内存置零(至少在Linux上如此)。
  • li>

这是一个小实验,显示np.zeros(...)不会提交内存,只会保留它:

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

NB:a=np.full(10**8, 0.0)将直接提交内存。