You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

std::unordered_map reserve方法为何未减少内存分配次数?

如何减少std::unordered_map大量插入时的内存分配次数

问题原因

std::unordered_map::reserve的核心作用是预分配桶数组的内存,目的是提前设置足够的桶数量,避免插入过程中因哈希冲突触发频繁rehash。但它不会预分配存储键值对的节点内存——std::unordered_map的每个元素都是独立的链表节点,默认情况下每次插入都会单独分配一个节点的内存。你看到的1001次分配中,1次是桶数组的分配,剩下1000次是每个元素节点的单独分配,所以调用reserve不会改变这个次数。

解决方案

1. 实现内存池分配器

自定义分配器时加入内存池逻辑,预先分配一块足够容纳所有元素节点的连续内存,后续插入直接从内存池中取空间,避免多次小内存分配。

修改后的分配器示例:

#include <string>
#include <unordered_map>
#include <iostream>
#include <vector>
#include <cstddef>

using namespace std;

inline size_t AllocationCounter = 0;

template <typename T>
struct pool_alloc {
    using value_type = T;
    static constexpr size_t POOL_CAPACITY = 1000; // 匹配插入总量
    static alignas(T) char memory_pool[POOL_CAPACITY * sizeof(T)];
    static size_t used_slots;

    pool_alloc() = default;

    template <typename U>
    constexpr pool_alloc(const pool_alloc<U>&) noexcept {}

    T* allocate(std::size_t n) {
        // unordered_map每次只分配单个节点,所以只处理n=1的情况
        if (n != 1) {
            AllocationCounter++;
            return static_cast<T*>(::operator new(n * sizeof(T)));
        }
        if (used_slots >= POOL_CAPACITY) {
            AllocationCounter++;
            return static_cast<T*>(::operator new(sizeof(T)));
        }
        // 从内存池取空间,不计入分配计数器
        T* ptr = reinterpret_cast<T*>(memory_pool + used_slots * sizeof(T));
        used_slots++;
        return ptr;
    }

    void deallocate(T* p, std::size_t n) noexcept {
        // 仅释放内存池外的节点
        if (p < reinterpret_cast<T*>(memory_pool) || p >= reinterpret_cast<T*>(memory_pool + POOL_CAPACITY * sizeof(T))) {
            AllocationCounter--;
            ::operator delete(p);
        }
        // 内存池内的节点可选择回收复用,这里简化处理
    }

    template <typename... Args>
    void construct(T* p, Args&&... args) {
        new (p) T(std::forward<Args>(args)...);
    }

    void destroy(T* p) noexcept {
        p->~T();
    }
};

// 静态成员初始化
template <typename T>
alignas(T) char pool_alloc<T>::memory_pool[pool_alloc<T>::POOL_CAPACITY * sizeof(T)];

template <typename T>
size_t pool_alloc<T>::used_slots = 0;

void with_reserve_and_pool()
{
    using MapType = unordered_map<size_t, size_t, std::hash<size_t>, std::equal_to<size_t>, pool_alloc<std::pair<const size_t, size_t>>>;
    MapType m1;
    m1.reserve(1000); // 预分配桶数组,避免rehash

    for (size_t i = 0; i < 1000; i++) {
        m1.insert({i, i});
    }

    printf("With reserve + pool allocator - %llu Allocations\n", AllocationCounter);
    // 重置状态供下一次测试
    pool_alloc<std::pair<const size_t, size_t>>::used_slots = 0;
    AllocationCounter = 0;
}

void without_reserve()
{
    using MapType = unordered_map<size_t, size_t, std::hash<size_t>, std::equal_to<size_t>, pool_alloc<std::pair<const size_t, size_t>>>;
    MapType m1;

    for (size_t i = 0; i < 1000; i++) {
        m1.insert({i, i});
    }

    printf("Without reserve - %llu Allocations\n", AllocationCounter);
}

int main()
{
    with_reserve_and_pool();
    without_reserve();
}

使用这个分配器后,with_reserve_and_pool的分配次数会降到1次(仅桶数组的分配),大幅减少内存分配开销。

2. 使用扁平哈希表替代std::unordered_map

第三方库如Abseil的absl::flat_hash_map、Boost的boost::flat_hash_map采用扁平存储结构,将元素放在连续内存块中。这类结构支持预分配足够的内存容纳所有元素,天然减少分配次数,同时性能通常优于标准库的unordered_map。

3. 保留reserve优化

即使使用内存池或扁平哈希表,调用reserve依然有价值——它能提前设置合适的桶数量,减少哈希冲突和rehash操作,进一步提升插入性能。


内容的提问来源于stack exchange,提问作者Boris

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 19:18:13