Skip to main content
GameDev.net gamedev.net
🔒 Locked

Multithreaded rendering and logic - message queues

Started by nekitusan May 23, 2009 at 7:56 AM 3 replies 1.6k views
Original Post
nekitusan
nekitusan
Hi, I want to make my engine multithreaded, so for that I would be using a message/command queues mechanism. My question is, for example the logic thread fills a queue for the render thread to draw, is STL fast enough or should I make a flat memory buffer which will be used as a pool, with rare realloc of memory, baring in mind that this command queue will be filled several times per second, and I dont know if push_back() will be fast enough. Other thing good when you have a linear buffer where you store command data is that you dont need to alloc a specific class for each command type, example:

class QueueCommand
{
   public:

   int m_id;
}

class SetRenderStateCommand: public QueueCommand
{
   public:
   int m_someCmdSpecificMember1;
   int m_someCmdSpecificMember2;
}

Methods to store the queue commands:

1. STL -> deque<QueueCommand*> m_queue; // bad, we must have pointers 
because we need different command types, with different sizes, too much small 
allocs; one workaround is to make the QueueCommand same size for all the 
command types, and just to have a class for all IDs like:

class RenderCommand
{
    int m_commandId;
    float m_param1,m_param2,m_param3,... until a maximum of params you settle on
}

and then you can have a: deque<RenderCommand> m_queue; // no pointer now,
 we have a constant size of the command data, OR we can use a memory pool manager for all those small allocs.

2. Flat buffer -> char* m_queue; // good, no more small allocs, just fill 
this buffer with command IDs and command params, but its a little tricky to pop
commands, needs a memory move operation when a command is popped from the queue
Thanks for any oppinion.
ToohrVyk
ToohrVyk
The only way to know if an approach is fast enough is to try it, especially with modern processor pipelines, caching and multithreading. Your priority should therefore be to build a working prototype as fast as possible so that you can identify the bottlenecks. And using the standard library is always faster than rolling out your own implementation in terms of development speed.

If the bottleneck turns out to be allocation, you can provide your own allocation strategy for the standard library containers to use, so you don't have to reinvent the containers themselves.

If the bottleneck turns out to be something else, at least you won't have to throw away too much work.

And if everything is fine, you'll be happier than if you had to write everything on your own.
nekitusan
nekitusan
Yes, you are right, I have just implemented it with STL deque, it works great, I will have to test it more with some massive queue size :) and some more allocs, now I'm using static fixed size command class, deque m_queue, no pointers, I will come back with results and maybe test some custom STL memory allocators.
Antheus
Antheus
To pass such data between threads, I use a semi-custom container. Each queue has its own, pre-allocated buffer. For performance-sensitive operations, the upper bound on number of items in queue can usually be set pretty well. Instances of arbitrary sizes can be allocated this way, even polymorphic types, but destructors will not be called (optionally, they can be).

The queue allocator simply allocates memory sequentially. Queues in this case behave as FIFO, single read/single write of each element.

Then I pre-allocate several such queues. When producer is done, it passes the pointer to one queue to other thread, and gets empty queue from pool. When consumer empties the queue, there is no need to deallocate anything, just the allocator's free pointer is reset.

Main reason for this is not so much great performance (although it helps), but more maximizing concurrency, as well as simplification of memory management. While default allocators are thread-safe, in this particular scenario I do not feel the need for it, since each queue is always being handled exclusively by one thread only.

The only contention that occurs in this case is once per logic update (which will likely be 50-100 times a second), and due to small amount of data transferred (single or double pointer-sized data, depends on mechanism), can be implemented efficiently in a lock-less manner. I don't support queue resizing. If writer determines that push failed, it submits current queue, and gets a fresh one. If no fresh queues are available, then consumer has some serious problems and it's time to pause.

None of the above is needed. It's just a "pattern" (sigh) I found myself using for this type of data exchange. There exist other ways and YMMV.
nekitusan
nekitusan
I use the consumer-producer queue switch too, for the logic and render, while the render thread loops again and again the consumed queue, the producer fills up a render command queue, and when done, it switches the queues, so the render will be always occupied with rendering a current queue of commands.
I understand your decision, and it seems a STL container and some custom memory pools will go nicely, with the fixed sized buffer too, thanks for the clarification guys :)

Topic Locked

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

Sign in to reply to this topic.