用于大 N 的字符串容器
String container for large N
我正在寻找适合大量字符串(> 10^9)的字符串容器。 字符串具有可变长度。 它必须快速插入和查找,并且具有节俭的内存使用。填充容器时字符串是无序的。平均字符串长度约为 10 个字节。对确切的字符串值进行查找。可擦除性 - 可选。N事先是未知的。适用于 64 位架构。用例 - 考虑 AWK 的关联数组。
map<string>
每根弦大约有 20-40 个咬合,每次插入调用一个 malloc(或两个)。所以它不快也不节俭。
有人可以指出我 C/C++ 库、数据结构或论文吗?
Relavant -- 哈希表库的比较
编辑我删除了"大数据",将N提升到更大的值,澄清了需求。
没有灵丹妙药,但基数树具有 trie 的优势(快速查找和插入,至少是渐近的)——空间消耗更好。
但是 - 两者都被认为不是"缓存效率" - 这可能很重要,尤其是在某个时候需要迭代数据时。
对于您的问题,64 位计算机上的指针几乎与数据的长度相匹配。因此,在问题中为每个字符串使用多个指针(平均长度小于 10 字节)将使数据结构的大小主导输入的大小。
处理此问题的一种常规方法是不使用指针来表示字符串。使用 32 位偏移量到存储所有字符串的大页面中的专用表示形式将使指针内存要求减半,但代价是需要对指针进行添加以检索字符串。
编辑:下面是这种表示的示例(未经测试的)实现(为简单起见,使用struct
,实际实现当然只会使用户界面公开)。该表示形式假定哈希表插入,因此为next_
留出空间。请注意,偏移量按 hash_node
大小缩放,以允许以 32 位偏移量表示。
struct hash_node {
uint32_t next_;
char * str () { return (const char *)(&next+1); }
const char * str () const { return (const char *)(&next+1); }
};
struct hash_node_store {
std::vector<hash_node> page_; /* holds the allocated memory for nodes */
uint32_t free_;
hash_node * ptr (uint32_t offset) {
if (offset == 0) return 0;
return &page_[offset-1];
}
uint32_t allocate (const char *str) {
hash_node *hn = ptr(free_);
uint32_t len = strlen(str) + 1;
uint32_t node_size =
1 + (len / sizeof(hash_node)) + !!(len % sizeof(hash_node));
strcpy(hn->str(), str);
free_ += node_size;
return 1 + (hn - &page_[0]);
}
};
哈希表将包含一个节点存储和一个哈希桶向量。
struct hash_table {
hash_node_store store_;
std::vector<uint32_t> table_; /* holds allocated memory for buckets */
uint32_t hash_func (const char *s) { /* ... */ }
uint32_t find_at (uint32_t node_offset, const char *str);
bool insert_at (uint32_t &node_offset, const char *str);
bool insert (const char *str) {
uint32_t bucket = hash_func(s) % table_.size();
return insert_at(table_[bucket], str);
}
bool find (const char *str) {
uint32_t bucket = hash_func(s) % table_.size();
return find_at(table_[bucket], str);
}
};
其中find_at
和insert_at
只是以预期方式实现的简单功能。
uint32_t hash_table::find_at (uint32_t node_offset, const char *str) {
hash_node *hn = store_.ptr(node_offset);
while (hn) {
if (strcmp(hn->str(), str) == 0) break;
node_offset = hn->next_;
hn = store_.ptr(node_offset);
}
return node_offset;
}
bool hash_table::insert_at (uint32_t &node_offset, const char *str) {
if (! find_at(node_offset, str)) {
uint32_t new_node = store_.allocate(str);
store_.ptr(new_node)->next_ = node_offset;
node_offset = new_node;
return true;
}
return false;
}
由于您只是插入值,因此字符串数据本身可以在插入时连接起来 - 每个都带有分隔符,例如 NUL。 该单个缓冲区中的字符偏移量唯一标识字符串。 这意味着共享公共子字符串的字符串集将完全冗余地单独指定,但反驳说,不会花费任何精力来查找或编码这种分解:这可能会适得其反高度不相关的字符串值(例如随机文本)。
要查找字符串,可以使用哈希表。 鉴于您的目标是避免频繁的动态内存分配,为了有效地处理冲突,您需要使用置换列表:这个想法是,当插入一个哈希到已使用的存储桶的字符串时,您可以添加一个偏移量(如有必要,环绕表)并尝试另一个存储桶,继续直到找到空存储桶。 这意味着您需要一个位移列表来尝试:您可以手动编码有限列表以帮助您入门,甚至可以在"大位移"列表上嵌套循环,该列表的值被添加到"小位移"列表中的值中,直到找到空桶,例如,两个手动编码的 10 个位移列表产生 100 种组合。 (可以使用替代哈希算法代替置换列表或与置换列表结合使用。 不过,您确实需要有一个合理的总桶与已用桶的比例......我希望 1.2 左右的东西通常可以正常工作,较大的值优先考虑速度而不是空间 - 您可以使用示例数据填充您的系统并根据口味进行调整。
因此,空间要求是:
total_bytes_of_string_data + N delimiters + total_buckets * sizeof(string_offset)
其中 sizeof(string_offset) 可能需要 8 个字节,因为 10^9 * 10 已经超过 2^32。
对于 ~10 个字符和 1.2*10^9 个存储桶的 10^9 字符串,这大约是 10^9 * (10+1) + 1.2*10^9 * 8 字节 = 20.6^10^9 字节或 19.1 GB。
值得注意的是,64 位虚拟地址空间意味着您可以安全地为串联字符串数据和哈希表分配比实际需要的更多的空间,并且只有那些实际访问的页面需要虚拟内存(最初是物理内存,但以后可以通过正常的虚拟内存机制交换到磁盘)。
讨论
如果没有对所用字符串数据或字符集中的重复性的假设/见解,就无法保证减少字符串内存使用量。
如果所有插入之后都进行了大量搜索,则对字符串数据进行排序并使用二进制搜索将是一个理想的解决方案。 但是,对于穿插搜索的快速插入,上述内容是合理的。
你也可以有一个基于平衡二叉树的索引,但为了避免每次插入的内存分配,你需要将很多节点分组到一个内存页中,并在不太精细的级别上手动管理它们的排序和拆分:实现起来很痛苦。 可能已经有一个图书馆在做,但我还没有听说过。
您已经添加了"AWK 中的关联数组"作为其用途的示例。 您只需将每个映射到的值紧跟在串联数据中的字符串键之后即可。
(低)误报率是否可以接受?如果是这样,那么布隆过滤器将是一种选择。如果您满足于百万分之一或 2^(-20) 的误报率,则需要使用大约是预期字符串数 30 倍的缓冲区大小,即 3*10^10 位。这不到4GB。您还需要大约 20 个独立的哈希函数。
如果您不能接受误报,您应该考虑在您构建的任何其他解决方案之前放置一个 Bloom 过滤器,以快速清除大多数负面。
- 重载操作程序时出错>>用于类中的字符串 memebr
- 一个函数,用于查找字符串1包含字符串2 c++的次数
- 具有多个模板的模板函数,用于特定数据类型(如字符串)?
- 用于存储由空格分隔的字符串的 C++/C 数据结构
- 需要帮助在 c++ 中将字符串转换为字符 ----错误 "const char *" 类型的值不能用于初始化 "char" 类型的实体
- 组合字符串不适用于 libCurl,C++
- 重载 std::字符串运算符+ 用于打印枚举名称
- 用于子集字符串的 Rcpp 函数
- C++ 虽然语句不适用于字符串和"or"
- C++ 用于在数组中打印字符串的随机数
- 用于拆分空格字符串的程序不起作用
- 用于从 ANSI 字符串转换为 std::basic_string <TCHAR>的正确函数声明
- C ++ txt 文件成数组逐行字符串和 int 用于纸牌游戏
- 为什么我的 if 语句不适用于 c++ 中的字符串?
- C++自定义流操纵器,用于更改流上的下一个字符串
- 如何在 python 包装中使用 unicode 字符串用于带有 cython 的 c++ 类?
- 将 Delphi 的字符串转换为 std::字符串用于C++
- C :将文本文件的内容存储在2D数组中,作为字符串(用于NULL终结器的麻烦?)
- 如何在 tchar 中添加格式化字符串(用于多个文件名等)
- Google协议缓冲区和std::字符串用于任意二进制数据