Wednesday, December 5, 2012

Multi-threaded task-based execution

Splitting work carefully into granular tasks  (while ensuring that each task has enough "meat" to offset synchronization overhead) and then running them in parallel is a performance win most of the time, especially when CPUs seem to have an increasing number of cores these days. Still, one must be careful to avoid false sharing or otherwise thrashing the local caches in each core as this is usually the biggest performance killer barring any unnecessary synchronization.

Most implementations I've seen define the order of execution of tasks implicitly by their dependencies. Something like:

Task taskA, taskB, taskC, taskD, taskE;
taskC.DependsOn(taskA, taskB);
taskD.DependsOn(taskC);
taskE.DependsOn(taskA, taskC);

AddAllTasks(pThreadPool);
pThreadPool->Run(); // Block until all tasks complete

Now figuring out which task goes first from just the dependencies is quite trivial in the above example, but it gets considerably more difficult in a real world situation:

Frostbite 2 task graph
Without a generated task graph like the one above, figuring out the order of execution of each task just from their dependencies alone in code can be tedious, which happens often enough especially when debugging.

While nothing stops ARF applications from using a task-execution framework like the one above (after all ARF is just a bunch of related libraries and is not really an "engine"), by and large I still prefer explicitly ordering tasks in code simply because it's much clearer:

pThreadPool->AddTask(taskA);
pThreadPool->AddTask(taskB);
pThreadPool->Run(); // Block until all tasks complete

pThreadPool->AddTask(taskC);
pThreadPool->Run();

pThreadPool->AddTask(taskD);
pThreadPool->AddTask(taskE);
pThreadPool->Run();

This is functionally equivalent to the one above but isn't the ordering immediately apparent?

In any case, in addition to the usual synchronization primitives and atomic instructions, one of the ARF modules also provides a robust and lightweight thread-pool implementation that supports multiple task consumers and producers, with the total amount of code within a contention block only about 8 instructions long, so squeezing maximum performance out of multi-threaded applications becomes less of a chore.



No comments:

Post a Comment