Skip to main content
 首页 » 编程设计

python之如何减少包含 4M+ 字符串对象的字典的内存占用

2025年05月04日15mate10pro

如何减少包含超过 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 缓存最常用的记录