[英]Curious memory consumption of pandas.unique()
在分析我的算法的內存消耗時,我很驚訝有時對於較小的輸入需要更多的內存。
這一切都歸結為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
。
掃描不同的輸入大小會產生下圖:
出於好奇和自我教育:如何解釋N=5*10^7
, N=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
讓我們來看看...
pandas.unique
說它是“基於哈希表的唯一”。
它調用此函數為您的數據獲取正確的哈希表實現,即htable.Int64HashTable
。
哈希表使用size_hint
= 值向量的長度進行初始化。 這意味着kh_resize_DTYPE(table, size_hint)
被調用。
這些函數在khash.h
中定義(模板化)。
它似乎為存儲桶分配了(size_hint >> 5) * 4 + (size_hint) * 8 * 2
字節的內存(可能更多,可能更少,我可能會離開這里)。
然后,調用HashTable.unique()
。
它分配一個空的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,
...,
]
希望這對事情有所啟發:)
@AKX 的回答解釋了為什么內存消耗在跳躍時會增加,但沒有解釋為什么它會隨着更多元素而減少——這個答案填補了空白。
pandas
使用khash
-map 來查找唯一元素。 創建哈希映射時,數組中的元素數用作提示:
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)
然而 khash-map 理解它,因為我們至少需要n
桶(而不是我們需要一個放置n
元素的地方):
SCOPE void kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets)
...
關於 khash-map 中桶數的兩個重要實現細節:
后果是什么? 讓我們看一個有 1023 個元素的數組:
一個有 1025 個元素的數組會發生什么?
這種內存消耗模式將隨着數組大小的每增加一倍而發生。 這是我們觀察到的。
對較小影響的解釋:
np.resize
將不提交的np.resize
內存歸零(至少在 Linux 上)。 這是一個小實驗,表明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
注意: a=np.full(10**8, 0.0)
將直接提交內存。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.