The Job union in Lix

By Simon N.

This is an optimization for better memory locality without sacrificing the good object-oriented features of the D Language.

I gave a 5-minute talk on this at DConf 2018 in Munich.

The original situation

Each Lix is a class object and has a Job class object that tells the Lix how to behave and maybe stores some Job-specific data.

This diagram captures the design; it is the object-oriented state pattern. A Lix can change her Job dynamically at runtime.

Networking games play in real time. When a player assigns a skill to one of their lix, that action is transferred over the network. Other players must go back in time, fit this action into their replay, a.k.a. game journal, and recompute physics for this new situation.

To guarantee easy recomputation behind the scenes, the game will create savestates by deep-copying the entire GameState. That means two calls of new: One for each Lix and one for the Lix's current Job.

A networking game with 8 players and more than 100 lixes per player can easily have 1,000 Lix per GameState, and each Lix has her own Job. When many skill assignments from other players arrive over the network, we will have to savestate 1 to maybe even 5 times per second. This makes 2,000 to 10,000 allocations with new.

Problem statement

Calling new to allocate 2,000 to 10,000 small class objects per second isn't optimal. I would like more efficient copying of savestates.

On the other hand, I would like to preserve the Job class hierarchy. I like D's object-oriented features; the D compiler should enforce that all subclasses implement the required abstract methods.

The Job subclasses Batter, Builder, Floater, etc., are of many different sizes. Some subclasses have more extra fields than others. These extra fields must be preserved during savestating.

My solution

Only class Lix changes, along with any references to Lix outside of the shown system.

Effective changes:

Extra safeguards:

The instances of struct Lix still sit in a garbage-collected dynamic array. Now, a Lix with its emplaced Job acts as a value type. For savestating, the dynamic array of Lixes can now be deep-copied like any other array of value types -- by lixes.dup. Deep copy and shallow copy have become the same.

Neither change affects the definition of Job or its subclasses; this is in accordance with the desire to keep this polymorphic hierarchy a D class with statically typechecked virtual method dispatch.

These changes brought a speed increase by 1.5 % during regular physics computations, presumably by better locality, even without safestating.

In networking games, when the GameState has to be deep-copied often as a savestate, the benefit is higher than 1.5 %, but this is very hard to measure and depends on how frantic any given networking game will play out.

How unsafe?

My hack cannot be 100 % safe, but its safety violations are well-limited:

Two different Lix will never share memory and are safely allocated in GameState's garbage-collected dynamic array.

Within the same Lix, different Jobs will share memory when one Job is replaced by another during execution of the old Job's perform(). After Batter, Builder, Floater, et al. tell the Lix to switch Jobs, it is the responsibility of the old Job subclass to ensure no further reads/writes of their own fields -- these fields will already have been overwritten in the jobUnion by the new Job.

None of the extra fields Batter, Builder, Floater etc. contain any pointers or references. My hack thus ensures no wild pointers.

Job itself has a class reference to the Lix that contains this Job. This reference to Lix is important because the Job steers the Lix and can tell her to get a new Job. But this reference is never changed when the Lix changes its Job, therefore this does not introduce unsafe memory access either.

In summary: The only damage can be reads or writes of non-pointer values from a different Job subclass. Such erroneous accesses may introduce physics bugs at best. They cannot crash the application or access memory unsafely outside of the jobUnion. I deem this acceptable in a video game.

-- Simon