|
Excellent!
I want a pool to handle (unsigned char*), whose length is not fixed.
just to replace:
int nLen = 256; // len has no limit
unsigned char *pBuf = new unsigned char[nLen];
to something like:
MemPool<unsigned char> mp;
unsigned char *pBuf = mp.New(nLen);
-----------------
|
|
|
|
|
Unfortunately this pool is for fixed sized objects only. In fact, this is what gives its O(1) trait.
Imagine that even if I can allocate different sized objects, it will be problematic if I remove one from the middle.
Example: The first block is of size 32.
Allocate 10. Allocate 5. Allocate 10.
If you allocate 30 now, it is easy to allocate a new block... but then, some bytes of this block will be "lost"... or, if I don't want to lose them, you will probably need to search them (which kills the O(1) guarantee... at least if it is a simple solution).
Then, imagine that you delete the block of 5 bytes. A new allocation simply tries to reuse the deleted block... but if you want to allocate 6 bytes, you can't. This wouldn't happen if all allocations were of the same size.
So, this pool will not solve your problem.
|
|
|
|
|
Just a suggestion. The "mempool" I desired may be generally.
I have test your pool on AIX/GCC4.2.4. It's good.
There are some results:
1、My machine is slow, so I change the NUMBER_OF_ITERATIONS to 100000. The result is 542288ms vs 707116ms.
2、There are little thing to change. I hope you can update the pool to a crossplatform one.
Thank you!
|
|
|
|
|
Hi,
There is a study of pool of small objects in 'Modern C++ Design' by Andrei Alexandrescu (Ch. 4). It is dealing with objects same size and a different size. The Loki library implemented the code.
How do you compare your approach with that of the book?
|
|
|
|
|
I don't have the book, so I can't compare.
Yet, as I presented it here, it is O(1) for single object allocations and deallocations (while there are some implementations that are O(log n) or even O(n) either for allocations or for deallocations).
So, I think that if you have something built based on that book, you can try and see the performance difference. I actually can't see many performance improvements, but I can see many "control" improvements, as this pool doesn't call the destructor of its objects if the pool itself is deleted before deleting each object.
|
|
|
|
|
Try to borrow it. It is in rank of Bjarne books and Design Patterns.
|
|
|
|
|
No. I will not try to borrow it. I presented an O(1) solution that works pretty fine. I can see other solutions and inspire myself, this doesn't make my solution useless... I am pretty sure that any "variable size" solution isn't capable of being O(1) and at the same time never "losing" memory.
I don't need to read documents to know the obvious.
|
|
|
|
|
Hi Paulo,
I consider myself quite familiar with C++ and I do have some problems with your code. However, before I go into too many details some other things... Why this line:
if (_memory == NULL)
Please stop using NULL - it is actually 0 and only due to backwards compatibility implicitly converted to the null pointer. Start using nullptr . For instance consider
void foo(int a);
void foo(void* a);
foo(NULL);
Additionally you should consider using C++ naming guidelines. I don't want to be to pedantic here, but I find it important to follow the common guidelines (I know - since C++ is old and everyone has made up his own kind of styling rules it is a mess, however, there are official guidelines from the committee): e.g. Put the asterisk directly after the type, use camelCase for your methods, use Egyptian brackets and consider using the STL more.
In your code you re-invented the wheel. Why create a linked list when there is std::list<T> ? But the last one is a lot more flexible, better tested and probably performing better. Additionally you did not implement a move constructor.
Using malloc in a C++ code is legit, but might end up in a disaster. After all the memory management is a little bit different (there is more behind new than just the additional call to a constructor method)... Generally it is good practice to avoid mixing C and C++ calls (of course sometimes you have to do it).
Finally I encourage you to use auto more often. Gives you the right type without guessing.
One last thing: For having reference counting memory management you could also rely on e.g. shared_ptr . It is fast, efficient and well-tested. If you really want to have a look on how to implement a more advanced GC then I recommend https://www.cs.princeton.edu/~appel/modern/c/software/boehm/[^] - it is a classic that helped me already a lot.
|
|
|
|
|
I actually agree with many things you said but:
* I actually continue to use C++ compilers that don't have the nullptr. I know, I could use a #define for it, yet in every place that I worked that used C++ they were still using NULL, so I don't have a problem with it.
Also, even if your example of foo(int)/foo(void* a) is correct, I am not using NULL in a situation that generates such ambiguity.
About the naming guidelines... I completely agree with you. But I actually used Borland C++ Builder for many time and also C#... actually C++ Builder naming conventions are pretty similar to .NET ones, so I used a C++ convention, even if it is not the standard one.
About reinventing the wheel, I must completely disagree. Note that I use the same buffer used for object allocations as a linked list. So I don't use extra memory. Items smaller than a pointer will actually be allocated with the size of a pointer because when I delete them they become the "node" of the linked list. So, reinventing the wheel was completely on purpose.
About the malloc, I am already considering using an extra template parameter for the allocation/deallocation, so users will be free to use malloc/free, new char[...]/delete[] or anything they like.
About auto... if I used it, it was actually an error. I didn't want to use auto because older compilers don't support it. I agree that it uses the right type, but I really want a portable code.
And for the GC, shared_ptr actually covers only reference-counted situations and the idea is to implement a garbage collector where A can have a strong reference to B and B have a strong reference to A. shared_ptr, as far as I know, doesn't allow this.
Edit:
About the move constructor, I should actually make the default constructor explicit and forbid the copy constructor and the equals operator, as I don't want a pool to be created if an int is given as parameter or to have implicit copies.
|
|
|
|
|
What compilers are you targeting ? The features nullptr and auto are available since a couple of years in the industry's leading compilers (even before the std=c++11 flag, e.g. icc had it with std=c++0x - supported since 2010).
If you really want to target an older standard then this is okay - but I can just ask you: Would you (with the same argument: portability) also leave out e.g. async / await in C#? If your project target type does not support .NET 4 then this is reasonable, otherwise there is no reason for such a constraint as this is only a compiler feature.
What I'm going at is - if you want real portability then just go for C. You can even run it on microcontrollers and you REALLY just get what you write. There are no hidden costs e.g. for vtbl, vptr etc.
Sorry for my comment about reinventing the wheel - I did not realize you had such a strong goal in mind. Have you checked if this really makes a huge difference? Would be interesting to see. Also: You should consider checking against std::vector<T> . Even though it will perform totally worse in only push / pop like operations, it will totally outperform a linked list once there are many traversals /iterations included.
And final remark: Yes shared_ptr is reference counted - which is why I remarked it. But I love the simplicity and it works great in the situations where my code does not define a point at which some allocation can be deleted.
|
|
|
|
|
Actually I did only really test this code in Visual C, but I am really trying to target most C++ compilers. And, as I am giving the source code, anyone having problems with it can change as needed (be it the names or the use of NULL).
About C#, it is a completely different beast. C# has much less variations than C++. I really see that people will not use a given feature in C# because it is version 4 or 5. I don't see people not using a giving feature because GCC# doesn't support it, or because a compiler for a processor that I even don't know doesn't support it.
About the std::list/vector, I am now unsure if you are talking about the fact that I link the nodes (so, I have a linked list of nodes) or if you are talking about the FirstDeleted. The FirstDeleted really reuses memory and so, I will not even try to do it differently. vector will not only use more memory but, in some cases, will not be O(1) (even if it is amortized as O(1)). About the nodes I will probably be able to use the list, but as I only add new nodes (I don't remove them, except when deleting the entire pool) I don't think my code got "complicated" because of its use.
For the shared_ptr, my GC implementation (it already works, I only don't have the article) is both reference counted and root/reference based, being capable of collecting circular references... and having the possibility of creating weak-references, so those references become NULL when all other references cease to exist (reference count reaches 0) or when the garbage collection collects a circular reference.
|
|
|
|
|
list/vector has been a general remark. If you plan to iterate over the list (after all a GC has to lookup if objects are still in use ...) then a linked list is certainly slower than a vector - even though a vector might have to expand at some stage, which makes allocation slower in general.
About C#: I hope you are not confusing .NET versions with C# versions. I only see people not using certain features due to .NET limitations (e.g. they are bound to .NET 3.5, hence they cannot use e.g. dynamic) and not because of a certain C# version.
I wonder what kind of people do not want to make their tools better? Of course I could use an older compiler - but then I could also use Assembler. The purpose of a compiler is to provide abstraction, efficiency & safety - upgrades are usually beneficial (some are not - but C++11 is DEFINITELY a much better standard than any C++ standard before) and should therefore be applied.
But let's stop this pointless discussion here. I have my strong opinions and you did a great job!
|
|
|
|
|
Don't worry, I am not seeing this as a "bad" discussion. But maybe I have the wrong experiences with C++... almost everyplace where I see C++ used, there's too much "dinossaur" practices and tools in use.
About C# and .NET, async/await is a compiler feature, not a .NET one, so if I use a the async/await I must use the appropriate version of the compiler, independently on the targetted .NET version.
And I actually started some other articles explaining how the base classes work. I then made some comparisons and I didn't like the performance of std::vector. std::unordered_map is even worse, as by default it is almost 10 times slower than the .NET dictionary and by creating a dictionary like class in C++ it was faster than the .NET version. I will not promise anything regarding those articles, but if you want I can send the test applications to you.
|
|
|
|
|
Yeah that would be nice.
While unordered_map is really slow, in my experience list/vector are really fast (at least with icc they are). I would love to see those test programs - maybe the performance is strongly compiler / environment dependent.
Async/Await is a hybrid. As like other C# features it relies on .NET code. In this case there is an async state machine (implementing IAsyncStateMachine ) that is available in .NET 4.5. Since .NET 4 has also Tasks and Microsoft did the work - you can get async/await to work with a NuGet package (provided that you have C# 5 that supplies you with the keywords, of course).
So for using async /await you will depend on the targeted .NET version (this happened in the past with yield - which will create an enumerator class for you; you'll need at least .NET 2 for this as this uses generics).
|
|
|
|
|
I will send you the test application when I arrive home.
|
|
|
|
|
Paul,
Good job. You could make some improvements. Because your pool is only for one type of object, which is a good thing, you can change the 'void *' and 'char *' in most places and change the types to 'T *', only casting if necessary, which is probably never. (I'd have to look closer to be sure that no casting is necessary, but some variables definitely use the template type and casting can be avoided).
Also, if type T uses multiple derivation, casting from 'void *' to 'T *' won't always give what is expected. See Scott Meyer's book "Effective C++" for a discussion of this. (It might be in his book "More Effective C++"?)
Finally, it's pretty standard in C++ to name class data members with the prefix "m_". It's not wrong to use just "_", but it is unusual in C++ code.
|
|
|
|
|
Sorry, but I didn't understand what you said.
My pool is only for one type?
If you are talking about a pool created for type X is not capable of creating type Y, it's true. But this is part of the O(1) performance. All objects must have the same size. In fact, I could make things differently and make the pool work for different types of objects as long as they have the same size, but as I know this will complicate things (even on utilization) I didn't do this optimization.
About the naming rules, well, I already had a comment on this. I actually have more of a .NET naming convention and C++ Builder naming convention in mind (which is pretty similar to .NET one). So, I know, it is not the most known standard, yet I think the thing that may bother users is the fact that methods use NamesLikeThis instead of namesLikeThis. The private members I really expect users to ignore, as they will not be visible to the users of the class if they don't look at the source code.
|
|
|
|
|
Paul,
Some of the functions you wrote could be added as method to your Object pool template class.
This would make the code simpler for two reasons. The code wouldn't have to deal with the size of objects. The compiler would do that. It also wouldn't be necessary to cast in the code. The methods could just use the template types in method argument lists, method return types, and inside of methods, just as if they were any other type.
Rather than trying to rewrite your large class, I'll put a simple template class here. I'm not going to compile this code, so I might make a mistake.
This class doesn't do anything, in fact, it's nonsense! I'm just trying to relate the syntax for template methods in C++ that allow using the types.
#ifndef SOMECLASS_H
#define SOMECLASS_H
template <typename T1, typename T2>
class SomeClass
{
protected:
T1 * m_instance;
T2 * m_something_else_ptr;
public:
SomeClass(int i);
~SomeClass();
T1 * GetInstance() const;
T2 * GetSomeOtherInstance() const;
void SomeOtherMethod(T1 * instance_ptr);
};
template <typename T1, typename T2>
SomeClass<T1, T2>::SomeClass(int i)
: m_instance_ptr(new T1(i))
, m_something_else_ptr(NULL)
{
m_something_else_ptr = new T2()
}
template <typename T1, typename T2>
SomeClass<T1, T2>::~SomeClass()
{
delete m_instance;
delete m_something_else_ptr;
}
template <typename T1, typename T2>
T1 * SomeClass<T1, T2>::GetInstance() const
{
return m_instance
}
template <typename T1, typename T2>
T2 * SomeClass<T1, T2>::GetSomeOtherInstance() const
{
return m_something_else_ptr;
}
template <typename T1, typename T2>
void SomeClass<T1, T2>::SomeOtherMethod(T1 * instance_ptr)
{
T1 foo(5);
T1 * foo_ptr = &foo;
T2 bar;
T2 * bar_ptr = &bar;
}
#endif // SOMECLASS_H
By the way, even when passing a class instance, you can use 'typename'. The 'typename' keyword was added to replace the class keyword because the former is too restrictive. However, some older compilers don't work properly with typename, although those are very old compilers.
modified 14-Apr-14 23:01pm.
|
|
|
|
|
Honestly I still don't understand what you are trying to say.
Note, I use casts for two reasons:
1) I use the placement new. So, I allocate raw memory. Then I do the "math" using char. It could appear that I could use the target type directly, but I multiply a value that's the sizeof() the target type aligned as a pointer. So, I can't simply use a cast to T and add the item number. I must use char and multiply the itemSize by the item number. Then I do the final cast;
2) For deleted items, I use the memory as a pointer to the next deleted item pointer (so, a pointer to a pointer). I could use an union between the real type and a pointer, but I decided to use a cast.
So, what I don't understand is: How do you want me to use template parameters differently?
In fact, I want to avoid more template parameters. I don't want users to have to pass other parameters. They only need to pass the minimum (the destination type they want) and the class will do all the job. If they really think that using the default allocator is bad, they could provide their own allocator. And that's all.
The only thing that I would change is the use of variadic template arguments to avoid the need of the GetNextWithoutInitializing, but I am not using a compiler that supports variadic templates and I still think that such method is useful.
It is important to note that if you use the GetNextWithoutInitializing the default constructor of the type T will never be invoked for that item. If I used an ordinary new[], all items will be constructed using the default constructor (and so I should not use the placement new anymore).
This is not only a performance penalty as it is a problem if a type doesn't provide a default constructor. This pool is capable of working with types that don't have a default constructor thanks to its capacity to allocate raw memory and use the placement new.
|
|
|
|
|
What I was trying to state before is related to how to declare template methods so that they are not inline, which would make your ObjectPool class better. Unlike C#, most C++ methods that are more than a few lines are not usually declared inline methods in the class definition. When they are, the compiler can decide to make the function inline in the code. This can cause the code to grow large and also hurt performance, because inline code might not be cached. So, if instantiating the same class type multiple places in the code using the same ObjectPool instance, you probably don't want the method put inline in every place it is used.
My last post shows how to hint to the compiler that the code should not be inlined. By creating a method body that is unique, similar to putting a C++ method in the implementation file, the compiler will (typically, not always) create a single method in memory for all instantiations of the template for a specific type T. (In some cases that you will probably realize, there will only be a single method for two instantiations of a template class for different types as long as the two types are the same size and the method only uses the size of the object). Modern compilers are pretty good about things like that. By the way, there are linkage issues with templates and how many times the compiler creates a method too, but modern compilers do a fair job of minimizing duplication of code.
I realized afterward, the code can be modified to have one cast, not zero casts. But only one is needed. The code can also be simplified with regard to the size of the template type. These changes make the code simpler and have no performance penalty at all.
First, change class DefaultMemoryAllocator to be:
template<typename T>
class DefaultMemoryAllocator
{
public:
static inline T * Allocate(size_t capacity)
{
return (T *)::operator new(capacity * sizeof(T), ::std::nothrow);
}
static inline void Deallocate(T *pointer)
{
::operator delete(pointer);
}
};
That needs only one cast.
Then change "_itemSize * _capacity" in the call to the allocator to just "_capacity".
I took the "size" parameter out of the Deallocate method. If you want that to extend capabilities in the future, you can add it back, but it is not used now - and I don't know how it could be used in the current code. Of course, if you remove that, then you have to remove the size at the call site.
To use that class above, declare the allocator in the ObjectPool class as shown below:
template <typename T, class TMemoryAllocator=DefaultMemoryAllocator<T> >
class ObjectPool
struct _Node
{
T *_memory;
size_t _capacity;
_Node *_nextNode;
<rest of code ommitted>
Note the "T *" declaration for the data member named "_memory".
Also note the declaration ends with TMemoryAllocator=DefaultMemoryAllocator<t> >. There has to be a space after the first ">" and the second ">" so the compiler won't confuse that with operator ">>".
Then change all other "char *" and all "void *" declarations in the code to "T *", and remove all casts except the one cast in the DefaultMemoryAllocator class.
Leave the rest of your code the same.
Also, see http://www.parashift.com/c++-faq/memory-pools.html[^] That does not deal with the O(1) performance, but ideas there are still relevant. You're code uses some ideas from that site.
modified 15-Apr-14 22:59pm.
|
|
|
|
|
The sizeof(T) in the DefaultMemoryAllocator will not work with char, which is used in the example.
I don't want to put the minimum size requirement in the allocator, as it should be independent of the ObjectPool, yet the pool really requires any type to be aligned as a pointer.
So, in a 64-bit system, a char, a short, a long, or any struct that has less than 8 bytes will actually multiply the value by 8. That's why I use that _itemSize. If the sizeof(T) was enough, I would have used sizeof(T) * something.
I do not do casts to (void *). I use void * to calculate the size of a pointer. And the char * is needed because I want to sum a value that may not the the size of T. Also note that in some situations I do a cast to T **. This is because a deleted item becomes "linked list" of deleted items (so, it is a pointer to the next free item).
Also, remember that how many casts this code does is not important. Changing the cast from one place to another but adding another template class is not really making things better. The important is if users will need to cast the results of the pool or not. And they don't. As I don't use variadic templates, they may require to use the emplacement new, but that's all.
About the size in the delete, I did it to support possible memory managers that don't track the size of the allocated block (amiga OS did had this case... I know there are other cases out there). Yet, in a typed memory allocator, that's not an issue.
About my code using ideas from that site... maybe we have compatible ideas, because I didn't use that site to write mine.
modified 15-Apr-14 22:58pm.
|
|
|
|
|
Paulo Zemek wrote: The sizeof(T) in the DefaultMemoryAllocator will not work with char, which is used in the example.
I don't want to put the minimum size requirement in the allocator, as it should be independent of the ObjectPool, yet the pool really requires any type to be aligned as a pointer.
The alignment always belongs in the allocator. Alignment is a property of the platform memory system, not of the type T. In any event, if you want it in the ObjectPool, it can be passed into the Allocate function, or another template parameter can be added to the DefaultMemoryAllocator for the alignment size.
For example:
template<typename T>
class DefaultMemoryAllocator
{
public:
static inline T * Allocate(size_t capacity, size_t alignment_in_bytes)
{
size_t size_aligned_type = sizeof(T);
size_t mult = size_aligned_type / alignment_in_bytes;
diff = size_aligned_type - (mult * alignment_in_bytes);
size_aligned_type += diff;
return (T *)::operator new(capacity * size_aligned_type, ::std::nothrow);
}
static inline void Deallocate(T *pointer)
{
::operator delete(pointer);
}
};
Of course, placement new, which calls malloc, will return an aligned pointer, so there is no need to worry about the first item.
But, sizeof(char) should be fine without doing any alignment. An array of char is fine, you can increment addresses by one and point to the next character. So now I do not understand what issue you are referring to. In any event, handling the size of the object in the ObjectPool class does not make sense to me - that is the exclusive domain of the allocator. Everything else in the C++ memory system is designed that way.
Perhaps there is something that I don't understand. If so, the DefaultMemoryAllocator I posted could be changed slightly to accomodate those changes without any limitation to the DefaultMemoryAllocator or the ObjectPool. Avoiding the casting has benefits I mention below, particularly avoiding the use of "void *" in the code.
Paulo Zemek wrote: I do not do casts to (void *). I use void * to calculate the size of a pointer. And the char * is needed because I want to sum a value that may not the the size of T. Also note that in some situations I do a cast to T **. This is because a deleted item becomes "linked list" of deleted items (so, it is a pointer to the next free item).
I did not write you "cast to void *". I wrote change the "declaration" of "void *" to "T *".
Using void * has issues when storing class instances in C++. If your type uses multiple inheritance, and you can't always control that if you use third-party code, then casting to "T *" from "void *" and then trying to cast again to one of the inherited types might not do what you expect. void * should be avoided in C++ whenever possible. (There are articles about this, I'll try to find one). In this case, there is no benefit to using "void *", so why use it?
Paulo Zemek wrote: Also, remember that how many casts this code does is not important. Changing the cast from one place to another but adding another template class is not really making things better. The important is if users will need to cast the results of the pool or not. And they don't. As I don't use variadic templates, they may require to use the emplacement new, but that's all.
As I mentioned above, C++, casting can result in bugs if casting from "void *" to a type that uses multiple inheritance.
Also, casts make code harder to read because the actual type stored is not clear throughout the code. Casts are sometimes necessary, but should be avoided.
modified 15-Apr-14 23:57pm.
|
|
|
|
|
Bill, I am not talking about real alignment. I am talking about the fact that if you use a pool of chars (like I did), when you "delete" an item from the pool, I will treat it as a node in a linked list (a pointer to the next deleted item).
So, independently on the size of the data and independently on its real alignment (chars can be 1-byte aligned), I need it to have at least a pointer size, which is a particularity of the Pool, because I do such unsafe casts.
"Of course, placement new, which calls malloc, will return an aligned pointer, so there is no need to worry about that."
No. The placement new doesn't call malloc. In fact, it doesn't call anything. The object is simply constructed on the pointer I am giving.
"I wrote change the "declaration" of "void *" to "T *". There is no reason to not do this, you always will only store a pointer to a potential T * instance in _memory."
Wrong. On the deleted items, instead of containing T the address contains a T*.
So, as I am already using a pointer to get there, it is either T* or T**. It is a needed extra level of indirection. And, as the size needed may not be the size of T, I calculate the value over char *, to correctly sum the pointer.
To explain with an example:
Imagine that I allocate memory for 4 chars from the pool. I will fill the word Test in each allocated char:
T___e___s___t___ (first deleted is NULL, or 0000)
They will not be 1 byte aligned (as char allows). In this case, they are 4 chars aligned (it is not 32-bit because it is text, but it could be).
When I delete an item (the s, for example), I will put the address of s in the first deleted member, but the s will point to the "next" deleted. As it was null (because s is the first deleted) the memory will be like this:
T___e___0000t___ (first deleted is 1008)
Now I delete the e. The address of e will be put in the first deleted, but the contents of e will point to the previous value that was there (the address of the deleted s... it becomes the "next" deleted in the sense that it is the next to be used when we do reallocations).
T___10080000t___ (first deleted is 1004)
* I am considering the buffer to be in address 1000, so the address of S is 1008.
Now I add a new value. It will be put in the place of the deleted e, then the value 1008, which is the address of deleted s, will become the first deleted again:
T___b___0000t___ (first deleted is 1008)
Finally, I add C:
T___b___c___t___ (first deleted is NULL)
So, the allocator of char can allocate one byte at a time. Yet I need a minimum of 4 bytes (in this demonstration) to fill the addresses of deleted items.
About the casts to void: I am dealing with raw memory. I return the value cast as the type the pool creates. There's no risk of seeing the class as the wrong sub-type, as the pool can't create sub-types.
Your last paragraph restates the problem of casts with multiple inheritance, but again, it doesn't exist.
This is a low level component. It allocates raw memory (something memory pools and components like vector are expected to do) then uses the placement new on that raw memory. After the object is returned, you can cast it from one type to another without ever using a void *, but the only thing the pool cares is about memory, placement new and "placement delete".
|
|
|
|
|
"Of course, placement new, which calls malloc, will return an aligned pointer, so there is no need to worry about that."
No. The placement new doesn't call malloc. In fact, it doesn't call anything. The object is simply constructed on the pointer I am giving.
I agree, I made a mistake here. I was referring to the operator new in the DefaultMemoryAllocator class, NOT to placement new. I have used placement new before, I just used the word "placement" because it was mentioned earlier and I am tired.
Okay, I get that you are storing pointers to deleted items in T instances. I will take a look at that again.
My point is, the alignment and the casts can be greatly reduced by using the template code differently. And, the template methods should definitely not be all inlined for the reasons stated in my earlier post.
It is possible to write a template the does the alignment for you, and store T instances in that template. I started to work out the details but it is late. Suffice to say, it would be something like.
pragma pack(sizeof(void *)) template <typename TS>
struct AlignedStruct
{
TS instance;
operator TS *()
{
return &instance;
}
};
pragma pack()
That class is a simplification to save space, and will make the size a bit larger than necessary - Using a technique in "Modern C++ Design" with the int2type template class, the sizeof(AlignedStruct<t>) can be made the same as the aligned value for class T that is calculated in your code now. The more complex solution requires making a few templates for the size ranges below sizeof(void *) and using template specialization and the int2type template class to automatically select the right template. Search for int2type - if nothing else, you will find the concept interesting.
The object pool would be declared as:
template<typename t,="" class="" tmemoryallocator="DefaultMemoryAllocator<AlignedStruct<T"> > >
Now, DefaultMemoryAllocator would allocate a size equal to.
sizeof(AlignedStruct<t> * capacity)
If the memory is declared as
AlignedStruct<t> * _memory;
Then
T * var = _memory;
will do the right thing because of the cast operator declared in AlignedStruct.
The same is true of any method that returns type T *.
Now I understand you will still have to cast for the deleted items, but most of the other code is automatic. By using AlignedStruct<t> internally, you can copy or return T * anytime, and then cast to whatever.
Also, using AlignedStruct<t> internally, you can now do:
address += count;
wheres before it was necessary to do:
address += count * _itemSize;
You can get rid of _itemSize;
Sure, I might be missing something, and there are other details, but the code can be simplified. There are only rare cases in C++ where the compiler cannot handle the size of objects automatically, and, while I was wrong before, casting cannot be totally eliminated because of your deleted items, it can be greatly reduced by making it automatic, along with making the alignment to a pointer size be automatic.
Oh, and I gave you a 5. I am nitpicking. I didn't test the code, but I presume it works and the design is clever. I would be interested in seeing timing data between your class and the corresponding boost class for a moderate size pool used over time.
modified 16-Apr-14 1:59am.
|
|
|
|
|
Okay about the placement new error.
But about the template... even if I can make the allocator deal with the alignment, I don't want to. It means that the allocator will need to know the alignment needed by the dictionary.
I can, of course, make it work as a template. But in that case I will give both the number of items requested and the minimum alignment. Even if today only the pool uses it, I don't want the allocator to be dependent on the pool. So, if it is a template it will receive the right type it needs to deal with and will be able to use the right alignment for it. Yet, I will give an extra requirement that's the alignment I want. But if I do that, I will need to receive a result of the final size used for the "object allocations". I really don't think this will make things easier.
So, this will actually simplify the places that use the cast, but it will complicate other places (the template will be harder, there will be more parameters, when receiving the result I will need to store the size used by the allocator)... so, I will solve a small cast and I will add other complexities.
About the boost comparison, I didn't made one, but I don't know if boost actually has an O(1) pool for individual deletes. As far as I know, boost has the advantage that it deletes all inner items if the pool itself is deleted, but individual deletes are pretty slow. So, they solve different problems (I actually know very rare situations where allocating many objects without deleting them will be acceptable... so in the end I can delete them all... but allocating and deallocating many small objects in a loop, for example, is something common).
About the 5... thank you
|
|
|
|
|