<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jason Xu</title>
    <description>The latest articles on DEV Community by Jason Xu (@jasonxudeveloper).</description>
    <link>https://dev.to/jasonxudeveloper</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4196356%2Fe1ea7fe7-5963-40be-abb3-6eb53046e76e.png</url>
      <title>DEV Community: Jason Xu</title>
      <link>https://dev.to/jasonxudeveloper</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jasonxudeveloper"/>
    <language>en</language>
    <item>
      <title>How EasyEcs runs a 100k-entity server tick in 1.72 ms with 0 B allocated</title>
      <dc:creator>Jason Xu</dc:creator>
      <pubDate>Tue, 20 Oct 2026 15:23:55 +0000</pubDate>
      <link>https://dev.to/jasonxudeveloper/how-easyecs-runs-a-100k-entity-server-tick-in-172-ms-with-0-b-allocated-56j5</link>
      <guid>https://dev.to/jasonxudeveloper/how-easyecs-runs-a-100k-entity-server-tick-in-172-ms-with-0-b-allocated-56j5</guid>
      <description>&lt;p&gt;Moving 100,000 positions is an easy way to benchmark an ECS. A real server tick also applies status effects, fires projectiles, handles deaths and respawns, and updates each player's area of interest. I built &lt;a href="https://github.com/JasonXuDeveloper/EasyEcs" rel="noopener noreferrer"&gt;EasyEcs&lt;/a&gt; for that whole workload in C# and .NET 10.&lt;/p&gt;

&lt;p&gt;On the project's 4-core 2.1 GHz Xeon VM, a warmed-up world of &lt;strong&gt;100,000 entities runs nine simulation systems in 1.72 ms and allocates 0 B per tick&lt;/strong&gt;. The systems do real work: setup checks that hits, deaths, spawns and grid changes happen. The &lt;a href="https://github.com/JasonXuDeveloper/EasyEcs/blob/master/benchmarks/EasyEcs.Benchmark/WorldSimulationBenchmark.cs" rel="noopener noreferrer"&gt;benchmark source&lt;/a&gt; times &lt;code&gt;world.Update()&lt;/code&gt; after warm-up.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Native .NET workload&lt;/th&gt;
&lt;th&gt;Measured time&lt;/th&gt;
&lt;th&gt;Allocation after warm-up&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;100k entities in one world, nine-system simulation tick&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.72 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0 B&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Four independent worlds × 25k, one long-lived thread each&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.92 ms total wall time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0 B&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a href="https://ecs.xgamedev.net/en/docs/example" rel="noopener noreferrer"&gt;100k-entity battle-server example&lt;/a&gt;, with replication, indexes, relations and saves&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.17 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0 B&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These runs do different amounts of work. Four worlds can represent separate matches; &lt;strong&gt;0.92 ms is the wall time until all four finish&lt;/strong&gt;, including the benchmark's thread barriers. It does not describe one 100k-entity world. The results use BenchmarkDotNet 0.15.2 on Ubuntu 24.04 and .NET 10.0.13; they do not include network transport. The &lt;strong&gt;1.68 ms&lt;/strong&gt; cross-library tick mentioned below comes from a separate suite, not this 1.72 ms run. The &lt;a href="https://ecs.xgamedev.net/en/docs/performance" rel="noopener noreferrer"&gt;performance guide&lt;/a&gt; gives the full setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operations beyond the main loop
&lt;/h2&gt;

&lt;p&gt;A tick is only part of a server's cost. These are head-to-head results for common operations on the &lt;strong&gt;same VM&lt;/strong&gt;. “Next fastest” is whichever of Friflo.Engine.ECS 3.6, Arch 2.1 and DefaultEcs 0.17 was fastest in that case. Each library uses its idiomatic API; setup checks that the resulting state is equivalent.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What the application does&lt;/th&gt;
&lt;th&gt;EasyEcs&lt;/th&gt;
&lt;th&gt;Next fastest&lt;/th&gt;
&lt;th&gt;Gap&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Save and load 10k entities&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;146 μs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;DefaultEcs 4.33 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;30×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add then remove a status across 100k entities by query&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;78.7 μs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Arch 1.21 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;15×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spawn then destroy 10k entities beside a 100k world&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;138 μs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Friflo 380 μs&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.75×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Query 100k players by rating range and zone sums&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;127 μs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Friflo 317 μs&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.49×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add then remove a status on 5% of 100k entities&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;517 μs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Friflo 1.00 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.94×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Find the 10 of 100k entities changed this tick&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;507 ns&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Arch 966 ns&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.91×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Iterate one component over 100k entities&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;37.7 μs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Friflo 59.9 μs&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.59×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sleep and wake 10k of 100k entities&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;478 μs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;DefaultEcs 745 μs&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.56×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The save row writes and reloads 10,000 units with position and stock; the libraries use different serialization formats. The 30× gap measures that complete operation through each library's normal path, not identical encoders. More broadly, these ratios describe &lt;strong&gt;individual operations&lt;/strong&gt;, not a 30× faster game.&lt;/p&gt;

&lt;p&gt;On the full cross-library 100k tick, EasyEcs is &lt;strong&gt;1.68 ms&lt;/strong&gt; and Friflo &lt;strong&gt;1.83 ms&lt;/strong&gt;, both 0 B. Across all 37 tests EasyEcs leads in 24, is within measurement error in 11, and trails in 2. The clear losses are a scan that ignores EasyEcs's change filter and cold per-entity creation without reserved capacity. The &lt;a href="https://ecs.xgamedev.net/en/docs/comparison#feature-by-feature" rel="noopener noreferrer"&gt;comparison page&lt;/a&gt; shows those cases and the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the code looks like
&lt;/h2&gt;

&lt;p&gt;An EasyEcs component is a struct. A system declares the data it reads (&lt;code&gt;in&lt;/code&gt;) and writes (&lt;code&gt;ref&lt;/code&gt;); the source generator builds the iteration job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ForEach&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Move&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="n"&gt;Position&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;Velocity&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;Clock&lt;/span&gt; &lt;span class="n"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Y&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Y&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://ecs.xgamedev.net/en/docs/demo" rel="noopener noreferrer"&gt;live browser demo&lt;/a&gt; takes contiguous spans from the same API and processes two particles per &lt;code&gt;Vector128&amp;lt;float&amp;gt;&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ForEach&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Move&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Span&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Position&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ReadOnlySpan&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Velocity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;velocities&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;Clock&lt;/span&gt; &lt;span class="n"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MemoryMarshal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cast&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Position&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Vector128&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MemoryMarshal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cast&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Velocity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Vector128&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;velocities&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Vector128&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt;
        &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// The complete demo handles an odd last particle too.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two demo jobs are &lt;a href="https://github.com/JasonXuDeveloper/EasyEcs/tree/master/demo/EasyEcs.Demo/Systems" rel="noopener noreferrer"&gt;real source files&lt;/a&gt;, not the nine-system server benchmark. The browser's roughly 0.1 ms motion reading is for &lt;strong&gt;drag plus movement&lt;/strong&gt; on an M4 through Mono AOT → WebAssembly; it must not be compared directly with the Xeon/CoreCLR server tick.&lt;/p&gt;

&lt;p&gt;In a separate &lt;strong&gt;one-archetype microbenchmark&lt;/strong&gt; on the Xeon VM, the generated &lt;code&gt;[ForEach]&lt;/code&gt; job updates 10,000 positions in &lt;strong&gt;9.1 μs&lt;/strong&gt;, or &lt;strong&gt;0.91 ns per entity&lt;/strong&gt;. A loop over separate &lt;code&gt;Position[]&lt;/code&gt; and &lt;code&gt;Velocity[]&lt;/code&gt; arrays takes 12.8 μs in that particular test. The &lt;a href="https://github.com/JasonXuDeveloper/EasyEcs/blob/master/benchmarks/EasyEcs.Benchmark/MicroBenchmarks.cs" rel="noopener noreferrer"&gt;microbenchmark source&lt;/a&gt; shows both loops. This is a narrow measurement, not a substitute for the full tick.&lt;/p&gt;

&lt;p&gt;A round-wide stun and its removal take two calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;QueryFilter&lt;/span&gt; &lt;span class="n"&gt;Fighters&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;QueryFilter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;With&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Vitals&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Stunned&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;Fighters&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RemoveAll&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Stunned&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the first round, EasyEcs can keep the status column and flip presence bits instead of moving 100,000 rows again. That is why the &lt;strong&gt;78.7 μs&lt;/strong&gt; result is a warmed-up repeated change, not the cost of creating a new archetype from scratch. The &lt;a href="https://ecs.xgamedev.net/en/docs/guide/bulk" rel="noopener noreferrer"&gt;bulk guide&lt;/a&gt; covers the semantics and the cases where per-entity updates are the right call.&lt;/p&gt;

&lt;h2&gt;
  
  
  What else is in the library?
&lt;/h2&gt;

&lt;p&gt;The rest of a server benefits from the same data model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;[Changed&amp;lt;T&amp;gt;]&lt;/code&gt; and &lt;code&gt;[Removed&amp;lt;T&amp;gt;]&lt;/code&gt; identify what to replicate.&lt;/li&gt;
&lt;li&gt;Value indexes and relations answer lookups without scanning every entity.&lt;/li&gt;
&lt;li&gt;Prefabs and bulk spawning create waves; snapshots and saves support rollback and recovery.&lt;/li&gt;
&lt;li&gt;Parallel systems run together only when their generated read/write sets do not conflict.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The package also includes a Roslyn analyzer with &lt;strong&gt;23 core diagnostics (EECS002–024)&lt;/strong&gt; and &lt;strong&gt;10 performance diagnostics (EECS025–034)&lt;/strong&gt;; some have code fixes. Native AOT is supported. The &lt;a href="https://ecs.xgamedev.net/en/docs/features" rel="noopener noreferrer"&gt;feature guide&lt;/a&gt; and &lt;a href="https://ecs.xgamedev.net/en/docs/guide/analyzer" rel="noopener noreferrer"&gt;analyzer guide&lt;/a&gt; go into the APIs and their costs.&lt;/p&gt;

&lt;p&gt;Zero allocation has a boundary: a new world, first query shape, first archetype or growth past reserved capacity can allocate. The &lt;a href="https://ecs.xgamedev.net/en/docs/faq#allocations" rel="noopener noreferrer"&gt;FAQ&lt;/a&gt; lists those cases. EasyEcs is an ECS library for &lt;strong&gt;.NET 10 game servers, simulations and other long-running real-time backends&lt;/strong&gt;; it does not provide network transport and does not support Unity's current runtimes.&lt;/p&gt;

&lt;p&gt;If this matches your workload, install it with &lt;code&gt;dotnet add package EasyEcs&lt;/code&gt;, follow the &lt;a href="https://ecs.xgamedev.net/en/docs/getting-started" rel="noopener noreferrer"&gt;getting-started guide&lt;/a&gt;, or run the &lt;a href="https://github.com/JasonXuDeveloper/EasyEcs/tree/master/samples/GameLoop" rel="noopener noreferrer"&gt;30 Hz GameLoop sample&lt;/a&gt;. The comparison and benchmark projects are public. Run a case from your own application, and open an issue if EasyEcs performs badly on it.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>performance</category>
      <category>opensource</category>
    </item>
    <item>
      <title>100k particles in C# WebAssembly: ~0.1 ms for motion on an M4</title>
      <dc:creator>Jason Xu</dc:creator>
      <pubDate>Tue, 20 Oct 2026 14:49:33 +0000</pubDate>
      <link>https://dev.to/jasonxudeveloper/100k-particles-in-c-webassembly-01-ms-for-motion-on-an-m4-2ipc</link>
      <guid>https://dev.to/jasonxudeveloper/100k-particles-in-c-webassembly-01-ms-for-motion-on-an-m4-2ipc</guid>
      <description>&lt;p&gt;Set the &lt;a href="https://ecs.xgamedev.net/en/docs/demo" rel="noopener noreferrer"&gt;EasyEcs live demo&lt;/a&gt; to &lt;strong&gt;100k&lt;/strong&gt; and move your pointer across the sparks. They bend into a vortex. On the M4 I used, the batched drag and movement systems take about &lt;strong&gt;0.1 ms&lt;/strong&gt; for around 100,000 live entities. The same work as per-entity &lt;code&gt;[ForEach]&lt;/code&gt; methods takes about &lt;strong&gt;0.5 ms&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd975nbv4dnjlxo6mtny4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd975nbv4dnjlxo6mtny4.png" alt="The EasyEcs browser demo at its 100k setting, with the pointer pulling sparks into a vortex and live ECS timings below" width="800" height="688"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;One live M4 browser frame. The selected target is 100k; the live count changes as sparks expire and are replaced.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is C# compiled with &lt;strong&gt;Mono AOT to WebAssembly&lt;/strong&gt;, using 128-bit SIMD. The reading varies from frame to frame, and the ~0.1 ms covers &lt;strong&gt;only drag and movement&lt;/strong&gt;. The separate “all systems” number adds lifetime, spawning, pointer attraction, bounces and fading. Neither figure includes copying the frame array or drawing it. The timers in &lt;a href="https://github.com/JasonXuDeveloper/EasyEcs/blob/master/demo/EasyEcs.Demo/Demo.cs" rel="noopener noreferrer"&gt;&lt;code&gt;Demo.Step&lt;/code&gt;&lt;/a&gt; mark the boundary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Stopwatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetTimestamp&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batched&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Batched&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PerEntity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;moved&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Stopwatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetTimestamp&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// lifetime, spawning, attraction, bounces, fading&lt;/span&gt;
&lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Stopwatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetTimestamp&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;start → moved&lt;/code&gt; is the motion group. &lt;code&gt;start → end&lt;/code&gt; adds the other ECS systems; rendering still happens later.&lt;/p&gt;

&lt;h2&gt;
  
  
  One job per spark, or a span of sparks
&lt;/h2&gt;

&lt;p&gt;The contrast is easier to see in code. Both are &lt;code&gt;[ForEach]&lt;/code&gt; jobs from the live demo. In the first, the generated loop calls &lt;code&gt;Move&lt;/code&gt; for each spark:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ForEach&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Move&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="n"&gt;Position&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;Velocity&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;Clock&lt;/span&gt; &lt;span class="n"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Y&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Y&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The batch version receives matching runs of position and velocity columns as spans. Each &lt;code&gt;Vector128&amp;lt;float&amp;gt;&lt;/code&gt; contains the X and Y values of &lt;strong&gt;two&lt;/strong&gt; sparks; the final scalar branch handles an odd-sized run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ForEach&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Move&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Span&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Position&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ReadOnlySpan&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Velocity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;velocities&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;Clock&lt;/span&gt; &lt;span class="n"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MemoryMarshal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cast&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Position&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Vector128&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MemoryMarshal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cast&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Velocity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Vector128&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;velocities&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Vector128&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt;
        &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;ref&lt;/span&gt; &lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;[^&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;velocities&lt;/span&gt;&lt;span class="p"&gt;[^&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Y&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;velocities&lt;/span&gt;&lt;span class="p"&gt;[^&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;Y&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Motion group in the live demo&lt;/th&gt;
&lt;th&gt;Work&lt;/th&gt;
&lt;th&gt;M4 browser reading during this check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Per-entity &lt;code&gt;[ForEach]&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Drag and movement, one spark per call&lt;/td&gt;
&lt;td&gt;around &lt;strong&gt;0.5 ms&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch Span + SIMD&lt;/td&gt;
&lt;td&gt;The same drag and movement, two sparks per vector&lt;/td&gt;
&lt;td&gt;around &lt;strong&gt;0.1 ms&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The table times &lt;strong&gt;drag and movement together&lt;/strong&gt;; the excerpts show only movement. The &lt;a href="https://github.com/JasonXuDeveloper/EasyEcs/blob/master/demo/EasyEcs.Demo/Systems/MovementSystem.cs" rel="noopener noreferrer"&gt;per-entity movement&lt;/a&gt;, &lt;a href="https://github.com/JasonXuDeveloper/EasyEcs/blob/master/demo/EasyEcs.Demo/Systems/MovementBatchSystem.cs" rel="noopener noreferrer"&gt;batch movement&lt;/a&gt; and both &lt;a href="https://github.com/JasonXuDeveloper/EasyEcs/blob/master/demo/EasyEcs.Demo/Systems/DragSystem.cs" rel="noopener noreferrer"&gt;drag&lt;/a&gt; &lt;a href="https://github.com/JasonXuDeveloper/EasyEcs/blob/master/demo/EasyEcs.Demo/Systems/DragBatchSystem.cs" rel="noopener noreferrer"&gt;versions&lt;/a&gt; are in the repo. You can switch between 1k, 10k, 50k and 100k on the &lt;a href="https://ecs.xgamedev.net/en/docs/demo" rel="noopener noreferrer"&gt;demo page&lt;/a&gt; and watch the numbers change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the data layout matters
&lt;/h2&gt;

&lt;p&gt;In an ECS, an &lt;strong&gt;entity&lt;/strong&gt; is an identity, &lt;strong&gt;components&lt;/strong&gt; are data such as position or health, and &lt;strong&gt;systems&lt;/strong&gt; loop over entities that have the components they need. Similar data sits together in memory, which suits tight loops.&lt;/p&gt;

&lt;p&gt;Layout alone does not make a program fast. A simulation or real-time backend also creates and destroys entities, expires buffs, looks up values, and saves or replicates state. Allocation in every tick invites GC pauses; scattered reads waste cache. EasyEcs is built around those operations as well as iteration.&lt;/p&gt;

&lt;h2&gt;
  
  
  How EasyEcs gets those spans
&lt;/h2&gt;

&lt;p&gt;EasyEcs stores C# structs in archetype tables with &lt;strong&gt;64-byte-aligned columns&lt;/strong&gt;. A source generator turns &lt;code&gt;[ForEach]&lt;/code&gt; methods into struct jobs that the JIT can inline. The batch method gets spans over the columns; the per-entity version does the same arithmetic one spark at a time.&lt;/p&gt;

&lt;p&gt;Outside the demo, EasyEcs applies structural changes immediately, even during a pass. If a component is removed and later restored, a presence bit can avoid moving the entity again. The library also has change tracking, indexes, relations, snapshots and parallel systems with checked read/write sets. Its warmed-up hot paths target &lt;strong&gt;0 B allocated&lt;/strong&gt;, with unit tests for that budget; creating a new table or growing past capacity can still allocate. The &lt;a href="https://ecs.xgamedev.net/en/docs/why-fast" rel="noopener noreferrer"&gt;performance design guide&lt;/a&gt; links the mechanisms to source and measurements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Browser demo versus native .NET
&lt;/h2&gt;

&lt;p&gt;The browser runs this demo on one thread with 128-bit SIMD. Native CoreCLR can use wider vectors and multiple threads. That gives native code more options, but the ~0.1 ms browser reading cannot be converted into a server tick: the workloads are different.&lt;/p&gt;

&lt;p&gt;The native benchmarks answer a separate question. On a 4-core 2.1 GHz Xeon VM with .NET 10.0.13, a &lt;strong&gt;100,000-entity tick across nine systems takes 1.72 ms and allocates 0 B&lt;/strong&gt; after warm-up. Four independent worlds of 25,000 entities, one per thread, finish in &lt;strong&gt;0.92 ms total&lt;/strong&gt;, also at 0 B. Saving and loading 10,000 entities and bulk changes across 100,000 show larger leads over other libraries in their own tests. The &lt;a href="https://ecs.xgamedev.net/en/docs/performance" rel="noopener noreferrer"&gt;performance guide&lt;/a&gt; and &lt;a href="https://ecs.xgamedev.net/en/docs/comparison" rel="noopener noreferrer"&gt;full comparison&lt;/a&gt; publish the setup, competitors and cases where EasyEcs loses.&lt;/p&gt;

&lt;p&gt;EasyEcs targets &lt;strong&gt;.NET 10&lt;/strong&gt; and fits game servers, simulations and other long-running real-time backends. It provides the ECS world, not network transport, and does not support Unity's current runtimes. Start with &lt;code&gt;dotnet add package EasyEcs&lt;/code&gt;, the &lt;a href="https://ecs.xgamedev.net/en/docs/getting-started" rel="noopener noreferrer"&gt;tutorial&lt;/a&gt; or the demo. If your workload exposes a gap, open an issue; if the library helps, a star on the &lt;a href="https://github.com/JasonXuDeveloper/EasyEcs" rel="noopener noreferrer"&gt;repository&lt;/a&gt; is welcome.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>performance</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
