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, 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;
}
到目前为止我尝试的事情:
当加载因子达到某个阈值时尝试重新散列/调整大小 – 在删除时添加这样的东西
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", 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;
}
解决方法:
你是对的,但部分是正确的.
在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::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> > > > > > > > >
这是您正在寻找的解决方案.
(ps.我最近违背了我的意愿做了很多.NET.这个问题很好地展示了关于C的好部分:我们可以使用我们的调试器进入标准库代码,确切地看到事情发生的时间和时间,我们可以采取行动随后,如果可能的话,在.NET中做这样的事就会生活在地狱里.)
标签:unordered-map,c,memory-management,visual-c,stl 来源: https://codeday.me/bug/20190727/1554073.html