🎉 Celebrating 25 Years of GameDev.net! 🎉

Not many can claim 25 years on the Internet! Join us in celebrating this milestone. Learn more about our history, and thank you for being a part of our community!

Custom memory allocator for STL - dead end due to constructors?

Started by
35 comments, last by JoeJ 7 hours, 28 minutes ago

a light breeze said:
But really that's a non-issue in most code, because almost all classes have cheap default constructors. std::vector certainly does

A default destructor which allocates memory is not cheap. Even less so if the allocation is actually redundant.

But again: The problem is not constructors and what they do or not, it is the lack of control when, how or if they should be called at all.
(I understand the OOP idea about automated initialization and destruction. It's often helpful and reducing complexity. But just as often it's the opposite. Personally i find it easier to do all this myself manually. That's more responsibility, but requires less knowledge about non-obvious language quirks.)

How could std::optional help here? It's new to me, and reading it up leaves me in the dark.

Advertisement

So, this might sound a bit weird, but, is not modifying std::vector implementation on your PC a viable option here? It seems to me, you can just modify the constructors of vector and be done? if not, copy-paste the entire thing into your own file and modify what is needed and just rename it: “my_vector”. Or is there a reason to avoid this? (I recommend it, because at work, std::stuff and specifically std::vector, where terrible experiences and I modified the headers to make it more sane and I was very happy with the result).

On another note, how many separate allocations do you have? (i.e. allocations that get a new memory and not reusing any unused memory from a custom allocator). If my math is correct - and I can definitely be wrong about this - you need at least: n = Memory_Available/(2*max_alloc) to fragment your Memory_Available after n allocations. Given that you should have 128TB on 64-bit Windows, And you have mentioned your max RAM usage is 20GB. I can assume your max_alloc size is like 10GB Then you have to at least have something like 7000 allocations that are completely separate. So, can't you group more allocations together? because I assume 7000 is a lot of room. But I don't know, if you are doing millions of separate allocations, then it is very possible.

None

AliAbdulKareem said:
So, this might sound a bit weird, but, is not modifying std::vector implementation on your PC a viable option here? It seems to me, you can just modify the constructors of vector and be done?

Modifying the default constructor won't help. It's called if we create an array of vectors, so we can't construct with a required state. And the state (allocator) is required, because it's already used to allocate the first (redundant) element.

To fix it, i would need change the source so it does not allocate anything before we add any elements.
But i'm not competent to edit STL source code. And even if i would not want to, since my STL would be no longer ‘standard’.

I thought about making my own vector container in a way it's compatible with STL algorithms, but i'm not competent, and i would be afraid my hack only work with certain STL implementations.

Switching to EASTL is a much better option. Thanks for that.
And it's really a shame the standard library for a high performance language fails on high performance requirements. >: )

AliAbdulKareem said:
(I recommend it, because at work, std::stuff and specifically std::vector, where terrible experiences and I modified the headers to make it more sane and I was very happy with the result).

I guess you guys do not sell the code, just the compiled output, i guess? ; )

AliAbdulKareem said:
On another note, how many separate allocations do you have? (i.e. allocations that gets a memory and not reusing any unused memory from a custom allocator).

Hard to say, but i guess 1-10 millions with practical settings. And they are not persistent. They phase in and out as the out of core processing regions traverse the scene.
Because i did anything with std::vector, i have no custom memory management or fixed pools at all.
But obviously i need to change this now…

JoeJ said:

a light breeze said:
But really that's a non-issue in most code, because almost all classes have cheap default constructors. std::vector certainly does

A default destructor which allocates memory is not cheap. Even less so if the allocation is actually redundant.

std::vector does not allocate memory in its default constructor. At least not unless its allocator allocates memory in its default constructor.

How could std::optional help here? It's new to me, and reading it up leaves me in the dark.

std::optional represents an object that may or may not exist. You can use it to delay construction.

// Allocates no heap memory; does not construct any vectors
std::optional<std::vector<int> > array_of_vectors[10];
for (auto &v: array_of_vector) {
  // Constructs one of the vectors.  Also allocates no heap memory.
  v.emplace(my_allocator);
}

a light breeze said:
std::vector does not allocate memory in its default constructor. At least not unless its allocator allocates memory in its default constructor.

It does the allocation. I did not know this either, but it does. Probably they need some head or tail node to exist even if the container is empty. One of the reasons why EASTL exists.
Idk if each memory allocation requires a thread lock in practice. But if so, each instance of a vector causes a (potential) lock, already before we reserve space and push stuff. That's really bad.

a light breeze said:
std::optional represents an object that may or may not exist. You can use it to delay construction.

Interesting.
Wanted to try if it works with an array too, using placement new instead emplace.
But neither MSVC nor Clang know about std::optional, although i have set C++17 as the language standard.
And i can't go higher currently. I have too many old school code which is no longer compatible.

JoeJ said:

a light breeze said:
std::vector does not allocate memory in its default constructor. At least not unless its allocator allocates memory in its default constructor.

It does the allocation. I did not know this either, but it does. Probably they need some head or tail node to exist even if the container is empty. One of the reasons why EASTL exists.

std::vector does not use nodes. It uses one block of dynamically allocated memory if it has any contents, and no such block if it has no contents. It is possible to create an “empty” std::vector with a block of memory by calling std::vector::reserve, but there is no reason why a default constructed std::vector should preallocate a memory block. And unless you have an unbelievably bad C++ standard library implementation, it doesn't. If you're seeing an allocation in a debugger, it has to come from somewhere else.


Idk if each memory allocation requires a thread lock in practice. But if so, each instance of a vector causes a (potential) lock, already before we reserve space and push stuff. That's really bad.

a light breeze said:
std::optional represents an object that may or may not exist. You can use it to delay construction.

Interesting.
Wanted to try if it works with an array too, using placement new instead emplace.
But neither MSVC nor Clang know about std::optional, although i have set C++17 as the language standard.
And i can't go higher currently. I have too many old school code which is no longer compatible.

std::optional was introduced in C++17, so unless you use an ancient compiler that doesn't fully support C++17, you should have it. Don't forget to add #include <optional> to your files before using it.

JoeJ said:
It does the allocation. I did not know this either, but it does. Probably they need some head or tail node to exist even if the container is empty.

I'm going to call this one out. I have walked through several of the major implementations and haven't seen them allocate. Going back to my days in the '90s working with boost people I remember people occasionally making that claim, and it constantly getting shut down where not even the minor implementations of the day pre-allocated.

While the language standard doesn't (cannot) guarantee there is no allocation, the default constructor is noexcept, which makes the allocation unlikely. The definition for what it does is clear that it is empty. There is no need for “some head or tail node to exist”, as they're internally essentially a buffer and a count, a zero-length container has no need for the buffer. Begin and end need to be equal, but can be any value although they're typically zero as a null pointer. Performing an allocation would be a rather slow system call relative to the expected performance, and any implementation worth its salt would flag that as a performance defect.

Microsoft's contains an unallocated _My_data buffer and convenience pointers _Myfirst, _Mylast, and _Myend all default initialized to zero. GCC's contains a similar pattern, an unallocated data buffer and convenience pointers _M_start, _M_finish, and _M_end_of_storage also default initialized to zero. Neither allocates.

Which implementation do you claim allocates on default construction?

a light breeze said:
Don't forget to add #include to your files before using it.

Hehe, oops - i did. How stupid.

Ok, it works! :

			using PagedAlloc = Allocators::PagedAllocator<Allocators::FreeListAllocator, 1024, 4>;
			using stdAlloc = Allocators::stdAllocator<int, PagedAlloc>;
			PagedAlloc pagedAlloc;
			PagedAlloc pagedAlloc2;

			stdAlloc alloc(&pagedAlloc);
			std::vector<int, stdAlloc > vec(alloc); // (!) allocates one element - believe or not
			
			std::optional< std::vector<int, stdAlloc> > array_of_vectors[10]; // no allocator called
			for (int i=0; i<10; i++)
			{
				array_of_vectors[i].emplace(alloc); // allocates one element, using my proper allocator! \:)/
			}
			array_of_vectors[5]->push_back(7);

Notice the comments. Semantics of calling it ‘elements’ or ‘nodes’ aside, the allocator is called for one element without reserving or pushing anything. Using VisualStudio STL, your assumption of ‘no assumption should happen’ is just that. I have not tried release build, but as said: The EASTL guys did mention this problem as one motivation to make their own STL from scratch.

Regarding the solution of the array problem, i guess it causes runtime overhead?
Not sure - it's modern C++ black magic to me. I better stick at EASTL, although it's the most practical workaround til yet. Thanks!

frob said:
I'm going to call this one out. I have walked through several of the major implementations and haven't seen them allocate.

Putting the breakpoint here:

Clicking continue, it breaks at my breakpoint in the allocator:

Posting allocator source. You could replicate replacing the calls to my real allocator with malloc:

	template <typename T, class Allocator>
	class stdAllocator 
	{
	public:
		using value_type = T;
    
		Allocator *allocator = nullptr;

		stdAllocator() = default;
		stdAllocator(const stdAllocator&) = default;
		stdAllocator(Allocator *allocator) : allocator(allocator) {}
    
		// Conversion constructor that passes the buffer along.
		template <typename U, class Allocator>
		stdAllocator(const stdAllocator<U, Allocator>& other) : allocator(other.allocator) {}
		// Required for the conversion constructor.
		template <typename U, class Allocator> friend class stdAllocator;

    
		T* allocate(std::size_t n) 
		{
			if (n > std::allocator_traits<stdAllocator>::max_size(*this)) 
			{
				throw std::bad_alloc();
			}
			if (!allocator) return nullptr;
			return (T*)allocator->Allocate(n * sizeof(T), sizeof(T)); // todo: alignment as param?
		}

		void deallocate(T* p, std::size_t) noexcept 
		{
			allocator->Deallocate(p);
		}

		template<typename U, typename... Args>
		void construct(U* p, Args&&... args) 
		{
			new(p) U(std::forward<Args>(args)...);
		}

		template<typename U>
		void destroy(U* p) noexcept 
		{
			p->~U();
		}

		friend bool operator==(const stdAllocator&, const stdAllocator&) { return true; }
		friend bool operator!=(const stdAllocator&, const stdAllocator&) { return false; }
	};

I'm using Visual Studio Community 2022.

It should not be like that, but i don't see what i could do wrong to trigger the allocation from my side.

… That's not the default constructor, nor is it a noexcept constructor.

Yes, that version allocates a proxy object. If it bothers you, specialize it to cause the length 1 allocation to not allocate on the heap, as is a common small object optimization.

Alternatively, if you have a need that is special purpose rather than general purpose (which the standard library attempts to be, general purpose) replace it with EASTL or whatever you like.

There's still nothing in the code that's specific about constructors being inherently problematic. They merely provide an easy syntax for the initialization you need to do in any language. Constructors provide a standardized name for that initialization. The new keyword specifically is a shortcut for the song-and-dance version of either malloc() followed by bzero(), or malloc() followed by memcpy() of a generic instance, or malloc() followed by calling an initialization function. The pattern remains the same, it's just that the language gives it an explicit common name with a guarantee that it won't be skipped by a bad programmer as a bug.

frob said:
… That's not the default constructor, nor is it a noexcept constructor.

Yes, but do you think allocation logic depends on using the default allocator vs. a custom allocator, so the proxy allocation only happens in the latter case?

Makes no sense either, but could be.

frob said:
Yes, that version allocates a proxy object. If it bothers you, specialize it to cause the length 1 allocation to not allocate on the heap, as is a common small object optimization.

Sure, but currently i do not bother about costs of the allocation. The problem is that it's happening prevents me from using custom allocators in any situation. I do not use arrays of vectors a lot, but i would need to add parametrized constructors to set allocators to practically any data structure, thus i can't do arrays of structs either, etc.

I could make it work using STL. But it would enforce big changes.

Advertisement