Skip to content

Commit

Permalink
deploy: 0aab001
Browse files Browse the repository at this point in the history
  • Loading branch information
slimsag committed Nov 25, 2024
1 parent 8752e2e commit cbb72ec
Showing 1 changed file with 5 additions and 2 deletions.
7 changes: 5 additions & 2 deletions docs/object/objects/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,17 @@
</span></span><span style=display:flex><span> });
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span> <span style=color:#75715e>// Print the monsters&#39; health
</span></span></span><span style=display:flex><span><span style=color:#75715e></span> <span style=color:#66d9ef>const</span> new_monster_id <span style=color:#f92672>=</span> app.monsters.get(new_monster_id);
</span></span></span><span style=display:flex><span><span style=color:#75715e></span> <span style=color:#66d9ef>const</span> new_monster <span style=color:#f92672>=</span> app.monsters.get(new_monster_id);
</span></span><span style=display:flex><span> std.debug.print(<span style=color:#e6db74>&#34;monster health: {}</span><span style=color:#ae81ff>\n</span><span style=color:#e6db74>&#34;</span>, .{new_monster.health});
</span></span><span style=display:flex><span>
</span></span><span style=display:flex><span> <span style=color:#75715e>// Give the monster 2x damage!
</span></span></span><span style=display:flex><span><span style=color:#75715e></span> new_monster.damage <span style=color:#f92672>*=</span> <span style=color:#ae81ff>2</span>;
</span></span><span style=display:flex><span> app.monsters.set(new_monster_id, new_monster); <span style=color:#75715e>// save the change
</span></span></span><span style=display:flex><span><span style=color:#75715e></span>}
</span></span></code></pre></div><h2 id=performance--philosophy>Performance & philosophy</h2><p>The first thing we should talk about is performance and memory optimization. <code>mach.Objects</code> isn&rsquo;t just a dynamic array of structs - it actually stores all of the fields of the object independently (struct-of-arrays, same as a <code>std.MultiArrayList(T)</code>), so internally it&rsquo;s <em>as if</em> there were a list of <code>[]f32</code> for the <code>health</code> of all objects, and another list of <code>[]f32</code> for the <code>damage</code> of all objects.</p><p>This design decision helps reduce the memory overhead of storing many objects, by eliminating <em>padding</em> between struct fields - which can <em>greatly</em> improve CPU cache efficiency and overall application performance. For more details you can watch <a href=https://vimeo.com/649009599>Andrew Kelley&rsquo;s Practical DOD talk</a> which teaches practical ways to apply <em>data-oriented design</em>.</p><p>A core design decision of Mach&rsquo;s object system is to encourage you to <em>write code that operates on many objects at once</em>. 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 <em>massively</em> improve performance by improving the odds that objects are in CPU L1/L2/L3 caches, reducing function call overhead, and more.</p><p><strong>System functions in Mach are restricted from having arbitrary arguments</strong> in part to encourage you to write functions that operate on <em>many objects at once</em>, and to write modules that <em>communicate through objects</em>. Rather than calling another module&rsquo;s functions to cause an effect, you should assume the module&rsquo;s functions will run in the future - and you just need to create/modify/update an object to create the desired effect.</p><h2 id=object-ids>Object IDs</h2><p>The first thing you might notice about the code snippet above is that when you create a new object, you get an <em>object ID</em> back:</p><div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-zig data-lang=zig><span style=display:flex><span><span style=color:#66d9ef>const</span> new_monster_id <span style=color:#f92672>=</span> app.monsters.get(new_monster_id);
</span></span></code></pre></div><h2 id=performance--philosophy>Performance & philosophy</h2><p>The first thing we should talk about is performance and memory optimization. <code>mach.Objects</code> isn&rsquo;t just a dynamic array of structs - it actually stores all of the fields of the object independently (struct-of-arrays, same as a <code>std.MultiArrayList(T)</code>), so internally it&rsquo;s <em>as if</em> there were a list of <code>[]f32</code> for the <code>health</code> of all objects, and another list of <code>[]f32</code> for the <code>damage</code> of all objects.</p><p>This design decision helps reduce the memory overhead of storing many objects, by eliminating <em>padding</em> between struct fields - which can <em>greatly</em> improve CPU cache efficiency and overall application performance. For more details you can watch <a href=https://vimeo.com/649009599>Andrew Kelley&rsquo;s Practical DOD talk</a> which teaches practical ways to apply <em>data-oriented design</em>.</p><p>A core design decision of Mach&rsquo;s object system is to encourage you to <em>write code that operates on many objects at once</em>. 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 <em>massively</em> improve performance by improving the odds that objects are in CPU L1/L2/L3 caches, reducing function call overhead, and more.</p><p><strong>System functions in Mach are restricted from having arbitrary arguments</strong> in part to encourage you to write functions that operate on <em>many objects at once</em>, and to write modules that <em>communicate through objects</em>. Rather than calling another module&rsquo;s functions to cause an effect, you should assume the module&rsquo;s functions will run in the future - and you just need to create/modify/update an object to create the desired effect.</p><h2 id=object-ids>Object IDs</h2><p>The first thing you might notice about the code snippet above is that when you create a new object, you get an <em>object ID</em> back:</p><div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-zig data-lang=zig><span style=display:flex><span> <span style=color:#66d9ef>const</span> new_monster_id<span style=color:#f92672>:</span> mach.ObjectID <span style=color:#f92672>=</span> app.monsters.new(.{
</span></span><span style=display:flex><span> .health <span style=color:#f92672>=</span> <span style=color:#ae81ff>100</span>,
</span></span><span style=display:flex><span> .damage <span style=color:#f92672>=</span> <span style=color:#ae81ff>10</span>,
</span></span><span style=display:flex><span> });
</span></span></code></pre></div><p>Object IDs are just stable integer identifiers, containing <em>a ton of information</em> in them:</p><ul><li>An array index that can be used to O(1) lookup the actual data / struct fields of the object.</li><li>The generation (or &lsquo;version&rsquo;) of the object, enabling detecting use-after-object-delete in many (but not all) cases.</li><li>Which module the object came from, allowing looking up type information about the object - or which module it came from - just from its ID.</li><li>Which exact list of objects in a module the object came from, allowing looking up detailed type information or the object&rsquo;s name - which enables debugging and type safety when passing opaque IDs around.</li></ul><h2 id=memory-allocation>Memory allocation</h2><p>Internally, a <code>mach.Objects()</code> list maintains <em>a recycling bin</em> of objects: when a <code>.new()</code> object is requested, it looks in the recycling bin to see if we have an index in the array which was a previously <code>.delete()</code>ed object. This allows for rapidly creating/destroying massive quantities of objects with very little overhead.</p><p>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.</p><h2 id=synchronization-and-multi-threading>Synchronization and multi-threading</h2><p>You may have noticed that we have this code around our usage of the <code>monsters</code> list:</p><div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-zig data-lang=zig><span style=display:flex><span> app.monsters.lock();
</span></span><span style=display:flex><span> <span style=color:#66d9ef>defer</span> app.monsters.unlock();
</span></span></code></pre></div><p>All <code>mach.Objects</code> have a read-write lock protecting them.</p><p>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&rsquo; values at any time - all being synchronized by this read-write lock.</p><p>Importantly, since all <code>mach.Objects</code> by convention have a read-write lock which should be held when working with them, it is possible to work with <em>arbitrary objects</em> 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&rsquo; values safely without knowing anything about that type of object - all at runtime.</p><p><strong>Performance note:</strong> 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 <code>mach.Objects</code> 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.</p><h2 id=iterating-objects>Iterating objects</h2><p>A very common thing to do is iterate all objects, which you can do like so:</p><div class=highlight><pre tabindex=0 style=color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4><code class=language-zig data-lang=zig><span style=display:flex><span><span style=color:#66d9ef>pub</span> <span style=color:#66d9ef>fn</span> tick(app<span style=color:#f92672>:</span> <span style=color:#f92672>*</span>App) <span style=color:#66d9ef>void</span> {
Expand Down

0 comments on commit cbb72ec

Please sign in to comment.