[reSIProcate] Fixing Memory Fragmentation and Increasing Performance (Part 2)
Hi guys,
As previously noted, Data presents challenges that greatly increase
memory fragmentation and reduce performance, while at the same time,
tries to service too many goals.
Consider this situation:
Data has a "local allocation" of 16 or 17 bytes, that is primarily only
helpful in the case of storing resiprocate's transaction ids. In that
case it gives a decent performance boost by not requiring another call
to the allocator. However, anytime a sip message is parsed, and Data is
in Share mode, those 16 or 17 bytes go to waste. Useless memory
allocations.
While keeping in touch with the capabilities of C++0x, we've come up
with a solution that can help reduce the space requirements for data
significantly, while providing good/strong move semantics, and type
safety. This yields major performance boosts when utilizing with things
like vector and methods like sort(), as well as cutting down on the
memory consumed. It also makes serialization much faster by allowing
allocators to exist on a per Uri level or a per SipMessage level,
reducing cache misses during the all intensive serialization processes.
Under the new proposal, we would add a virtually inherited DataBuffer
class, with a global version that uses the heap for its allocations.
This way, Data can be specialized based on how it is used, therefore in
a Uri, it could be one DataBuffer instance for the whole class that all
the Data objects share and it manages the allocations and for all of
those instances.
As part of this, we could also still solve the TransactionId problem by
creating a DataBuffer that has a LocalAllocation of 16 or 17 bytes.
Therefore allowing full customization based on the use.
As part of the recommendation, we fully remove or make things like
reserve/capacity dummy functions, and we move towards something like
realloc instead. Likewise we get rid of variables mCapacity and mMine,
and replace with a pointer to the DataBuffer instance.
As for move semantics, a move constructor, a move operator=, and a
specialization of std::swap all need to be defined.
Usually I implement a move constructor through move operator=, but in
this case, it is important to maintain proper ownership and integrity
over the DataBuffer. Likewise to take advantage of the benefits of
vector and sort(), we need to separate how they work to make usage as
safe as possible.
Here are the basic rules that I've defined for move operations:
- If mDataBuffer == other.mDataBuffer (or NULL if both use the global
DataBuffer), then move operation can be carried as a regular move
- If the move constructor is called by not move operator= as is the case
with push_back and vector (required per the upcoming standard), then the
above requirement does not apply, and mDataBuffer can be passed inward,
because it would be initializing uninitialized memory.
- Finally even though 99.999% of objects in a vector are going to
utilize the same DataBuffer, for the corner case of sort or any other
operation that uses std::swap, a std::swap specialization is required so
that it operates like the Move Constructor does therefore allowing full
performance moves in complicated operations with the least amount of
memory copied.
I'm interested in everyone's thoughts, let me know.
Dan