Jump to content

Leaderboard

Popular Content

Showing content with the highest reputation on 2013-06-27 in all areas

  1. So today I took some time to actually test out the gross performance of the memory pool. The best way to test it (and the most suitable if it's ever used in 0 A.D.) was to write a custom std::allocator. I chose to create two kinds of allocators only (mostly because STL allocators are temporary objects): Now some of you might remember that the "pool" and "dynamic_pool" classes are size based; so in order to make them fit with generic allocators we have to create a proxy: template<unsigned TSIZE> struct _global_pool { static dynamic_pool<TSIZE, true> pool; }; template<unsigned TSIZE> dynamic_pool<TSIZE, true> _global_pool<TSIZE>::pool(true); This is pretty cool, we now have a "global_pool" allocator and a "bin_allocator" allocator. Both of them just access the global template instantiation of the _global_pool<>::pool variable. We can now test out the allocator pretty easily: std::vector<int, global_pool<int>> vec; But that looks really nasty. Can you imagine having that all over the codebase? Me neither. A typedef would be cumbersome, since you'd have to declare it everywhere. Soon the code will be full of typedefs - very counter-intuitive indeed. So the best I could do was to write wrappers on-top of all the STL containers and also the string class. This works out perfectly, because global_pool<> will not work on variable sized allocations, so you can't use it in std::unordered_map nor std::vector for example. By writing the wrappers we ensure that the correct type of allocator is used. As a plus; since STL containers are already heavily debugged and refined, we don't have worry about crashing our memory pools anymore. To the wrappers: /** * Overrides std::vector and plugs in a bin_allocator as the allocator. * The bin_allocator allocates from global memory pools based * on the chunk size. */ template<class T> class psvector : public std::vector<T, bin_allocator<T> >; Using it is really simple and since it's just a subclass of std::vector, it has all of its functionality. psvector<int> vec; Ahh, that looks perfect. The P is for perfect and S is for shiny. (just kidding; - the ps stands for pyrogenesis) Finally, we can see the performance difference of Syscall memory allocations vs Memory pooled allocations: VC++11, Release, default optimizations 1000x ps::stl 42ms 1000x std::stl 1170ms A 28x performance difference. That's right. Just by plugging in a memory pool we won by committing less overall memory and less System Alloc calls. Our allocations are faster and they take up less memory in the long run - isn't that sweet? Here's the test code (nothing fancy): Since I've finished the STL container wrapper and memory pool code, It'll only be a matter of time until this gets into the pyrogenesis source. Limitations: 1) bin_allocator only 'bins' allocations in range 4..1024. Even then, the actual allocation chunks are: 4 8 12 16 20 24 32 40 48 56 64 80 96 112 128 160 192 224 256 384 512 640 768 896 1024 So it wastes some memory in favor of speed. However, this 'waste' of space will definitely not matter, since we win A LOT more in the long term and the speed is just fantastic. ps::stl mem-commited: 522K std::stl mem-commited: 384K 2) Pool deallocation scheme is not linear. The global pools are auto-cleaned when they go empty; - except the first pool, which is never deallocated. I'm afraid we'll have to live with this for now - cleaning the pools doesn't make sense anyways. What we could do further: 1) Extend the wrappers over more structures such as std::stringstream and friends 2) Test how well this scales in 0 A.D. - the initial pool reserve can be increased for less overhead and to match 0 A.D. requirements. Remember that the number of pools is increased on demand and pools have a fixed initial capacity. If we know we will have 10000 allocations, we can reserve pool size beforehand. 3) Give RedFox a cookie, since he spent all day (and night) making this beast work.
    1 point
×
×
  • Create New...