5
\$\begingroup\$

I am currently developing a rendering engine, which I plan to use for creating games. The engine makes heavy use of multithreading; I have a thread for OpenGL rendering, a thread for updating, a thread for processing user input, a thread for audio, etc. I have heavily tested it, and the threads work like clockwork. The multithreading approach has shown to be faster, more robust and flexible on a multicore processor, which I verified by profiling.

However, something which I have not considered is CPU-core affinity for threads. Should I care about it? What benefits does pinning threads provide, and to which CPU core do I pin which thread?

\$\endgroup\$
5
\$\begingroup\$

I wouldn't bother unless you have a very specific issue which you know is solved by this. (And if you have to ask, you probably don't.)

Modern OSs are quite good at distributing threads around CPU cores to balance the workload well and avoid overworking individual cores, plus your code is not going to be the only running code that needs to use CPU cores. Threads in your drivers, in any libraries you use, and in other programs or background processes will also need CPU time, and you certainly don't have knowledge of exactly what is going on there. You don't want to be at the risk of starving your GPU's driver of the CPU time it needs just because you were greedy with your own code, do you?

For example, and just to illustrate, here's a Task Manager shot of a single-threaded game running on Windows 10:

enter image description here

Bearing in mind the usual caveats about Task Manager as a profiling tool, we can nonetheless make a few observations.

  • The game itself is single-threaded (and, in fact, runs flat-out).
  • At no stage is any CPU core pegged at 100% usage.
  • Average CPU usage exceeds 25% (not visible in this shot, it was 35%).
  • So over 4 cores we can roughly say that 25% was the game running flat-out and 10% was other work.
  • And the OS nicely moved the work around cores to keep the system running well.
\$\endgroup\$
  • \$\begingroup\$ I'd like to give one example where the OS isn't smart about distributing loads: when the system you're running on has HyperThreading, or if two mostly idle threads are running on the same core. If you have two threads that share a lock and wait on it, one thread can starve another thread if the time from unlocking to locking is virtually non-existant. I had a worker thread lock on work, unlock on done, and loop. The command thread couldn't get a hold of the lock because the worker kept stealing the lock. I believe setting thread affinity would have fixed this. \$\endgroup\$ – leetNightshade Feb 2 '17 at 23:06
4
\$\begingroup\$

I agree Le Comte du Merde-fou's answer is the best general rule here:

I wouldn't bother unless you have a very specific issue which you know is solved by this.

For the curious though, I'd like to posit a couple examples of the types of "very specific issue" which might make this worthwhile. The ones I know of mainly come up when developing for consoles - where the hardware is fixed and you can optimize to its particular behaviour, rather than needing to scale to arbitrary core configurations & execution environments.

  1. When running on some consoles, certain cores may be earmarked for OS tasks or occasional interruptions. I won't get into details that might tread on NDAs, but on a general level you may want to use these cores for work that's tolerant to throttling (asynchronous background tasks, or workers in a job system that can scale to use the core when available, without starving when it's not), and keep tasks on your critical path for the frame on cores where you know you have exclusive control.

  2. When doing (very) large amounts of batch work with predictable data locality, paying attention to which threads use which data and which cache that data will be in can sometimes let you reduce redundant fetching & synchronization work between cores' private caches. Shared cache mechanisms are already lightning-fast compared to a trip to main memory though, so this is almost certainly micro-optimization unless you've already fine-tuned just about everything else in your engine.

\$\endgroup\$
  • 1
    \$\begingroup\$ +1, it's nice to see some actual examples (I was thinking more abstractly, like maybe you know that the overhead of moving a thread to another core is such-and-such compared to the work it does, etc) and it highlights the danger that you need to know exactly what you're doing, including hardware specifics of your target environment, if you try this kind of optimization. \$\endgroup\$ – Maximus Minimus Jan 28 '17 at 8:39

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.