如何减少带有4M +带字符串对象的字典的内存占用?

时间:2018-08-18 00:02:38

标签: python python-3.x

如何减少包含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_id:int
  • 名称:字符串,每个平均大约13个字符
  • 类型:int

在某些网络搜索中,由于字符串对象需要大量内存,因此User.name似乎占用了大部分空间。

我已经通过使用__slots__将占用空间从大约2GB减少到1.5GB,但是我需要进一步减少占用空间。

5 个答案:

答案 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)

但是,我不认为这是针对您实际遇到的问题的最佳设计。其他选择包括:

  • 检查数据库设计,尤其是索引
  • 使用memcache / redis缓存最常用的记录

答案 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有两个问题,但都可以解决:

  • 取决于基础实现,一个巨大的数据库可能不起作用,或者运行得很慢。您可以使用像KyotoCabinet这样的现代变体来解决该问题,但需要第三方包装。
  • 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的大小)。