我已声明并定义了以下HashTable类。请注意,我需要哈希表的哈希表,所以我的HashEntry结构包含一个HashTable指针。公共部分与传统的哈希表函数并不是什么大不了的,所以为了简单起见我删除了它们。
enum Status{ACTIVE, DELETED, EMPTY};
enum Type{DNS_ENTRY, URL_ENTRY};
class HashTable{
private:
struct HashEntry{
std::string key;
Status current_status;
std::string ip;
int access_count;
Type entry_type;
HashTable *table;
HashEntry(
const std::string &k = std::string(),
Status s = EMPTY,
const std::string &u = std::string(),
const int &a = int(),
Type e = DNS_ENTRY,
HashTable *t = NULL
): key(k), current_status(s), ip(u), access_count(a), entry_type(e), table(t){}
};
std::vector<HashEntry> array;
int currentSize;
public:
HashTable(int size = 1181, int csz = 0): array(size), currentSize(csz){}
};
我正在使用二次探测,当我点击array.size()/2
时,我在rehash函数中将矢量的大小加倍。当需要更大的表格大小时,使用以下列表。
int a[16] = {49663, 99907, 181031, 360461,...}
我的问题是这个类消耗了这么多内存。我只是用massif对它进行了分析,发现它需要33MB(3300万字节!)的内存才能插入125000个内存。要明确,实际上
1 insertion -> 47352 Bytes
8 insertion -> 48376 Bytes
512 insertion -> 76.27KB
1000 insertion 2MB (array size increased to 49663 here)
27000 insertion-> 8MB (array size increased to 99907 here)
64000 insertion -> 16MB (array size increased to 181031 here)
125000 insertion-> 33MB (array size increased to 360461 here)
这些可能是不必要的,但我只想告诉你内存使用情况如何随输入而变化。正如您所看到的,当重新执行rehashing时,内存使用量会翻倍。例如,我们的初始数组大小是1181.我们刚看到125000个元素 - &gt; 33MB。
要调试问题,我将初始大小更改为360461.现在127000插入不需要重新散列。我看到这个初始值使用了20MB的内存。这仍然是巨大的,但我认为这表明重新出现存在问题。以下是我的rehash功能。
void HashTable::rehash(){
std::vector<HashEntry> oldArray = array;
array.resize(nextprime(array.size()));
for(int j = 0; j < array.size(); j++){
array[j].current_status = EMPTY;
}
for(int i = 0; i < oldArray.size(); i++){
if(oldArray[i].current_status == ACTIVE){
insert(oldArray[i].key);
int pos = findPos(oldArray[i].key);
array[pos] = oldArray[i];
}
}
}
int nextprime(int arraysize){
int a[16] = {49663, 99907, 181031, 360461, 720703, 1400863, 2800519, 5600533, 11200031, 22000787, 44000027};
int i = 0;
while(arraysize >= a[i]){i++;}
return a[i];
}
这是rehashing和其他任何地方使用的插入函数。
bool HashTable::insert(const std::string &k){
int currentPos = findPos(k);
if(isActive(currentPos)){
return false;
}
array[currentPos] = HashEntry(k, ACTIVE);
if(++currentSize > array.size() / 2){
rehash();
}
return true;
}
我在这里做错了什么?即使它是由重新发生引起的,当没有进行重复时,它仍然是20MB,我相信20MB对于10万件物品来说太多了。这个哈希表应该包含800万个元素。
答案 0 :(得分:2)
由于您使用的是开放式寻址,因此一半的哈希槽必须为空。由于HashEntry非常大,所以在每个空槽中存储一个完整的HashEntry是非常浪费的。
您应该将HashEntry结构存储在其他位置并将HashEntry *放在哈希表中,或者使用更密集的加载因子切换到链接。任何一方都会减少这种浪费。
此外,如果你要移动HashEntry对象,交换而不是复制,或使用移动语义,这样你就不必复制那么多字符串。请务必清除您不再使用的任何条目中的字符串。
另外,即使你说你需要HashTables的HashTables,你也不能解释原因。如果小哈希表不具有内存效率,那么使用一个具有有效表示的复合键的哈希表通常会更有效。
答案 1 :(得分:2)
360,461 HashEntry占用20 MB的事实并不令人惊讶。你试过看sizeof(HashEntry)
了吗?
每个HashEntry包括两个std :: strings,一个指针和三个int。正如老笑话所说的那样,回答这个问题并不容易。&#34;字符串有多长?&#34;,在这种情况下,因为有各种各样的字符串实现和优化,所以你可能会发现sizeof(std::string)
介于4到32个字节之间。 (在32位架构上只有4个字节。)实际上,字符串需要三个指针和字符串本身,除非它恰好是空的。如果sizeof(std :: string)与sizeof(void *)相同,那么你可能得到了一个不太新的GNU标准库,其中std::string
是一个不透明的指针包含两个指针的块,引用计数和字符串本身。如果sizeof(std :: string)是32个字节,那么你可能有一个最近的GNU标准库实现,其中字符串结构中有一些额外的空间用于短字符串优化。有关某些测量,请参阅Why does libc++'s implementation of std::string take up 3x memory as libstdc++?的答案。我们只说每个字符串32个字节,并忽略细节;它不会受到太大的影响。
因此两个字符串(每个32字节)加上一个指针(8个字节)加上三个整数(另外12个字节)和四个字节的填充,因为其中一个整数位于两个8字节对齐的对象之间,并且&#39;每个HashEntry总共88个字节。如果你有360,461个哈希条目,那将是31,720,568字节,大约30 MB。事实上,你只是&#34;只有&#34;使用20MB可能是因为您使用旧的GNU标准库,它将空字符串优化为单个指针,并且大多数字符串都是空字符串,因为从未使用过一半的插槽。
现在,让我们来看看rehash。简化为基本要素:
void rehash() {
std::vector<HashEntry> tmp = array; /* Copy the entire array */
array.resize(new_size()); /* Internally does another copy */
for (auto const& entry : tmp)
if (entry.used()) array.insert(entry); /* Yet another copy */
}
在最高峰时,我们有两个较小阵列的副本以及新的大阵列。即使新阵列只有20 MB,峰值内存使用量几乎是其两倍也就不足为奇了。 (实际上,这又是一个惊人的小,并不奇怪的大。可能实际上没有必要更改新向量的地址,因为它位于当前分配的内存空间的末尾,可以只是扩展。)
请注意,我们对所有数据进行了两次复制,而array.resize()
可能会执行另一次。让我们看看我们是否可以做得更好:
void rehash() {
std::vector<HashEntry> tmp(new_size()); /* Make an array of default objects */
for (auto const& entry: array)
if (entry.used()) tmp.insert(entry); /* Copy into the new array */
std::swap(tmp, array); /* Not a copy, just swap three pointers */
}
这样,我们只做一个副本。通过调整大小而不是(可能的)内部副本,我们对新元素进行批量构造,这应该是类似的。 (它只是把记忆归零。)
此外,在新版本中,我们只复制实际的字符串一次,而不是每次复制两次,这是副本中最狡猾的部分,因此可能节省很多。
适当的字符串管理可以进一步减少开销。 rehash实际上并不需要复制字符串,因为它们没有被更改。因此我们可以将字符串保留在别处,比如在字符串向量中,并且只需将索引用于HashEntry中的向量。由于您不希望持有数十亿字符串,只有数百万字符串,因此索引可能是一个四字节的int。通过对HashEntry字段进行混洗并将枚举减少到一个字节而不是四个字节(在C ++ 11中,您可以指定枚举的基础整数类型),HashEntry可以减少到24个字节,并且不会&# 39;需要为多个字符串描述符留出空间。
答案 2 :(得分:1)
我已经像你们所建议的那样改变了我的结构,但有一件事没有人注意到。
在进行重组/调整大小时,我的rehash
函数会调用insert
。在这个插入函数中,我正在递增currentSize
,它保存散列表有多少元素。因此,每次需要调整大小时,currentSize会自动翻倍,而它应该保持不变。我删除了该行并编写了正确的代码以进行重复,现在我觉得我没事。
我现在使用两种不同的结构,并且该程序为800万个元素消耗1.6GB内存,这是由于多字节字符串和整数所预期的。这个数字之前就像是7-8GB。