繁体   English   中英

Python(或C)中内存有效的字符串到字符串映射

[英]Memory-efficient string-to-string map in Python (or C)

我需要一个内存高效的数据结构来存储大约一百万个键 - 值对,其中键是大约80字节的字符串,值是大约200字节的字符串,总键和值大小约为280MB。 我还需要通过键有效地查找值,最好是哈希映射。 内存开销应尽可能少,例如对于280MB的有用数据,数据结构不应使用超过300MB的虚拟内存(包括malloc()开销和其他所有内容)。 使用模式如下:我们从一个空的数据结构开始,我们逐渐填充它,从不更改键,也从不改变值的长度。 作为一个加号,数据结构可能支持更改值的长度,代价是100%的值开销(意味着对于x值字节,x个字节可能暂时浪费在未使用的缓冲区空间中)。

我需要一个纯Python模块,或一个内置的Python模块,或一个C实现,最好是(C)Python绑定。 我更喜欢是否可以将整个数据结构序列化到磁盘,并且可以非常快速地将其读回。

为了证明这么小的开销是可能的,我创建了一个带开放寻址的简单设计,包含1个数据块的4字节指针的125万个元素的哈希表,包含密钥和值长度的数据块作为基础-128 varints 这种设计有一个重要的限制:它不允许在不浪费存储区域的情况下移除或更换线对。 根据我的计算,有100万个密钥值对,每个280字节,开销小于3.6%(10 080 000字节)。 上述限制更加慷慨,它们允许20 000 000字节的开销。

我刚刚找到了http://www.pytables.org/ ,它提供了快速访问和内存高效的数据打包。 我必须仔细检查它以检查它是否符合我的需要。

好的,简单的方法。

使用python字典表示数据结构。 我用一百万个随机键值对填充了一个python字典,其中键是80个字符,值是200个字符。 它在我的计算机上耗费了360,844 Kb,超出了你的规格不超过300 MB,但无论如何我提供它作为解决方案,因为它仍然非常有效。

这也无法满足您对C API的要求。 我不确定你为什么需要C,但由于问题是标记为Python并且没有C标签,我将提供纯Python以查看它是否适合该法案。

关于坚持。 使用cPickle模块。 它非常快,而且很简单。 要保存字典:

cPickle.dump(mydict, "myfile.pkl")

要重新加载字典:

mydict = cPickle.load("myfile.pkl")

第二个简单的想法是使用shelve模块,它基本上是基于磁盘的python字典。 内存开销非常低(全部在磁盘上)。 但它也慢得多。

Martijn在评论中提到了这一点(不确定人们为什么评论答案),但我同意:使用SQLite。 你应该试一试,看看它是否能满足你的需求。

如果您不打算进行大量删除,那么这并不难。 删除会导致碎片化。

您还需要提交固定长度的密钥。 你提到了80个字节。 您的密钥是否允许复制? 如果没有,那就更容易了。

所以,这就是你做的。

您创建一个数组:

struct {
    char value[80];
    char *data;
} key;

你保持这个数组排序。

如果您的密钥可以复制,那么您需要:

struct link {
    char *data;
    link *next;
}

struct {
    char value[80];
    link *data;
} key;

(我的C是生锈的,但这是它的要点)后者的每个键都指向一个链接的值列表。

然后查找是一个简单的二进制搜索。 “痛苦”是维护这个数组和插入/删除键。 它并不像听起来那么痛苦,但它可以节省大量内存,特别是在64位系统上。

你想要减少的是指针的数量。 当你有很多充满指针的结构时,指针是昂贵的。 在64位系统上,指针是8个字节。 因此,对于单个指针,您的内存预算将达到8MB。

所以,费用是建立数组,复制和压缩内存(如果你“知道”你将有一百万行并且可以提交,然后立即malloc(1000000 * sizeof(key)),它将为你节省扩展期间的一些复制)。

但是不要害怕,一旦它启动并运行,性能就相当不错。 现代cpu实际上非常擅长复制100M内存块。

另外,我只是在Java中做了类似的事情。 在64位JVM上,具有25M条目的映射是2G的RAM。 我的解决方案(使用类似的技术)大约600M)。 Java使用的指针多于C,但前提是相同的。

你尝试过使用简单的词典吗? 您的大多数数据都是字符串,因此开销可能符合您的要求。

您可以使用密钥的sha1而不是密钥本身。 如果键是唯一的,那么键的sha1哈希也是可能的。 它可以节省内存,以便在您的限制内发出吱吱声。

from random import choice
from string import letters
from hashlib import sha1

def keygen(length):
    return "".join(choice(letters) for _ in xrange(length))

def gentestdata(n=1000*1000):
    # return dict((sha1(keygen(80)).digest(), keygen(200)) for _ in xrange(n))
    d = {}
    for _ in xrange(n):
        key = sha1(keygen(80)).digest()
        assert key not in d
        value = keygen(200)
        d[key] = value
    return d

if __name__ == '__main__':
    d = gentestdata()

在我的ubuntu盒子上,最高可达304 MB的内存:

2010-10-26 14:26:02 hbrown@hbrown-ubuntu-wks:~$ ps aux | grep python
[...]
hbrown   12082 78.2  7.5 307420 303128 pts/1   S+   14:20   4:47 python

足够近? 这是python,而不是C.


稍后:另外,如果您的数据有些多余,您可以gzip值。 这是时间与空间的权衡。

使用SQLite是个好主意。 快速实施可以很轻松地判断您是否足够快


如果您确定必须自己动手,我建议您使用以下内容:

您如何预测对的数量,或者它的上限?
您如何预测总数据大小或其上限?

竞技场分配器用于字符串和节点。 (通常,你会在竞技场列表上工作,所以你不必预测总大小)。

对齐取决于您的算法,原则上您可以按字节顺序打包,唯一的开销是您的分配,这只会对您的工作集产生最小的影响。

但是,如果你必须对这些字符串运行任何cmp / copy等操作,请记住,通过以下保证,你可以从这些字符串操作中挤出一点或很多:

  • 所有元素都是CPU字对齐的
  • 所有填充字节都是(例如)0
  • 只要不跨越CPU边界,就可以安全地读取“超出”字符串结尾

索引的哈希表 字典也可以起作用,但只有在潜在的退化/重组会成为严重问题时才有意义。 我不知道C的任何“股票”哈希表实现,但应该有一个,对吧? 对? 只需用对arena分配器的调用替换分配。


记忆局部性

如果您可以保证查找永远不会请求不在映射中的字符串,则应将密钥存储在单独的竞技场中,因为它们仅在散列冲突时需要。 这可以显着改善记忆位置。 (在这种情况下,如果你有一个“最终”表,你甚至可以将碰撞键复制到一个新的竞技场,然后扔掉所有其他的。虽然这样做的好处可能是微不足道的。)

根据您的访问模式,分离可能会有所帮助或受到伤害。 如果您通常在每次查找后使用该值一次,那么在同一个舞台上成对使用它就很棒。 如果您例如查找几个键,然后重复使用它们的值,单独的竞技场是有意义的。


如果必须支持“有趣的字符”/ Unicode,请在存储字符串之前对其进行规范化。

Judy应该具有记忆效率: http//judy.sourceforge.net/
(基准: http//www.nothings.org/computer/judy/ ,请参阅“数据结构大小”)。
另见: http//www.dalkescientific.com/Python/PyJudy.html

也,

对于固定大小的密钥,在C ++中有http://panthema.net/2007/stx-btree/ (我确信使用自定义C包装器可以在CPython中使用它)。 如果数据集允许,您可以将可变长度键存储在值中,并使用可变长度键的哈希值或前缀作为固定长度键。

相同的逻辑适用于http://google-opensource.blogspot.ru/2013/01/c-containers-that-save-memory-and-time.htmlhttp://code.google.com/p/sparsehash / - 不使用重std :: string作为键,使用32位或64位整数键,从实际可变长度键以某种方式。

您可以使用struct module来打包二进制数据,并在需要时将其解压缩。 您可以使用此方法实现内存高效的存储。 我猜访问会很痛苦。

Apache Portable Runtime(又名APR)有一个基于c的哈希表。 您可以在http://apr.apache.org/docs/apr/0.9/group_ apr _hash.html上查看文档。

所有你存储的apr_hash_t都是无效*。 因此,它可以让您完全控制值。 因此,如果您愿意,可以存储指向100字节块的指针,而不是字符串的实际长度。

由于我找不到任何能够紧密包装内存的现有解决方案,我决定自己在C中实现它。 in the question. 在问题中用查看我的设计。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM