加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 百科 > 正文

c – std :: unordered_map不释放内存

发布时间:2020-12-16 10:03:51 所属栏目:百科 来源:网络整理
导读:我正在观察MSVC14(VS2015)中std :: unordered_map的奇怪行为. 考虑以下场景.我创建一个无序的地图,并用虚拟结构填充它,消耗大量的内存,比如1Gb,插入总体100k元素.然后你开始从地图中删除元素.假设你已经删除了一半的元素,那么,你希望释放一半的内存.对?错误
我正在观察MSVC14(VS2015)中std :: unordered_map的奇怪行为.
考虑以下场景.我创建一个无序的地图,并用虚拟结构填充它,消耗大量的内存,比如1Gb,插入总体100k元素.然后你开始从地图中删除元素.假设你已经删除了一半的元素,那么,你希望释放一半的内存.对?错误!我看到当map中的元素数量超过一定阈值时释放内存,在我的情况下它是1443个元素.可能会说使用VirtualAllocEx或HeapAlloc从OS分配大块的malloc优化实际上它不会释放内存因为优化决定了策略,所以可能不会调用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);
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,n * sizeof(Tp)));
    }
    void deallocate(Tp* p,std::size_t n)
    {
        HeapFree(heap,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,{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;
}

到目前为止我尝试的事情:
当加载因子达到某个阈值时尝试重新散列/调整大小 – 在删除时添加这样的东西

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()));
}

最后,将allocate_ptr替换为allocate_shared,并将SimpleAllocator实例传递给它.另外,我在这里和那里修改了STL代码,比如在std :: unordered_map的向量上调用std :: vector :: shrink_to_fit(msvc stl unordered_map的实现基于列表和向量),它也没有用.
EDIT001:对所有非信徒.以下代码与先前的代码或多或少相同,但使用std :: vector< Entity>而不是unordered_map.操作系统回收内存.

#include <memory>
#include <vector>
#include <map>
#include <random>
#include <thread>
#include <iostream>

struct Entity
{
    Entity()
    {
        _6 = std::string("a",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())
              << ",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;
}

解决方法

你是对的,但部分是正确的.
在VC中实现C unordered_map的方式是使用内部的std :: vector,它是一个桶列表和一个std :: list,它保存了map的节点

在图中,它看起来像这样:

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

现在,当您擦除节点时,它们实际上已从列表中删除,但它没有说明桶数组.在达到某个阈值后(通过每个桶具有太多元素,或者对于元素数量具有太多桶),调整桶数组的大小

也证明了我的观点,这是一个用最新的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::allocator<char> > > > >
+       _Vec    { size=2048 }   std::vector<std::_List_unchecked_iterator<std::_List_val<std::_List_simple_types<std::pair<int const,std::allocator<char> > > > > >,std::_Wrap_alloc<std::allocator<std::_List_unchecked_iterator<std::_List_val<std::_List_simple_types<std::pair<int const,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::allocator<char> > > > >
+       _Vec    { size=262144 } std::vector<std::_List_unchecked_iterator<std::_List_val<std::_List_simple_types<std::pair<int const,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::allocator<char> > > > >
+       _Vec    { size=32768 }  std::vector<std::_List_unchecked_iterator<std::_List_val<std::_List_simple_types<std::pair<int const,std::allocator<char> > > > > > > > >

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

(ps.我最近违背了我的意愿做了很多.NET.这个问题很好地展示了关于C的好部分:我们可以使用我们的调试器进入标准库代码,确切地看到事情发生的时间和时间,我们可以采取行动随后,如果可能的话,在.NET中做这样的事就会生活在地狱里.)

(编辑:李大同)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读