如何减少包含4M +个带字符串对象的字典的内存占用?
它目前消耗约1.5 GB的RAM,由于成本过高(基于云),我需要在资源有限的系统上向其添加数百万个对象。
下面是一些简化的代码,它们说明了我在做什么。基本上,我从数据库中获取了大约400万用户,并将所有信息放入所有用户的本地字典中以便快速访问(出于性能原因,我必须使用用户数据的本地副本)。
import pymysql
class User:
__slots__ = ['user_id', 'name', 'type']
def __init__(self):
user_id = None
name = None
type = None
cursor.execute("SELECT UserId, Username, Type FROM Users")
db_query_result = cursor.fetchall()
all_users = {}
for db_user in db_query_result:
user_details = User()
user_details.name = db_user[1]
user_details.type = db_user[2]
db_user_id = db_user[0]
all_users[str(db_user_id)] = user_details
在某些网络搜索中,由于字符串对象需要大量内存,因此User.name似乎占用了大部分空间。
我已经通过使用__slots__
将占用空间从大约2GB减少到1.5GB,但是我需要进一步减少占用空间。
答案 0 :(得分:2)
如果您确实需要本地数据,请考虑将其保存到主机上的SQLite数据库中,然后让SQLite为您将热数据集加载到内存中,而不是将所有数据都保留在内存中。
db_conn = sqlite3.connect(path_to_sqlite_file)
db_conn.execute('PRAGMA mmap_size={};'.format(mmap_size))
如果确实需要内存中的所有数据,请考虑将主机上的交换空间配置为更便宜的选择。操作系统会将较冷的内存页面交换到此交换空间。
当然,如果name
是大字符串,则始终可以使用gzip压缩字符串。其他技巧还包括:如果名称中重复出现单词,则使用索引进行重复数据删除。
您也可以使用结构代替类。
sys.getsizeof(u) # 64 bytes
sys.getsizeof(struct.pack('HB13s', 10, 1, b'raymond')) # 49 bytes
# unsigned short for user ID, unsigned byte for type, string with 13 bytes
如果您知道用户ID是连续的,并且正在使用固定长度的结构,则还可以通过计算字节偏移量来查找简单数组,而不是使用dict。 (在这里,numpy数组很有用。)
all_users = np.array([structs])
all_users = (struct0, struct1, struct2, ...) # good old tuples are OK too e.g. all_users[user_id] would work
对于更接近生产质量的东西,您将需要一个数据准备步骤,将这些结构附加到文件中,以便以后在实际使用数据时可以读取
# writing
with open('file.dat', mode='w+') as f:
for user in users:
f.write(user) # where user is a fixed length struct
# reading
with open('file.dat', mode='r') as f:
# given some index
offset = index * length_of_struct
f.seek(offset)
struct = f.read(length_of_struct)
但是,我不认为这是针对您实际遇到的问题的最佳设计。其他选择包括:
答案 1 :(得分:2)
13个字符的字符串的实际字符串存储(如果全部为Latin-1)仅占用13个字节,如果全部为BMP,则仅占用26个字节,如果具有来自Unicode的所有字符,则仅占用52个字节。
但是,str
对象的开销又是52个字节。因此,假设您主要使用的是Latin-1,则您所使用的存储量大约是需要的5倍。
如果您的字符串被编码为UTF-8或UTF-16-LE或最适合您的数据的字符串,并且大小都相同,那么您可能希望将它们存储在一个大的平面阵列中,然后将它们拉出并根据需要即时对其进行解码,如James Lim's answer所示。尽管我可能会使用NumPy本机结构化dtype而不是使用struct
模块。
但是,如果您有一些巨大的字符串,并且当大多数字符串只有10个字节长时,您不想为每个字符串浪费88个字节怎么办?
然后您要一个字符串表。这只是一个巨大的bytearray
,所有(编码)字符串都存放在其中,并且您将索引存储到该表中,而不是存储字符串本身。这些索引只是int32
或最坏的int64
值,您可以毫无问题地将它们打包到数组中。
例如,假设您的所有字符串都不超过255个字符,我们可以将它们存储为“ Pascal字符串”,其长度字节后跟编码字节:
class StringTable:
def __init__(self):
self._table = bytearray()
def add(self, s):
b = s.encode()
idx = len(self._table)
self._table.append(len(b))
self._table.extend(b)
return idx
def get(idx):
stop = idx + self._table[idx]
return self._table[idx+1:stop].decode()
所以现在:
strings = StringTable()
for db_user in db_query_result:
user_details = User()
user_details.name = strings.add(db_user[1])
user_details.type = strings.add(db_user[2])
db_user_id = strings.add(str(db_user[0]))
all_users[db_user_id] = user_details
当然,除了,您可能仍想用一个numpy数组替换那个all_users
。
答案 2 :(得分:1)
您应该使用cursor.fetchall()
来将结果集留在服务器端,而不是使用SSCursor
来将所有数据存储在客户端。
import pymysql
import pymysql.cursors as cursors
conn = pymysql.connect(..., cursorclass=cursors.SSCursor)
以便您可以一一读取行:
cursor = conn.cursor()
cursor.execute('SELECT UserId, Username, Type FROM Users')
for db_user in cursor:
user_details = User()
user_details.name = db_user[1]
user_details.type = db_user[2]
...
根据您要对all_users
字典执行的操作,您可能也不需要将所有用户信息都存储在字典中。如果您可以一个一个地处理每个用户,请直接在上方的for
循环内进行操作,而不要建立一个庞大的字典。
答案 3 :(得分:1)
您实际上是否需要此缓存的在内存中的 ,还是仅在本地系统上的 ?
如果是后者,则只需使用本地数据库。
由于您只想要像字典一样的内容,因此只需要key-value database。最简单的KV数据库是dbm
,Python开箱即用地支持该数据库。使用Python中的dbm
就像使用dict一样,只不过数据是在磁盘上而不是在内存中。
不幸的是,dbm
有两个问题,但都可以解决:
dbm
键和值只能是bytes
。 Python的dbm
模块将所有内容包装起来以允许透明地存储Unicode字符串,但除此之外没有其他内容。但是Python附带了另一个模块shelve
,该模块可让您透明地存储可以在dbm中腌制的任何类型的值。但是您可能想使用功能更强大的键值数据库,例如Dynamo或Couchbase。
实际上,您甚至可以仅使用Redis或Memcached之类的KV数据库(纯粹在内存中),因为它们将存储的数据要紧凑得多。
或者,您可以将数据从远程MySQL转储到本地MySQL甚至本地SQLite中(并且可以选择将ORM放在其前面)。
答案 4 :(得分:0)
借助recordclass,可以减少内存占用量:
from recordclass import dataobject
class User(dataobject):
__fields__ = 'user_id', 'name', 'type'
与基于__slots__
的实例相比,每个User实例现在所需的内存更少。
差异等于24个字节(PyGC_Head
的大小)。