c – std :: unordered_map不释放内存
我正在观察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的实现基于列表和向量),它也没有用. #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中做这样的事就会生活在地狱里.) (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |