我在MSVC14(VS2015)中观察std::unordered_map
的奇怪行为。
考虑以下场景。我创建一个无序的地图,并用虚拟结构填充它,消耗大量的内存,比如1Gb,插入总体100k元素。然后你开始从地图中删除元素。假设你已经删除了一半的元素,那么,你希望释放一半的内存。对?错误!我看到当map中的元素数量超过某个阈值时释放内存,在我的例子中它是1443个元素。
可以说使用{{1从OS分配大块是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;
}
然后当它没有帮助尝试愚蠢的东西,比如创建临时容器,复制/移动剩余的条目,清除原始条目,并从temp复制/移回原始。像这样的东西
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
实例传递给它。
另外,我已经在这里和那里修改了STL代码,就像调用{在allocate_shared
SimpleAllocator
std::vector::shrink_to_fit
上std::unordered_map's
的msvc stl实施基于vector
和unordered_map
),它也没有用。
EDIT001:适用于所有非信徒。以下代码与先前的代码大致相同,但使用list
代替vector
。操作系统回收内存 。
std::vector<Entity>
答案 0 :(得分:4)
你是对的,但部分是正确的。
在VC ++中实现C ++ unordered_map
的方式是使用内部std::vector
,桶列表和std::list
保存节点地图
在图表中,它看起来像是:
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中做这样的事情就会生活在地狱里。)
答案 1 :(得分:2)
假设您已删除了一半元素,那么,您希望释放一半内存。正确?
其实没有。我希望内存分配器是根据我的程序执行效率来编写的。我希望它能分配比你需要的更多的内存,只有在订购时或者确定永远不再需要内存时才将内存释放回操作系统。
我希望尽可能多地在用户空间中重用内存块,并将它们分配到连续的块中。
对于大多数应用程序来说,一个迂腐的内存分配器,它从操作系统分配内存并在对象被销毁的那一刻返回它将导致程序非常缓慢和大量的磁盘抖动。它(在实践中)意味着在所有流行的操作系统中,即使是最小的40字节字符串也会被分配自己的4k页面,因为intel芯片组只能处理这种大小的页面中的内存(或者可能更大一些系统?)
答案 2 :(得分:1)
好的,在向微软开放优质支持票后,我得到了以下答案。我们已经知道大部分内容,但我们没有考虑过。
- 在Windows中,内存以Pages
的形式在堆中分配- 在STL中没有任何缓存,我们在您调用erase后立即调用RtlHeapFree
- 您看到的是Windows如何管理堆
- 一旦你标记要删除的东西,它可能不会返回到没有内存压力的操作系统,它可能会决定成本 在将来重新分配内存不仅仅是将其保留在内存中 过程
- 这是任何Heap算法的工作方式
- 要考虑的另一件事是;如果您要删除的值恰好跨页面分散;除非所有的价值观 页面内部为空,它将驻留在内存中
- 如果您非常关注立即减少私有字节,则可能需要编写自己的内存管理器而不是 取决于Windows Heap Handle。
醇>
重点是我的。我想它回答了这个问题,或者问题就像“这是Windows堆管理的工作方式”一样简单。在任何情况下都没有针对这个问题的(简单)解决方案,也许最好使用类似boost :: intrusive容器的东西,理论上它应该提供更好的局部性,这样Windows内存管理器就有更好的机会将内存返回给OS。 / p>
UPDATE001: 提升侵入式容器也没有做到这一点。
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;
}