C++ 哈希表需要大量内存

C++ 哈希表需要大量内存,c++,data-structures,hashtable,C++,Data Structures,Hashtable,我声明并定义了以下哈希表类。请注意,我需要一个哈希表的哈希表,以便HashEntry结构包含一个哈希表指针。公共部分没什么大不了的,它有传统的哈希表函数,所以为了简单起见,我删除了它们 enum Status{ACTIVE, DELETED, EMPTY}; enum Type{DNS_ENTRY, URL_ENTRY}; class HashTable{ private: struct HashEntry{ std::string key; Statu

我声明并定义了以下哈希表类。请注意,我需要一个哈希表的哈希表,以便HashEntry结构包含一个哈希表指针。公共部分没什么大不了的,它有传统的哈希表函数,所以为了简单起见,我删除了它们

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){}
};
我的问题是这个类占用了太多内存。我刚刚用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)
这些可能是不必要的,但我只是想向您展示内存使用情况如何随输入而变化。正如您所看到的,当重新灰化完成时,内存使用量会翻倍。例如,我们的初始数组大小是1181。我们刚刚看到125000个元素->33MB

为了调试这个问题,我将初始大小更改为360461。现在,127000个插入不需要重新灰化。我看到这个初始值使用了20MB的内存。这仍然是巨大的,但我认为这表明重新洗牌存在问题。下面是我的再灰化功能

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];
}

我做错了什么?即使它是由再灰化引起的,当没有再灰化时,它仍然是20MB,我相信20MB对于10万个项目来说太多了。这个哈希表应该包含大约800万个元素。

因为您使用的是开放寻址,所以一半的哈希槽必须是空的。由于HashEntry相当大,在每个空插槽中存储一个完整的HashEntry是非常浪费的

您应该将HashEntry结构存储在其他地方,并将HashEntry*放入哈希表中,或者切换到负载因子更密集的链接。任何一个都可以减少这种浪费

另外,如果要移动HashEntry对象,可以交换而不是复制,或者使用移动语义,这样就不必复制那么多字符串。确保清除不再使用的任何条目中的字符串


此外,即使您说需要哈希表中的哈希表,您也没有真正解释原因。如果小哈希表没有内存效率,那么使用一个哈希表和高效表示的复合键通常会更有效。

360461 HashEntry占用20 MB这一事实并不令人惊讶。您是否尝试过查看
sizeof(HashEntry)

每个HashEntry包括两个std::strings、一个指针和三个int。正如老笑话所说,回答“字符串有多长?”这个问题并不容易,在本例中,因为有大量的字符串实现和优化,所以您可能会发现
sizeof(std::string)
在4到32字节之间。(在32位体系结构上只有4个字节。)实际上,字符串需要三个指针和字符串本身,除非它恰好为空。如果sizeof(std::string)与sizeof(void*)相同,那么您可能得到了一个不太新的GNU标准库,
std::string
是一个不透明的指针,指向一个包含两个指针、一个引用计数和字符串本身的块。如果sizeof(std::string)是32字节,那么您可能有一个最近的GNU标准库实现,其中字符串结构中有一些额外的空间用于短字符串优化。有关一些测量值,请参见的答案。让我们只说每个字符串32字节,忽略细节;不会差太多

因此,两个字符串(每个32字节)加上一个指针(8字节)加上三个整数(另外12个字节)和四个字节的填充,因为其中一个整数位于两个8字节对齐的对象之间,每个HashEntry总共88字节。如果你有360461个散列条目,那就是31720568字节,大约30MB。您“仅”使用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 */
}
这样,我们只复制一份。我们对新元素进行批量构造,而不是(可能的)通过调整大小进行内部复制,这应该是类似的。(这只是将内存归零。)

此外,在新版本中,我们只复制实际字符串一次,而不是两次,这是副本中最烦琐的部分,因此可能会节省大量资源


适当的字符串管理可以进一步减少开销。rehash实际上不需要复制字符串,因为它们不会更改。因此,我们可以将字符串保留在其他位置,比如字符串向量中,只需在HashEntry的向量中使用索引。由于您不希望保存数十亿个字符串,只有数百万个,因此索引可以是一个四字节的int。通过对HashEntry字段进行混洗,并将枚举减少到一个字节而不是四个字节(在C++11中,您可以指定枚举的基本整数类型),HashEntry可以减少到24个字节,没有必要为这么多的字符串描述符留出空间。

我已经按照大家的建议稍微改变了我的结构,但有一件事没有人注意到

重新设置大小时,my
rehash
函数调用
insert
。在这个insert函数中,我增加了
currentSize
,它保存了一个哈希表有多少个元素。因此,每次需要调整大小时,currentSize都会加倍,而它本应保持不变。我删除了这一行,并编写了适当的代码进行重新灰化,现在我想我没事了


我现在使用两种不同的结构,程序为800万个元素消耗了1.6GB内存,这是由于多字节字符串和整数所造成的。这个数字以前是7-8GB。

是否有理由为每个条目存储整个表?如果您可以发布代码,将
HashTable
分配给
HashEntry
@Jason,则可能会有所帮助。哈希表的每个条目都可以在其条目中包含一个HashTable。我想不起来了
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;
}
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 */
}
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 */
}