Skip to main content
gamedev.net
🔒 Locked

Networking memory management issues

Started by Nairou 6 replies 850+ views
Original Post
Nairou
Nairou
I've done a bit of network programming with sockets in other projects, and created a networking class that allows me to easily perform server/client networking tasks, however the one part I was never satisfied with was the memory management of incoming and outgoing packets. I currently use two std::vector buffers per socket; one for incoming packets (or partial packets) that have not yet been processed, and one for outgoing packets that have not yet been sent. When a packet in the buffer is processed, the remaining buffer was moved to the beginning of the vector to process the next packet. I used a std::vector for these so that memory allocation was taken care of, and by moving all remaining buffer data to the beginning of the vector after each packet was processed, I loosely ensured that the buffer wouldn't keep growing and being reallocated. But it feels flawed. For one, all that copying of packets within the vector surely can't be efficient when you're dealing with game environments and lots of connected players. And by relying on std::vectors for my packet buffers, I never know when a larger-than-normal packet will arrive and cause my vectors to be reallocated, possibly several times. Given that there is a continual stream of packets incoming and outgoing, what are some ways to minimize reallocation and memory copying? Or is it just a fact of life with networking? I don't really have any game examples of how this could be done, my method just doesn't seem like it would be the best there is. Any tips would be awesome.
Palidine
Palidine
For starters I'd switch to a std::queue instead of a std::vector. It's a container specifically designed for what you're using your vector for (i.e. a queue). Since it's designed for this task i'd imagine it performs better.

That said, have you actually profiled your application and identified this as a hotspot for your application? I'd switch containers anyway but in general there's zero point in trying to optimize something that you haven't empirically identified as a problem area.

-me
Nairou
Nairou
Thanks for the reply. I'll take a look at queue, though my main concern is whether they are contiguous or not, since I'm basically using the vectors as byte arrays (I don't know a better way to efficiently handle byte sequences than in a vector/array).

I haven't done any profiling, and really I'm at too early a stage to worry about optimization, I just want to make sure my general design is solid so that I don't run into trouble later. Anything requiring a large amount of copying data around, as I'm currently doing, raises a red flag to me, to double-check it.
Antheus
Antheus
Quote:
Original post by Nairou
I haven't done any profiling, and really I'm at too early a stage to worry about optimization, I just want to make sure my general design is solid so that I don't run into trouble later. Anything requiring a large amount of copying data around, as I'm currently doing, raises a red flag to me, to double-check it.


Yes, it's not optimal. But is it a problem? This is not a design issue ( Network layer buffers data for later processing in FIFO manner) <- no performance here - design in such a way to fulfil the design.

Also:
http://elonen.iki.fi/code/misc-notes/ringbuffer/index.html

No copying required, except as required on each buffer overrun, and even then you don't need to, if you're using scatter/gather writes.

But everything else depends on other factors.

Nairou
Nairou
Excellent points, Antheus, and thanks for the link. Could you perhaps explain how scatter/gather writes would be applied to something like this? Does that just mean allocating a number of buffers rather than one, and keep track of which pieces of a packet are in which buffers? I assume this is done just to allow the total buffer size to expand without reallocating the initial buffer?

As far as it being a design decision, this sort of stuff has already impacted design when creating and using my networking class, that's why I wanted to double-check it before having to rework it again and impact a lot of other dependent code. Previously, my networking class interface took a buffer pointer, where it copied the incoming packets. That was later ditched for the central buffers and passing pointers to them, in order to cut down on memory copying and, more importantly, to keep track of partial packets that weren't ready to be handed off. So I just want to be smart as I move forward in this, as bad networking can make for unpleasant laggy gameplay.
Kylotan
Kylotan
I'm not sure how they would apply - the issue really is just the efficient use of the data structure and isn't networking specific. If you only need one buffer in each direction now, you'll only need one buffer in each direction after changing to ring buffers. It's simple in concept - instead of moving the data and reading from the same place, you leave the data in place and read from a different position. Ultimately it should be a pretty trivial operation, apart from when you choose to resize the buffer, but that can just revert back to the copying method for that iteration only.
Antheus
Antheus
Scatter/gather saves you an extra copy + 1 buffer.

Assuming you need to read a data chunk 70 bytes long. Your ringbuffer has only 20 bytes left at the end, but 24,000 in the beginning. Maximum possible data chunk size is 3000 bytes.

In order to read the data, you'd need temporary buffer to read the whole packet into, and then copy from temp buffer into ringbuffer.

Instead, scatter/gather allows you to pass two buffers: ( ringbuffer.end-70, 70 ) and (ringbuffer.begin, 2930) to socket read, and read straight into ring buffer.

Same goes for writing. If your data chunk is split across the end of the ringbuffer, you'd be saving an extra copy, by just passing two buffers again.

Even more, if you always want to read maximum possible ammount of data (up to 64k), then you can always pass entire available space in ringbuffer to socket call, and it'll fill up as much as needed.

Note that this isn't necessary or needed, it's just making maximum use of underlying API.

Quote:
So I just want to be smart as I move forward in this, as bad networking can make for unpleasant laggy gameplay


Not because of memory allocations. Those will never affect the gameplay. The threading/processing strategy will determine laginess.
jpetrie
jpetrie
Quote:

For starters I'd switch to a std::queue instead of a std::vector. It's a container specifically designed for what you're using your vector for (i.e. a queue). Since it's designed for this task i'd imagine it performs better.

FWIW, std::queue is actually a container adapter; it takes a third template parameter which is the type of the container to adapt. By default this is a std::vector, so performance would actually likely end up being quite similar.

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.