【问题标题】:hashtable needs lots of memory哈希表需要大量内存
【发布时间】:2016-01-01 23:36:35
【问题描述】:

我已经声明并定义了以下 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)

这些可能是不必要的,但我只是想向您展示内存使用情况如何随输入而变化。如您所见,重新散列完成后,内存使用量翻了一番。例如,我们的初始数组大小是 1181。而我们刚刚看到 125000 个元素 -> 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];
}

这是用于重新散列和其他任何地方的插入函数。

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 对于 100k 个项目来说太多了。这个哈希表应该包含大约 800 万个元素。

【问题讨论】:

  • 是否有理由为每个条目存储整个表?如果您可以发布将HashTable 分配给HashEntry 的代码,这可能会有所帮助。
  • @Jason 哈希表的每个条目都可以在其条目中包含一个哈希表。除了这个自我参照的定义,我想不出其他任何东西。当然感谢您的帮助,但我不明白您将 HashTable 分配给 HashEntry 是什么意思。这些是不同的类,可以互相分配吗?
  • @Jason 在这些分析过程中我也没有任何嵌套的哈希表。它只是一个主哈希表,其 HashEntries 中的 HashTables 为 NULL。
  • @A.S.H 在这些分析期间,没有嵌套的哈希表。所以我什至没有碰它们,我只是调用了插入函数并得到了这些内存使用情况。
  • 我猜“我们不允许”解释了您为什么不使用 std::unordered_map,这将是显而易见的解决方案。

标签: c++ data-structures hashtable


【解决方案1】:

360,461 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 标准库实现,其中字符串结构中有一些额外的空间用于短字符串优化。有关一些测量,请参阅Why does libc++'s implementation of std::string take up 3x memory as libstdc++? 的答案。假设每个字符串 32 个字节,忽略细节;它不会偏离太多。

所以两个字符串(每个 32 个字节)加上一个指针(8 个字节)加上三个整数(另外 12 个字节)和四个填充字节,因为其中一个整数在两个 8 字节对齐的对象之间,总共是每个 HashEntry 88 个字节。如果你有 360,461 个哈希条目,那将是 31,720,568 字节,大约 30 MB。您“仅”使用 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 个字节,并且不会不需要为尽可能多的字符串描述符留出空间。

【讨论】:

    【解决方案2】:

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

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

    此外,如果您要移动 HashEntry 对象,请交换而不是复制,或者使用移动语义,这样您就不必复制这么多字符串。请务必清除您不再使用的所有条目中的字符串。

    另外,即使你说你需要 HashTables 的 HashTables,你并没有真正解释为什么。如果小型散列表不节省内存,则使用具有高效表示的复合键的散列表通常更有效。

    【讨论】:

    • 是的,HashEntry 指针会减少内存,我会试试的。
    • 另外,问题是要写一个DNS Manager。所以我认为 DNS 值可以进入哈希表。每个 DNS 值都可以包含一个 URL 列表。我们应该注册 DNS 和 URL 并快速访问它们。这就是我想出嵌套哈希表的原因。但我想听听更好的解决方案,因为即使就时间而言,我的解决方案也可能不够。
    【解决方案3】:

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

    当重新散列/调整大小时,我的rehash函数调用insert。在这个插入函数中,我增加了currentSize,它保存了一个哈希表有多少个元素。因此,每次需要调整大小时, currentSize 都会翻倍,而它应该保持不变。我删除了该行并编写了正确的代码进行重新散列,现在我认为我没问题。

    我现在使用两个不同的结构,程序消耗 1.6GB 内存来存储 800 万个元素,这是多字节字符串和整数的预期结果。这个数字之前是 7-8GB。

    【讨论】:

      猜你喜欢
      • 2011-03-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-11-27
      • 2015-03-13
      • 2015-08-26
      • 2013-03-19
      相关资源
      最近更新 更多