Messages outgoing buffer, avoiding frequent heap allocations

Started by
14 comments, last by jmp97 15 years, 4 months ago
@Kylotan: I see, thanks for sharing.

I just wanted to share what I came up with after carefully examining your input:

Messages are not fixed size so I cannot do fixed size object pre-allocations.

I went with a system that works a bit like <vector> such that allocations happen in chunks with some well-sized initial allocated space. The reasoning is that by choosing the initial allocation size with care, allocations for more chunks will happen only at rare occasions, memory will not have to be freed (unless after some timeout additionally allocated memory has not been used), the allocated bytes can be used directly for sending (no byte copy), and memory is mostly contiguous.

Each client maintains a byte buffer manager. The byte buffer manager merely holds a list of buffer arrays (char[]) which have a fixed size. Initially, there is only one pre-allocated buffer array in the buffer manager.
Should the first buffer array fill up a new one is allocated and added to the list.
The byte buffer manager maintains a read and write cursor and exposes functions to move either:

char* BufferManager::requestBytes(unsigned int numBytes)

Checks if the number of bytes in the current buffer array - counting from the write cursor position - is available. A pointer to the buffer array is returned for writing and the caller will have to make sure not to write more bytes than what was requested from the buffer. The write cursor is moved after the last requested byte. If there is need to allocate a new buffer array, a new one is allocated and the pointer is returned from this new buffer array.
This is transparent to the caller.

unsigned int BufferManager::nextDataSize();

Returns the number of bytes which can be used fo sending in one go.

char* BufferManager::getReadCursor()

Returns a pointer for sending the bytes in the buffer.

char* BufferManager::shiftReadCursor(int numBytes)

Shifts the read cursor for the given number of bytes, transparently using the next buffer arrays in the list if necessary. Returns a pointer for sending the bytes in the buffer at the newly determined read cursor position. When the cursor is set such that there are no more remaining bytes after the cursor, write and read cursor are both reset to the first byte of the first buffer array.

Some space may be wasted when a requested byte chunk does not fit onto the end of one buffer array, but if the buffer array sizes are carefully I assume that to be negligible.
Another problem is that for those parts which have already been sent (read cursor was shifted away from position 0), writing to the beginning of the buffer to use these free bytes is not possible, only wrting at the current write cursor position up to the end.
Thus, if the maximum number of byte arrays is filled up, but some of the buffer has been read (read cursor pos > 0) the buffer manager will not "wrap" to write to the beginning of the buffer up to (read cursor pos - 1), like a circular buffer. I figured that when this condition occurs sending to the client was too slow for a given time and I would just disconnect.

This was mostly done as an exercise to collect some profiling results and experiment with pre-allocation. I hope this can serve as inspiration for others.
Advertisement
Quote:Messages are not fixed size so I cannot do fixed size object pre-allocations


If messages have a maximum size, then you can pre-allocate that maximum upper size for each pre-allocated message.
enum Bool { True, False, FileNotFound };
That is correct but I was worried about "wasting" memory that way, since only a few messages are max. sized.
I did not want to reserve 500 bytes for messages of 16 - 128 bytes. Or did I miss a point?
I use a duo scheme, where each message buffer has a mininum size, ie 64 bytes, prellocated buffer which about 95% of the packets will fit into just fine, but it has the capablity to allocate a larger buffer if need be ( this is pretty rare ). It maanges its own mememory, so will clean up the buffer once it get destroyed or recycled back into the pool.

This for me seems to be the best comprimise between memory efficenty ( ie no/low runtime allocation overhead ) and flexblity ( ie don't have to worry about various size packets and best optimize for them). At any one time only a few packets are pending so the pool really never gets that large so memory isn't wasted.

Good Luck!

-ddn
Quote:I did not want to reserve 500 bytes for messages of 16 - 128 bytes. Or did I miss a point?


Let's suppose the max message size is 1 kB. (That's a large message!)
Let's suppose the max users on your server is 1,000. (That's a lot of users!)
Let's suppose each user can have up to 100 outstanding messages. (That's a lot of messages!)
If you pre-allocate the absolutely worst case of messages, you'll allocate 100 MB. Yee-haw!
That's about 2.5% of available memory on a current server with 4 GB of RAM (those 4 GB will cost you all of $70 to buy).

However, typically, you'll keep a free list of messages, where sent message objects go back onto the list, and new messages are pulled from the list unless the list is empty, in which case you allocate a new message on demand.

Considering that the network bandwidth out is your limiting factor: What will you be paying for? 10 Mbit/s? That's about 1 MB/s of messages, which if you drain the queue per user 10 times a second means a peak allocation of 100 kB times your load factor, which might be as high as 20 (50 bytes average per message) and still you're only using 2 MB. The amount of pre-allocated messages is highly unlikely to ever be something to worry about (and unlikely to ever get close to the theoretical 100 MB max).
enum Bool { True, False, FileNotFound };
Ok, thanks for your advice.

This topic is closed to new replies.

Advertisement