如何减少包含超过 400 万个字符串对象的字典的内存占用?
它目前消耗大约 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_id: 整数
- name: string, 每个平均大概13个字符
- 类型:整数
根据一些网络搜索,在我看来,由于字符串对象需要大量内存,User.name 占用了大部分空间。
我已经通过使用 __slots__ 将占用空间从大约 2GB 减少到 1.5GB,但我需要进一步减少它。
请您参考如下方法:
如果您确实需要本地数据,请考虑将其保存到主机上的 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 是连续的,并且您正在使用固定长度的结构,您还可以通过计算字节偏移量来查找简单数组,而不是使用字典。 (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)
但是,我不相信这是针对您实际遇到的问题的最佳设计。其他替代方案包括:
- 检查您的数据库设计,尤其是您的索引
- 使用 memcache/redis 缓存最常用的记录
