DataOriented Software Design 101 How conventional objectoriented techniques
Data-Oriented Software Design 101 How conventional object-oriented techniques kill performance and what you can do about it. by Christopher Myburgh
Innocent looking object-oriented code namespace Object. Oriented. Physics. Engine { Body is a reference type. Body class Body objects could be scattered all over { heap memory. public Vector 3 Position; public Vector 3 Velocity; // members for other data: mass, net force, damping, angular motion etc. } public class World { private List<Body> _bodies; public void Step(float dt) { //. . . foreach (Body body in _bodies) { body. Position += body. Velocity * dt; } //. . . } } } Every iteration of the loop can incur a cache-miss.
What's a cache-miss? Cache is small, fast memory located on the CPU. Programmers never work with cache directly, but data must be present in cache before the CPU can process it. A “cache-miss” is when the data required for an operation is not in cache and must first be retrieved from main memory. Data is copied from main memory to cache in chunks. So when a cache-miss occurs, memory adjacent to the desired data is copied as well. Frequent cache-misses are bad because main memory is much slower than the CPU in most computer systems. So when data needs to be copied to cache, the CPU must idle, wasting cycles until the data arrives from main memory.
Improvement attempt no. 1 namespace Object. Oriented. Physics. Engine 2 { struct Body is now a value { public Vector 3 Position; public Vector 3 Velocity; // members for other data: mass, net force, damping, angular motion etc. } public class World { private Body[] _bodies; private int _body. Count; public void Step(float dt) { //. . . type. Bodies are now allocated together in one contiguous block of memory, so many bodies will be read into cache at a time. for (int i = 0; i < _body. Count; ++i) { _bodies[i]. Position += _bodies[i]. Velocity * dt; } //. . . } } } Cache-misses are reduced, but cache memory is still being wasted with data not relevant to the operation.
Data-oriented programming to the rescue namespace Data. Oriented. Physics. Engine { public class World { private Vector 3[] _body. Positions; private Vector 3[] _body. Velocities; Flatten the Body type into arrays for each member. // arrays for other body data: mass, net force, damping, angular motion etc. private int _body. Count; public void Step(float dt) { //. . . for (int i = 0; i < _body. Count; ++i) { _body. Positions[i] += _body. Velocities[i] * dt; } //. . . } } } Minimal cache-misses! Only the data relevant to the operation is now read into cache, and it's all in contiguous memory!
Another example: scene graph namespace Object. Oriented. Scene. Graph { class Scene. Node { public Scene. Node Parent; public List<Scene. Node> Children; public Matrix Local. Transform; public Matrix World. Transform; } Multiple heap allocations per scene node! public class Scene { private Scene. Node _root. Node; private void Update. Child. Transforms(Scene. Node node) { foreach (Scene. Node child. Node in node. Children) { child. Node. World. Transform = child. Node. Local. Transform * node. World. Transform; Update. Child. Transforms(child. Node); } } public void Draw() { // update world transforms _root. Node. World. Transform = _root. Node. Local. Transform; Update. Child. Transforms(_root. Node); //. . . } } } Recursive updates require jumping all over heap memory! Cache-misses galore!
The data-oriented take namespace Data. Oriented. Scene. Graph { public class Scene { private int[][] _parent. Node. Indices; private Matrix[][] _local. Transforms; private Matrix[][] _world. Transforms; private int[] _scene. Node. Counts; private int _graph. Height; An array for each level of the graph, sorted by parent node index. public void Draw() { // update world transforms _world. Transforms[0][0] = _local. Transforms[0][0]; for (int i = 1; i < _graph. Height; ++i) { for (int j = 0; j < _scene. Node. Counts[i]; ++j) { int parent. Node. Index = _parent. Node. Indices[i][j]; _world. Transforms[i][j] = _local. Transforms[i][j] * _world. Transforms[i - 1][parent. Node. Index]; } } //. . . } } } No more recursion. The graph is updated one level at a time, processing data in the same order as it is laid out in memory. Super cache-friendly win!
Cons of data-oriented design A system's public interface can become more restrictive and less elegant for clients to use. The vast majority of a system's data and logic tends to end up in a single, massive class. Inserting and removing data from the system can become far more complex, and thus prone to bugs that can corrupt the state of the entire system.
- Slides: 8