代码之家  ›  专栏  ›  技术社区  ›  kreuzerkrieg

std::unordered_map不释放内存

  •  8
  • kreuzerkrieg  · 技术社区  · 9 年前

    我观察到 std::unordered_map MSVC14(VS2015)。 考虑以下场景。我创建了一个无序的映射,并用虚拟结构填充它,它消耗了大量内存,比如1Gb,总共插入了100k个元素。然后开始从地图中删除元素。假设您删除了一半的元素,那么您希望释放一半的内存。正当错误的我看到当地图中的元素数超过某个阈值时,内存就会释放,在我的例子中是1443个元素。
    有人可能会说这是 malloc 优化以使用从操作系统分配大数据块 VirtualAllocEx HeapAlloc 实际上,它并没有将内存释放回系统,因为优化决定了策略,并且可能不会调用 HeapFree 以便将来重用已分配的内存。
    为了消除这种情况,我为 allocate_shared ,它没有成功。因此,主要问题是为什么会发生这种情况,以及如何“压缩” unordered_map ?
    代码

    #include <windows.h>
    #include <memory>
    #include <vector>
    #include <map>
    #include <unordered_map>
    #include <random>
    #include <thread>
    #include <iostream>
    #include <allocators>
    
    HANDLE heap = HeapCreate(0, 0, 0);
    template <class Tp>
    struct SimpleAllocator
    {
        typedef Tp value_type;
        SimpleAllocator() noexcept
        {}
        template <typename U>
        SimpleAllocator(const SimpleAllocator<U>& other) throw()
        {};
        Tp* allocate(std::size_t n)
        {
            return static_cast<Tp*>(HeapAlloc(heap, 0, n * sizeof(Tp)));
        }
        void deallocate(Tp* p, std::size_t n)
        {
            HeapFree(heap, 0, p);
        }
    };
    template <class T, class U>
    bool operator==(const SimpleAllocator<T>&, const SimpleAllocator<U>&)
    {
        return true;
    }
    template <class T, class U>
    bool operator!=(const SimpleAllocator<T>& a, const SimpleAllocator<U>& b)
    {
        return !(a == b);
    }
    
    struct Entity
    {
        Entity()
        {
            _6 = std::string("a", dis(gen));
            _7 = std::string("b", dis(gen));
            for(size_t i = 0; i < dis(gen); ++i)
            {
                _9.emplace(i, std::string("c", dis(gen)));
            }
        }
        int _1 = 1;
        int _2 = 2;
        double _3 = 3;
        double _4 = 5;
        float _5 = 3.14f;
        std::string _6 = "hello world!";
        std::string _7 = "A quick brown fox jumps over the lazy dog.";
        std::vector<unsigned long long> _8 = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
        std::map<long long, std::string> _9 = {{0, "a"},{1, "b"},{2, "c"},{3, "d"},{4, "e"},
        {5, "f"},{6, "g"},{7, "h"},{8, "e"},{9, "j"}};
        std::vector<double> _10{1000, 3.14};
        std::random_device rd;
        std::mt19937 gen = std::mt19937(rd());
        std::uniform_int_distribution<size_t> dis = std::uniform_int_distribution<size_t>(16, 256);
    };
    
    using Container = std::unordered_map<long long, std::shared_ptr<Entity>>;
    
    void printContainerInfo(std::shared_ptr<Container> container)
    {
        std::cout << std::chrono::system_clock::to_time_t(std::chrono::system_clock::now())
            << ", Size: " << container->size() << ", Bucket count: " << container->bucket_count()
            << ", Load factor: " << container->load_factor() << ", Max load factor: " << container->max_load_factor()
            << std::endl;
    }
    
    int main()
    {
        constexpr size_t maxEntites = 100'000;
        constexpr size_t ps = 10'000;
        stdext::allocators::allocator_chunklist<Entity> _allocator;
        std::shared_ptr<Container> test = std::make_shared<Container>();
        test->reserve(maxEntites);
    
        for(size_t i = 0; i < maxEntites; ++i)
        {
            test->emplace(i, std::make_shared<Entity>());
        }
    
        std::random_device rd;
        std::mt19937 gen(rd());
        std::uniform_int_distribution<size_t> dis(0, maxEntites);
        size_t cycles = 0;
        while(test->size() > 0)
        {
            size_t counter = 0;
            std::cout << "Press any key..." << std::endl;
            std::cin.get();
            while(test->size() > 1443)
            {
                test->erase(dis(gen));
            }
            printContainerInfo(test);
            std::cout << "Press any key..." << std::endl;
            std::cin.get();
            std::cout << std::endl;
        }
        return 0;
    }
    

    到目前为止我尝试过的事情: 当负载系数达到某个阈值时,尝试重新散列/调整大小-在擦除中 while 添加如下内容

    if(test->load_factor() < 0.2)
    {
        test->max_load_factor(1 / test->load_factor());
        test->rehash(test->size());
        test->reserve(test->size());
        printContainerInfo(test);
        test->max_load_factor(1);
        test->rehash(test->size());
        test->reserve(test->size());
    }
    

    然后,当它没有帮助时,尝试一些愚蠢的操作,比如创建临时容器、复制/移动剩余条目、清除原始条目,然后从临时文件复制/移回原始文件。有点像这样

    if(test->load_factor() < 0.2)
    {
        Container tmp;
        std::copy(test->begin(), test->end(), std::inserter(tmp, tmp.begin()));
        test->clear();
        test.reset();
        test = std::make_shared<Container>();
        std::copy(tmp.begin(), tmp.end(), std::inserter(*test, test->begin()));
    }
    

    最后,更换 shared_ptr 具有 分配_共享 并通过 SimpleAllocator 实例。
    此外,我还到处修改了STL代码,比如调用 std::vector::shrink_to_fit 在…上 std::unordered_map's vector (msvc stl实现 无序映射 基于 list 矢量 ),它也不起作用。

    EDIT001:适用于所有非信徒。以下代码或多或少与前面的代码相同,但使用 std::vector<Entity> 而不是 无序映射 .记忆

    #include <memory>
    #include <vector>
    #include <map>
    #include <random>
    #include <thread>
    #include <iostream>
    
    struct Entity
    {
        Entity()
        {
            _6 = std::string("a", dis(gen));
            _7 = std::string("b", dis(gen));
            for(size_t i = 0; i < dis(gen); ++i)
            {
                _9.emplace(i, std::string("c", dis(gen)));
            }
        }
        int _1 = 1;
        int _2 = 2;
        double _3 = 3;
        double _4 = 5;
        float _5 = 3.14f;
        std::string _6 = "hello world!";
        std::string _7 = "A quick brown fox jumps over the lazy dog.";
        std::vector<unsigned long long> _8 = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
        std::map<long long, std::string> _9 = {{0, "a"}, {1, "b"}, {2, "c"}, {3, "d"}, {4, "e"},
                                               {5, "f"}, {6, "g"}, {7, "h"}, {8, "e"}, {9, "j"}};
        std::vector<double> _10{1000, 3.14};
        std::random_device rd;
        std::mt19937 gen = std::mt19937(rd());
        std::uniform_int_distribution<size_t> dis = std::uniform_int_distribution<size_t>(16, 256);
    };
    
    using Container = std::vector<std::shared_ptr<Entity>>;
    
    void printContainerInfo(std::shared_ptr<Container> container)
    {
        std::cout << std::chrono::system_clock::to_time_t(std::chrono::system_clock::now())
                  << ", Size: " << container->size() << ", Capacity: " << container->capacity() << std::endl;
    }
    
    int main()
    {
        constexpr size_t maxEntites = 100'000;
        constexpr size_t ps = 10'000;
        std::shared_ptr<Container> test = std::make_shared<Container>();
        test->reserve(maxEntites);
    
        for(size_t i = 0; i < maxEntites; ++i)
        {
            test->emplace_back(std::make_shared<Entity>());
        }
    
        std::random_device rd;
        std::mt19937 gen(rd());
        size_t cycles = 0;
        while(test->size() > 0)
        {
            std::uniform_int_distribution<size_t> dis(0, test->size());
            size_t counter = 0;
            while(test->size() > 0 && counter < ps)
            {
                test->pop_back();
                ++counter;
            }
            ++cycles;
            if(cycles % 7 == 0)
            {
                std::cout << "Inflating..." << std::endl;
                while(test->size() < maxEntites)
                {
                    test->emplace_back(std::make_shared<Entity>());
                }
            }
            std::this_thread::sleep_for(std::chrono::seconds(1));
            printContainerInfo(test);
            std::cout << std::endl;
        }
        return 0;
    }
    
    3 回复  |  直到 9 年前
        1
  •  9
  •   Kevin Boris Azanov    6 年前

    你是对的,但有一部分。

    方式C++ unordered_map 在VC++中通过使用内部 std::vector ,这是 存储桶列表 、和a std::list 哪一个 保存地图的节点 .

    在图表中,如下所示:

    buckets : [][][*][][][][][][*][][][][][][*]
                   |            |            |
                   |            |            | 
                 ---             ------      |
                 |                    |      |
                 V                    V      V
    elements: [1,3]->[5,7]->[7,1]->[8,11]->[10,3]->[-1,2]
    

    现在,当您擦除节点时,它们实际上会从列表中删除,但这并没有说明 buckets数组 。达到某个阈值后,会调整buckets数组的大小(或者每个bucket中的元素太多,或者对于元素的数量来说,bucket太多)。

    这也证明了我的观点,这里有一个用最新VC++编译的示例:

    std::unordered_map<int, std::vector<char>> map;
    for (auto i = 0; i < 1000; i++) {
        map.emplace(i, std::vector<char>(10000));
    }
    
    for (auto i = 0; i < 900; i++) {
        map.erase(i);
    }
    

    查看调试器中的原始视图,我们可以看到:

    +       _List   { size=100 }    std::list<std::pair<int const ,std::vector<char,std::allocator<char> > >,std::allocator<std::pair<int const ,std::vector<char,std::allocator<char> > > > >
    +       _Vec    { size=2048 }   std::vector<std::_List_unchecked_iterator<std::_List_val<std::_List_simple_types<std::pair<int const ,std::vector<char,std::allocator<char> > > > > >,std::_Wrap_alloc<std::allocator<std::_List_unchecked_iterator<std::_List_val<std::_List_simple_types<std::pair<int const ,std::vector<char,std::allocator<char> > > > > > > > >
    

    这意味着尽管我们只有100个元素,但地图保留了2048个桶。

    所以,不是 全部的 删除元素时会释放内存。地图维护了另一段内存,以保存存储桶本身,并且该内存比元素内存更顽固。


    让我们更加疯狂!

    std::unordered_map<int, std::vector<char>> map;
    for (auto i = 0; i < 100'000; i++) {
        map.emplace(i, std::vector<char>(10000));
    }
    
    for (auto i = 0; i < 90'000; i++) {
        map.erase(i);
    }
    

    擦除循环结束时的结果:

    +       _List   { size=10000 }  std::list<std::pair<int const ,std::vector<char,std::allocator<char> > >,std::allocator<std::pair<int const ,std::vector<char,std::allocator<char> > > > >
    +       _Vec    { size=262144 } std::vector<std::_List_unchecked_iterator<std::_List_val<std::_List_simple_types<std::pair<int const ,std::vector<char,std::allocator<char> > > > > >,std::_Wrap_alloc<std::allocator<std::_List_unchecked_iterator<std::_List_val<std::_List_simple_types<std::pair<int const ,std::vector<char,std::allocator<char> > > > > > > > >
    

    现在,在64位上 std::_List_unchecked_iterator<...> 为8字节。我们有262144个,所以我们保存了262144*8/(1024*1024)=2MB的大量未使用数据。 这就是您看到的高内存使用率 .

    使命感 map.rehash(1024*10) 在删除了所有多余的节点之后,似乎有助于减少内存消耗:

    +       _List   { size=10000 }  std::list<std::pair<int const ,std::vector<char,std::allocator<char> > >,std::allocator<std::pair<int const ,std::vector<char,std::allocator<char> > > > >
    +       _Vec    { size=32768 }  std::vector<std::_List_unchecked_iterator<std::_List_val<std::_List_simple_types<std::pair<int const ,std::vector<char,std::allocator<char> > > > > >,std::_Wrap_alloc<std::allocator<std::_List_unchecked_iterator<std::_List_val<std::_List_simple_types<std::pair<int const ,std::vector<char,std::allocator<char> > > > > > > > >
    

    这就是您正在寻找的解决方案。

    (附言:我最近违背自己的意愿做了很多.NET。这个问题确实展示了C++的优点:我们可以用调试器进入标准库代码,看看事情是如何发生的,什么时候发生的,然后我们可以对其采取行动。如果可能的话,在.NET中做这样的事情会是一个地狱。)

        2
  •  3
  •   Richard Hodges    9 年前

    假设您删除了一半的元素,那么您希望释放一半的内存。正当

    事实上没有。我希望内存分配器是根据程序执行效率来编写的。我希望它分配的内存比需要的多,只有在命令或确定不再需要内存时才将内存释放回操作系统。

    对于大多数应用程序,一个迂腐的内存分配器从操作系统分配内存,并在对象被破坏时将其返回,这将导致程序速度极慢和大量磁盘颠簸。实际上,这也意味着,在所有流行的操作系统上,即使是最小的40字节字符串也会被分配给自己的4k页面,因为英特尔芯片组只能处理这样大小的页面(或者在某些系统上更大的页面?)

        3
  •  2
  •   kreuzerkrieg    9 年前

    好的,在向微软开出高级支持票后,我得到了以下答案。大部分我们已经知道了,但有一些我们没有考虑。

    1. 在windows中,内存以页面的形式在堆中分配
    2. 在STL中没有任何缓存,在您调用erase后,我们直接调用RtlHeapFree
    3. 您看到的是Windows如何管理堆
    4. 一旦您将某个内容标记为删除,它可能不会返回到没有内存压力的操作系统,它可能会决定 在未来重新分配内存不仅仅是将其保留在 过程
    5. 这就是任何堆算法的工作原理
    6. 另一件要考虑的事情是:;如果要删除的值恰好分布在多个页面上;除非所有值
    7. 如果您对立即减少私有字节非常挑剔,那么您可能必须编写自己的内存管理器,而不是 取决于Windows堆句柄。

    更新001: Boost侵入式容器也没有做到这一点。

    struct Entity : public boost::intrusive::unordered_set_base_hook<>
    {
        explicit Entity(size_t id)
        {
            first = id;
            _6 = std::string("a", dis(gen));
            _7 = std::string("b", dis(gen));
            for(size_t i = 0; i < dis(gen); ++i)
            {
                _9.emplace(i, std::string("c", dis(gen)));
            }
        }
    
        size_t first = 1;
        int _1 = 1;
        int _2 = 2;
        float _5 = 3.14f;
        double _3 = 3;
        double _4 = 5;
        std::string _6 = "hello world!";
        std::string _7 = "A quick brown fox jumps over the lazy dog.";
        std::vector<unsigned long long> _8 = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
        std::map<long long, std::string> _9 = {{0, "a"}, {1, "b"}, {2, "c"}, {3, "d"}, {4, "e"},
                                               {5, "f"}, {6, "g"}, {7, "h"}, {8, "e"}, {9, "j"}};
        std::vector<double> _10{1000, 3.14};
        std::random_device rd;
        std::mt19937 gen = std::mt19937(rd());
        std::uniform_int_distribution<size_t> dis = std::uniform_int_distribution<size_t>(16, 256);
    };
    
    struct first_is_key
    {
        typedef size_t type;
    
        const type& operator()(const Entity& v) const { return v.first; }
    };
    
    using Container = boost::intrusive::unordered_set<Entity, boost::intrusive::key_of_value<first_is_key>>;
    
    void printContainerInfo(const Container& container)
    {
        std::cout << std::chrono::system_clock::to_time_t(std::chrono::system_clock::now())
                  << ", Size: " << container.size() << ", Bucket count: " << container.bucket_count() << std::endl;
    }
    
    int main()
    {
        constexpr size_t maxEntites = 100'000;
        Container::bucket_type* base_buckets = new Container::bucket_type[maxEntites];
        Container test(Container::bucket_traits(base_buckets, maxEntites));
    
        std::random_device rd;
        std::mt19937 gen(rd());
        std::uniform_int_distribution<size_t> dis;
    
        while(test.size() < maxEntites)
        {
            auto data = new Entity(dis(gen));
            auto res = test.insert(*data);
            if(!res.second)
            {
                delete data;
            }
        }
    
        printContainerInfo(test);
        while(test.size() > 0)
        {
            while(test.size() > maxEntites * 2 / 3)
            {
                test.erase_and_dispose(test.begin(), [](Entity* entity)
                                       {
                                           delete entity;
                                       });
            }
    
            printContainerInfo(test);
            while(test.size() < maxEntites)
            {
                auto data = new Entity(dis(gen));
                auto res = test.insert(*data);
                if(!res.second)
                {
                    delete data;
                }
            }
        }
        return 0;
    }