diff --git a/docs/object/objects/index.html b/docs/object/objects/index.html index 48542e6..a1f1d48 100644 --- a/docs/object/objects/index.html +++ b/docs/object/objects/index.html @@ -22,14 +22,17 @@ }); // Print the monsters' health - const new_monster_id = app.monsters.get(new_monster_id); + const new_monster = app.monsters.get(new_monster_id); std.debug.print("monster health: {}\n", .{new_monster.health}); // Give the monster 2x damage! new_monster.damage *= 2; app.monsters.set(new_monster_id, new_monster); // save the change } -

Performance & philosophy

The first thing we should talk about is performance and memory optimization. mach.Objects isn’t just a dynamic array of structs - it actually stores all of the fields of the object independently (struct-of-arrays, same as a std.MultiArrayList(T)), so internally it’s as if there were a list of []f32 for the health of all objects, and another list of []f32 for the damage of all objects.

This design decision helps reduce the memory overhead of storing many objects, by eliminating padding between struct fields - which can greatly improve CPU cache efficiency and overall application performance. For more details you can watch Andrew Kelley’s Practical DOD talk which teaches practical ways to apply data-oriented design.

A core design decision of Mach’s object system is to encourage you to write code that operates on many objects at once. For example, instead of writing a function that manipulates a single object (like how a Java programmer might), we instead prefer to store all our objects in a big array, and write a function that operates over all of them all at once. This can massively improve performance by improving the odds that objects are in CPU L1/L2/L3 caches, reducing function call overhead, and more.

System functions in Mach are restricted from having arbitrary arguments in part to encourage you to write functions that operate on many objects at once, and to write modules that communicate through objects. Rather than calling another module’s functions to cause an effect, you should assume the module’s functions will run in the future - and you just need to create/modify/update an object to create the desired effect.

Object IDs

The first thing you might notice about the code snippet above is that when you create a new object, you get an object ID back:

const new_monster_id = app.monsters.get(new_monster_id);
+

Performance & philosophy

The first thing we should talk about is performance and memory optimization. mach.Objects isn’t just a dynamic array of structs - it actually stores all of the fields of the object independently (struct-of-arrays, same as a std.MultiArrayList(T)), so internally it’s as if there were a list of []f32 for the health of all objects, and another list of []f32 for the damage of all objects.

This design decision helps reduce the memory overhead of storing many objects, by eliminating padding between struct fields - which can greatly improve CPU cache efficiency and overall application performance. For more details you can watch Andrew Kelley’s Practical DOD talk which teaches practical ways to apply data-oriented design.

A core design decision of Mach’s object system is to encourage you to write code that operates on many objects at once. For example, instead of writing a function that manipulates a single object (like how a Java programmer might), we instead prefer to store all our objects in a big array, and write a function that operates over all of them all at once. This can massively improve performance by improving the odds that objects are in CPU L1/L2/L3 caches, reducing function call overhead, and more.

System functions in Mach are restricted from having arbitrary arguments in part to encourage you to write functions that operate on many objects at once, and to write modules that communicate through objects. Rather than calling another module’s functions to cause an effect, you should assume the module’s functions will run in the future - and you just need to create/modify/update an object to create the desired effect.

Object IDs

The first thing you might notice about the code snippet above is that when you create a new object, you get an object ID back:

    const new_monster_id: mach.ObjectID = app.monsters.new(.{
+        .health = 100,
+        .damage = 10,
+    });
 

Object IDs are just stable integer identifiers, containing a ton of information in them:

Memory allocation

Internally, a mach.Objects() list maintains a recycling bin of objects: when a .new() object is requested, it looks in the recycling bin to see if we have an index in the array which was a previously .delete()ed object. This allows for rapidly creating/destroying massive quantities of objects with very little overhead.

Additionally, since Mach has insights into the object lists it has the opportunity to analyze the required memory allocation as you e.g. play through your game, save that information to disk and compile it into future builds of the game - to allocate just the right amount in the future ahead of time for even fewer runtime memory allocations and better performance.

Synchronization and multi-threading

You may have noticed that we have this code around our usage of the monsters list:

    app.monsters.lock();
     defer app.monsters.unlock();
 

All mach.Objects have a read-write lock protecting them.

This enables multiple threads, each running independent Mach module system functions, to coordinate with one-another without data race conditions. For example, you may have some game logic which works on monsters, while having a background thread handling network packets to keep monsters across the network synchronized, while also having a debug editor for your game allowing you to edit monsters’ values at any time - all being synchronized by this read-write lock.

Importantly, since all mach.Objects by convention have a read-write lock which should be held when working with them, it is possible to work with arbitrary objects safely without knowing what they are or under what contract they can be manipulated. For example, a GUI editor can read or write arbitrary objects’ values safely without knowing anything about that type of object - all at runtime.

Performance note: Since it is a read-write lock, multiple threads can read at once. Mutexes are cheap as long as there is no thread contention, and mach.Objects lists are designed to be generally large in size and performant in other ways - so this works out and keeps sometimes quite complex multi-threaded code simple to reason about.

Iterating objects

A very common thing to do is iterate all objects, which you can do like so:

pub fn tick(app: *App) void {