Saturday, August 13, 2005

On fear of change in game development: my modest proposal

Gabe Newell of Valve recently gave a really interesting interview, available here: http://www.next-gen.biz/index.php?option=com_content&task=view&id=510&Itemid=2




The funny thing is, I disagree with him completely. His basic premise is that multi-core systems mean that game developers need to throw away their existing code base, and many of the threading issues in game development are unsolved or only exist in Doctoral theses at present. Finally, he states that "Really good engineers are going to be much more valuable and engineers who used to be valuable writing game code in the previous generation may end up becoming thorns in the side of key programmers who can write multi-core game code."




First of all, my comment on the last sentiment: GOOD! It really is about time that good software engineering came to game development, rather than the backwards hackery seen so often. I'll expand on this throughout my discourse.




There are several fundamental problems with game development at present. The first is gradually being solved: a bad mixture of Not Invented Here syndrome, and wheel reinvention. Far too many games bake their own engine from scratch (often focussing on graphics to the exclusion of all else), when all they need to do is implement a similar rendering pipeline to other x generation setups. Innovation is needed, but for developers who aren't focussing on a new engine, this is wasted effort. Worse, many, many games are still written in C, with a gradual move to C++. Until recently, it was still common to see arguments on Flipcode and Gamedev against OOP design and established design patterns on the ground that vtable lookups are slow (they are really fast on modern compilers). Java, C#, and other modern languages are relegated to the status of laughing-stock because they abstract the hardware a step further away from the programmer with a VM (virtual machine). This is the first reason why moving to multi-threaded programming is difficult for many game developers: they are using ancient paradigms, while the business and scientific community has long since moved on.




The second fundamental problem of game development is an obsession with close-to-the-metal code, even when it can actually hurt your system! I've seen many cases in which assembly language is used, even though the assembly produced is actually slower than CPU-targeted compiled code. In a world of out-of-order execution, smart compilers, and different optimizations working better on different architectures (such as branch misprediction killing performance on the P4, but not the Athlon), compiled code � particularly high-level compiled code with high-level optimizations � is frequently more efficient than hand-tuned assembly. There are exceptions, but these tend to be special cases such as vectorization � and the compilers are catching up. Most fundamentally, micro-optimization abounds when the correct solution is to pick a sound algorithm in the first place.




The third fundamental problem in modern game development is an insistence on linear execution. Almost every piece of game code I've seen assumes a single message loop (the badly written ones eat 100% CPU when idle by polling for messages in a busy loop!), and tacks game logic in a neatly ordered loop. If you do this for server code, you may as well give up on ever serving more than a handful of clients at once � as server designers have known for 20 years or more. Games are no different. Even on a single CPU, this gives disadvantages: the scheduler cannot pass messages to your program while it performs actions prior to polling the message queue (meaning your program can appear to lock up hard, taking the system with it if you didn't clean up exclusive DirectX locks), IO requires a hard pre-empt from the kernel (meaning an unpredictable context shift � possibly in the middle of a frame calculation - if you fill the disk cache), and any other programs the user has running have a difficult time remaining usable. For hyperthreaded systems, it is likely that the second execution thread will actually slow such setups down; the game is probably using most of the execution units, and doesn't play nice with addition processes that desire some CPU time.




In other words, game development is (outside of graphics research) stuck in the early 1990s. Multi-threading is not difficult; if it were, I wouldn't use it in every Windows application I create, nor for simple tasks like separating data collection from display in a traffic monitor under FreeBSD. The key is a very strong design, isolating thread contention for resources as much as possible. There are many, many, books on this topic. Admittedly, most don't talk about games directly, but it doesn't require a Ph.D. to extrapolate basic computer science principles! Hopefully, the move towards hiring �really good engineers� will mean that code design will catch up in game development! It isn't hard to lock shared structures (via critical sections, spinlocks, etc.), it just requires that you think about access patterns.




The funny thing is, multi-threading is a shoe-in for most games. Physics should run independently of rendering � and I've seen many timer hacks to achieve this. Why not just put the physics engine in it's own thread, and render snapshots of the world? (Some physics problems are parallel in nature, and could even use multiple threads themselves!) Likewise, user interaction is a naturally threaded problem. Why should you only care about the keyboard (or other input device) status at a certain point in the loop (and revert to using DirectX's buffered input mode if you are concerned about missing events)? You might get a considerably more responsive game by checking IO in a timed thread, and feeding instructions to the phsyics engine. It might even be worth switching to an event-based IO system, in which the main process passes events to relevant threads; it works for every other interactive system out there! Even Ultima Online recognized that mouse input belonged in a thread. Likewise, sound really belonds in a thread with a priority that requests activity frequently enough to ensure that sound doesn't skip even if something else has slowed down (I believe some iterations of the Quake engine separated sound for this reason). Graphical rendering is often linear in nature, but could also be its own thread, so as to separate it from the main execution thread (ensuring that a bug will not freeze the main application thread).


How does one separate all of this game data to avoid contention/locks? Have each thread work with local data, and place data ready to be rendered (essentially a snapshot, although for IO it could be a queue) into a shared area when ready. Yes, you will need to lock this, but a copy should be a fast operation relative to the actual calculations � and you can start work on the next frame while the renderer lets the user know what's going on.


In a single-core system, this may not offer much advantage � but is unlikely to seriously slow you down. Modern schedulers are very good, and give the illusion of simultaneous execution rather well; if your graphics thread is waiting on a vertex buffer upload (handled by the GPU over AGP/PCIx), why not let the scheduler keep another part of your game running? On future multi-core systems, it is very likely that you can significantly improve the state of AI, physics, and general responsiveness with this strategy. Tim Sweeney of Epic has indicated that the Unreal engine will benefit greatly from multiple cores fo this very reason � there is no reason for other game developers to fear them.




Finally, the great language debate. Recent benchmarks show that languages contained in virtual machines (VMs) are very nearly as fast as C++, sometimes faster depending upon just-in-time (JIT) optimizations. Garbage collection, while occasionally slow (usually slowness in GC is an indication that you've done something wrong) makes cleanup very easy. Study after study shows that for most logic, higher-level languages make programmers considerably more efficient; no more re-inventing basic structures, for one thing. More importantly, modern languages are built for the multi-threaded world. Managing Windows threads in C++ with the Windows API is painful (but quite manageable). Managing them with the C# threading library is very, very easy. The same could be said of pthreads in C versus Java threads on a *nix box (although pthreads is pretty straightforward). I stopped using C++, except for assemblies that really need features that aren't available in C#, a few years ago. Some game developers have done the same, but they are a tiny minority. The move towards multi-core systems can only encourage the adoption of modern languages.




Footnote: C# and Java are both far behind academic languages such as Haskell in terms of innovation, and haven't even caught up with LISP and Smalltalk in terms of features. I'm not proposing a widespread move to the bleeding edge, just a step forwards to where the business world has been for a few years now.


Mood: relaxed
Music: None

No comments: