Repository navigation
Expand file tree
/
Copy path2026-09-06-apecs-benchmark.html
More file actions
793 lines (788 loc) · 67.8 KB
/
Copy path2026-09-06-apecs-benchmark.html
File metadata and controls
793 lines (788 loc) · 67.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>apecs Benchmark Report</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500&display=swap"
/>
<style>
:root {
--ground: #ffffff;
--surface: #f4f4f4;
--surface-2: #e9e9e9;
--rule: #e4e4e4;
--ink: #0b0b0b;
--ink-2: #6b6b6b;
--ink-3: #9c9c9c;
--mark: #c4c4c4;
--red: #f43535;
/* The product blue is the positive, against the brand red. The design guide
reserves it for real product UI, so this is a deliberate extension of it —
and it separates from the red far better than a green would for a
red-blind reader. The deep step is the one that carries white labels at AA. */
--blue: #008cff;
--blue-deep: #006ec7;
--blue-soft: #e2f0ff;
/* One validated categorical set, used only where colour carries identity:
CVD-checked against this surface, and never the sole channel — every line
is labelled at its end. */
--s-apecs: #0079db;
--s-bitecs: #0f7a5a;
--s-koota: #7a3e9d;
--s-becsy: #b0641a;
--sans: 'Geist', system-ui, -apple-system, 'Segoe UI', sans-serif;
--mono: 'Geist Mono', ui-monospace, 'SF Mono', Menlo, monospace;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--ground);
color: var(--ink);
font-family: var(--sans);
font-size: 15px;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
.page { max-width: 1000px; margin: 0 auto; padding: 64px; }
@media (max-width: 720px) { .page { padding: 32px 24px; } }
main { display: flex; flex-direction: column; gap: 80px; }
section { display: flex; flex-direction: column; gap: 24px; }
h1, h2, h3 { margin: 0; text-wrap: balance; font-weight: 600; letter-spacing: -0.02em; }
h1 { font-size: 44px; line-height: 1.1; }
h2 { font-size: 24px; line-height: 1.25; }
h3 { font-size: 14px; font-weight: 500; letter-spacing: 0; }
h3 .unit { color: var(--ink-3); font-weight: 400; }
p { margin: 0; max-width: 66ch; color: var(--ink-2); }
p strong { color: var(--ink); font-weight: 500; }
code { font-family: var(--mono); font-size: 0.92em; color: var(--ink); }
.label {
font-size: 12px;
font-weight: 500;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--ink-3);
}
.masthead { display: flex; flex-direction: column; gap: 24px; padding-bottom: 40px; }
.dek { font-size: 19px; line-height: 1.5; max-width: 60ch; }
.runmeta {
display: flex;
flex-wrap: wrap;
gap: 8px 32px;
padding-top: 24px;
border-top: 1px solid var(--rule);
font-family: var(--mono);
font-size: 12.5px;
color: var(--ink-2);
}
.runmeta span::before { content: attr(data-key) ' '; color: var(--ink-3); }
.tiles { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
@media (max-width: 900px) { .tiles { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 720px) { .tiles { grid-template-columns: 1fr; } }
.tile {
background: var(--surface);
border-radius: 24px;
padding: 32px;
display: flex;
flex-direction: column;
gap: 16px;
}
.tile .figure.is-gap { color: var(--red); }
.tile .figure.is-win { color: var(--blue-deep); }
.tile .figure {
font-family: var(--mono);
font-size: 40px;
font-weight: 500;
letter-spacing: -0.03em;
line-height: 1;
font-variant-numeric: tabular-nums;
}
.tile p { font-size: 14px; }
.section-head { display: flex; flex-direction: column; gap: 16px; }
figure { margin: 0; display: flex; flex-direction: column; gap: 16px; }
.frame { background: var(--surface); border-radius: 24px; padding: 32px; overflow-x: auto; }
.frame svg { display: block; width: 100%; min-width: 560px; height: auto; }
figcaption { font-size: 13.5px; color: var(--ink-3); max-width: 74ch; line-height: 1.55; }
.legend { display: flex; flex-wrap: wrap; gap: 8px 24px; font-family: var(--mono); font-size: 12px; color: var(--ink-2); }
.legend span { display: inline-flex; align-items: center; gap: 8px; }
.key { width: 11px; height: 11px; border-radius: 50%; background: var(--ink); }
.key.hollow { background: var(--ground); box-shadow: inset 0 0 0 2px var(--ink); }
.key.s-apecs { background: var(--s-apecs); }
.key.s-bitecs { background: var(--s-bitecs); }
.key.s-koota { background: var(--s-koota); }
.key.s-becsy { background: var(--s-becsy); }
.key.dash.s-apecs { background: none; border-top-color: var(--s-apecs); }
.key.dash { width: 16px; height: 0; border-radius: 0; border-top: 2px dashed var(--ink); }
svg text { font-family: var(--mono); font-size: 12px; font-variant-numeric: tabular-nums; }
svg .row-label, svg .col-label { font-family: var(--sans); font-size: 13px; fill: var(--ink-2); }
svg .row-label.is-strong, svg .col-label.is-strong { fill: var(--ink); font-weight: 500; }
svg .value { fill: var(--ink-2); }
svg .value.on-dark { fill: var(--ground); font-weight: 500; }
svg .cell { fill: var(--ink); font-size: 11.5px; }
svg .cell.on-dark { fill: var(--ground); }
svg .tick { fill: var(--ink-3); font-size: 11px; }
svg .grid { stroke: var(--rule); stroke-width: 1; }
svg .axis-line { stroke: var(--ink-3); stroke-width: 1; }
svg .ref { stroke: var(--ink-3); stroke-width: 1; stroke-dasharray: 2 3; }
svg .link { stroke: var(--mark); stroke-width: 2; }
svg .bar { fill: var(--mark); }
svg .bar.is-strong { fill: var(--ink); }
svg .bar.is-over { fill: var(--red); }
svg .bar.is-weak { fill: var(--ink-2); }
svg .dot { fill: var(--mark); stroke: var(--surface); stroke-width: 2; }
svg .dot.is-strong { fill: var(--ink); }
svg .dot.hollow { fill: var(--surface); stroke: var(--mark); }
svg .dot.hollow.is-strong { stroke: var(--ink); }
svg .win { fill: none; stroke: var(--blue); stroke-width: 2.5; }
svg .line { fill: none; stroke: var(--ink); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
svg .line.dashed { stroke-dasharray: 5 4; stroke-width: 2; }
svg .s-apecs { stroke: var(--s-apecs); }
svg .s-bitecs { stroke: var(--s-bitecs); }
svg .s-koota { stroke: var(--s-koota); }
svg .s-becsy { stroke: var(--s-becsy); }
svg .mark { stroke: var(--surface); stroke-width: 1.5; }
svg .mark.s-apecs { fill: var(--s-apecs); }
svg .mark.s-bitecs { fill: var(--s-bitecs); }
svg .mark.s-koota { fill: var(--s-koota); }
svg .mark.s-becsy { fill: var(--s-becsy); }
.scroll { overflow-x: auto; }
table { border-collapse: collapse; width: 100%; font-size: 14px; }
th, td { text-align: right; padding: 12px 16px; border-bottom: 1px solid var(--rule); white-space: nowrap; }
th:first-child, td:first-child { text-align: left; padding-left: 0; }
th:last-child, td:last-child { padding-right: 0; }
thead th {
font-size: 12px;
font-weight: 500;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--ink-3);
border-bottom-color: var(--ink-3);
}
tbody td { font-family: var(--mono); font-variant-numeric: tabular-nums; color: var(--ink-2); }
tbody td:first-child { font-family: var(--sans); color: var(--ink); }
tbody tr:last-child td { border-bottom: none; }
td.lead { color: var(--ink); font-weight: 500; }
td.sub { font-family: var(--sans); font-size: 13px; color: var(--ink-3); white-space: normal; }
td .mul { display: block; font-size: 12px; color: var(--ink-3); }
.status { display: inline-flex; align-items: center; gap: 8px; font-family: var(--sans); font-size: 13px; }
.status::before { content: ''; width: 8px; height: 8px; border-radius: 50%; background: var(--blue); }
.status.over { color: var(--red); }
.status.over::before { background: var(--red); }
tr.over td { color: var(--ink); }
.note {
border-left: 2px solid var(--ink);
padding: 4px 0 4px 24px;
max-width: 74ch;
font-size: 15px;
color: var(--ink-2);
}
.note .lab { display: block; margin-bottom: 8px; }
.method { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; }
@media (max-width: 720px) { .method { grid-template-columns: 1fr; } }
.mcell { background: var(--surface); border-radius: 24px; padding: 24px 24px 28px; display: flex; flex-direction: column; gap: 8px; }
.mcell p { font-size: 14px; }
footer {
margin-top: 80px;
padding-top: 24px;
border-top: 1px solid var(--rule);
display: flex;
flex-direction: column;
gap: 16px;
}
footer pre { margin: 0; font-family: var(--mono); font-size: 12.5px; color: var(--ink-2); overflow-x: auto; }
a { color: var(--ink); text-decoration-color: var(--ink-3); text-underline-offset: 3px; }
:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }
/* Print: the surfaces and the heat cells are data, so they have to survive the
print pipeline's default of dropping backgrounds. */
@page { size: A4 portrait; margin: 14mm; }
@media print {
:root { print-color-adjust: exact; -webkit-print-color-adjust: exact; }
body { font-size: 11pt; }
.page { padding: 0; max-width: none; }
main { gap: 40px; }
h1 { font-size: 32px; }
h2 { font-size: 19px; }
.frame, .mcell, .tile { overflow: visible; }
figure, .frame, .tiles, .method, .note, tr { break-inside: avoid; }
.legend { break-after: avoid; }
section { break-inside: auto; }
h2 { break-after: avoid; }
.section-head { break-inside: avoid; break-after: avoid; }
.scroll { overflow: visible; }
.frame svg { min-width: 0; }
}
</style>
</head>
<body>
<div class="page">
<header class="masthead">
<div class="label">Diffusion Studio · apecs 0.1.0</div>
<h1>Where apecs stands</h1>
<p class="dek">
The whole measured surface as it is today: eight benchmarks against bitECS, koota and becsy
with a hand-written typed-array loop as the floor, the nine SPEC §12.1 budgets, both known
gaps, and what a query in key order costs.
</p>
<div class="runmeta">
<span data-key="machine">Apple M1</span>
<span data-key="node">v20.19.0</span>
<span data-key="suite">6 September 2026</span>
<span data-key="comparison">6 September 2026 · min of 3 runs</span>
</div>
</header>
<main>
<section>
<div class="tiles">
<div class="tile">
<div class="figure is-win">0.97×</div>
<p><strong>The hot loop, against a hand-written typed-array loop.</strong> 100 000
entities, two traits, one integrate pass through <code>chunks</code>.</p>
</div>
<div class="tile">
<div class="figure">38.1 B</div>
<p><strong>Per entity</strong>, where the payload itself is 16 and the next-lightest
library charges 154.</p>
</div>
<div class="tile">
<div class="figure">8 of 9</div>
<p><strong>SPEC §12.1 budgets held.</strong> The miss is per-entity access by handle, at
37.45× of a flat
array.</p>
</div>
<div class="tile">
<div class="figure">16.2×</div>
<p><strong>The next ergonomic tier, at eight traits.</strong> apecs holds
~2.0 ns per entity visit at every trait
count; koota and becsy lose an inline and never get it back.</p>
</div>
</div>
</section>
<section>
<div class="section-head">
<div class="label">Cross-library</div>
<h2>Against the field</h2>
<p>
apecs against the fastest of bitECS, koota and becsy on each benchmark, log scale. Each
library runs its own fastest correct idiom, one process per pair, and no timing is
published until every library has matched the same entity count.
</p>
</div>
<figure>
<div class="frame"><svg viewBox="0 0 824 320" role="img" aria-label="apecs against its fastest rival on each benchmark"><line class="grid" x1="219.5631469664209" y1="26" x2="219.5631469664209" y2="292" /><text class="tick" x="219.5631469664209" y="308" text-anchor="middle">10×</text><line class="grid" x1="344.63150924858553" y1="26" x2="344.63150924858553" y2="292" /><text class="tick" x="344.63150924858553" y="308" text-anchor="middle">5×</text><line class="grid" x1="510.0042939273281" y1="26" x2="510.0042939273281" y2="292" /><text class="tick" x="510.0042939273281" y="308" text-anchor="middle">2×</text><line class="grid" x1="760.1410184916573" y1="26" x2="760.1410184916573" y2="292" /><text class="tick" x="760.1410184916573" y="308" text-anchor="middle">2×</text><text class="tick" x="204" y="16" text-anchor="start">apecs slower</text><text class="tick" x="804" y="16" text-anchor="end">apecs faster</text><line class="axis-line" x1="635.0726562094927" y1="22" x2="635.0726562094927" y2="292" /><text class="row-label is-strong" x="186" y="48" text-anchor="end">Spawn/despawn</text><rect class="bar is-strong" x="635.0726562094927" y="34" width="166.3" height="19"><title>Spawn/despawn: 2.5× vs bitECS faster</title></rect /><text class="value on-dark" x="791.3489483157497" y="48" text-anchor="end">2.5× vs bitECS</text><text class="row-label is-strong" x="186" y="80" text-anchor="end">5 systems</text><rect class="bar is-strong" x="635.0726562094927" y="66" width="77.9" height="19"><title>5 systems: 1.5× vs bitECS faster</title></rect /><text class="value" x="722.9370001133907" y="80" text-anchor="start">1.5× vs bitECS</text><text class="row-label is-strong" x="186" y="112" text-anchor="end">26 archetypes</text><rect class="bar is-strong" x="635.0726562094927" y="98" width="37.9" height="19"><title>26 archetypes: 1.2× vs bitECS faster</title></rect /><text class="value" x="682.9967799560204" y="112" text-anchor="start">1.2× vs bitECS</text><text class="row-label is-strong" x="186" y="144" text-anchor="end">Query + exclusions</text><rect class="bar is-strong" x="635.0726562094927" y="130" width="34.8" height="19"><title>Query + exclusions: 1.2× vs bitECS faster</title></rect /><text class="value" x="679.854281095162" y="144" text-anchor="start">1.2× vs bitECS</text><text class="row-label is-strong" x="186" y="176" text-anchor="end">Iterate 1 trait</text><rect class="bar is-strong" x="635.0726562094927" y="162" width="34.2" height="19"><title>Iterate 1 trait: 1.2× vs bitECS faster</title></rect /><text class="value" x="679.3039370990225" y="176" text-anchor="start">1.2× vs bitECS</text><text class="row-label is-strong" x="186" y="208" text-anchor="end">Move 100 000</text><rect class="bar is-weak" x="554.4848006913072" y="194" width="80.6" height="19"><title>Move 100 000: 1.6× vs bitECS slower</title></rect /><text class="value" x="544.4848006913072" y="208" text-anchor="end">1.6× vs bitECS</text><text class="row-label is-strong" x="186" y="240" text-anchor="end">Add/remove trait</text><rect class="bar is-weak" x="321.70603600719494" y="226" width="313.4" height="19"><title>Add/remove trait: 5.7× vs becsy slower</title></rect /><text class="value on-dark" x="331.70603600719494" y="240" text-anchor="start">5.7× vs becsy</text><text class="row-label is-strong" x="186" y="272" text-anchor="end">Look up by handle</text><rect class="bar is-weak" x="216.7335637003651" y="258" width="418.3" height="19"><title>Look up by handle: 10.2× vs bitECS slower</title></rect /><text class="value on-dark" x="226.7335637003651" y="272" text-anchor="start">10.2× vs bitECS</text></svg></div>
<figcaption>
The losses are the same story: work that touches one entity at a time rather than a whole
column. Iteration and query matching, where a column is the unit, are wins.
</figcaption>
</figure>
<figure>
<div class="frame"><svg viewBox="0 0 824 348" role="img" aria-label="Every benchmark and library as a multiple of the hand-written baseline"><text class="col-label" x="233.6" y="22" text-anchor="middle">hand-written</text><text class="col-label is-strong" x="364.79999999999995" y="22" text-anchor="middle">apecs</text><text class="col-label" x="496" y="22" text-anchor="middle">bitECS</text><text class="col-label" x="627.1999999999999" y="22" text-anchor="middle">koota</text><text class="col-label" x="758.4" y="22" text-anchor="middle">becsy</text><text class="row-label is-strong" x="154" y="56" text-anchor="end">Iterate 1 trait</text><rect x="170.0" y="36" width="127.2" height="30" fill="var(--surface-2)"><title>hand-written · Iterate 1 trait: 1.0× baseline</title></rect><text class="cell" x="233.6" y="56" text-anchor="middle">1.0×</text><rect x="301.2" y="36" width="127.2" height="30" fill="rgb(220,236,252)"><title>apecs · Iterate 1 trait: 1.2× baseline, fastest in this row</title></rect><rect class="win" x="302.2" y="37" width="125.2" height="28" /><text class="cell" x="364.79999999999995" y="56" text-anchor="middle">1.2×</text><rect x="432.4" y="36" width="127.2" height="30" fill="rgb(239,239,239)"><title>bitECS · Iterate 1 trait: 1.4× baseline</title></rect><text class="cell" x="496" y="56" text-anchor="middle">1.4×</text><rect x="563.6" y="36" width="127.2" height="30" fill="rgb(211,211,211)"><title>koota · Iterate 1 trait: 2.9× baseline</title></rect><text class="cell" x="627.1999999999999" y="56" text-anchor="middle">2.9×</text><rect x="694.8" y="36" width="127.2" height="30" fill="rgb(191,191,191)"><title>becsy · Iterate 1 trait: 4.8× baseline</title></rect><text class="cell" x="758.4" y="56" text-anchor="middle">4.8×</text><text class="row-label is-strong" x="154" y="92" text-anchor="end">5 systems</text><rect x="170.0" y="72" width="127.2" height="30" fill="var(--surface-2)"><title>hand-written · 5 systems: 1.0× baseline</title></rect><text class="cell" x="233.6" y="92" text-anchor="middle">1.0×</text><rect x="301.2" y="72" width="127.2" height="30" fill="rgb(226,240,255)"><title>apecs · 5 systems: 0.9× baseline, fastest in this row</title></rect><rect class="win" x="302.2" y="73" width="125.2" height="28" /><text class="cell" x="364.79999999999995" y="92" text-anchor="middle">0.9×</text><rect x="432.4" y="72" width="127.2" height="30" fill="rgb(239,239,239)"><title>bitECS · 5 systems: 1.4× baseline</title></rect><text class="cell" x="496" y="92" text-anchor="middle">1.4×</text><rect x="563.6" y="72" width="127.2" height="30" fill="rgb(219,219,219)"><title>koota · 5 systems: 2.3× baseline</title></rect><text class="cell" x="627.1999999999999" y="92" text-anchor="middle">2.3×</text><rect x="694.8" y="72" width="127.2" height="30" fill="rgb(197,197,197)"><title>becsy · 5 systems: 4.1× baseline</title></rect><text class="cell" x="758.4" y="92" text-anchor="middle">4.1×</text><text class="row-label is-strong" x="154" y="128" text-anchor="end">Move 100 000</text><rect x="170.0" y="108" width="127.2" height="30" fill="var(--surface-2)"><title>hand-written · Move 100 000: 1.0× baseline</title></rect><text class="cell" x="233.6" y="128" text-anchor="middle">1.0×</text><rect x="301.2" y="108" width="127.2" height="30" fill="rgb(224,224,224)"><title>apecs · Move 100 000: 2.1× baseline</title></rect><text class="cell" x="364.79999999999995" y="128" text-anchor="middle">2.1×</text><rect x="432.4" y="108" width="127.2" height="30" fill="rgb(216,233,250)"><title>bitECS · Move 100 000: 1.3× baseline, fastest in this row</title></rect><rect class="win" x="433.4" y="109" width="125.2" height="28" /><text class="cell" x="496" y="128" text-anchor="middle">1.3×</text><rect x="563.6" y="108" width="127.2" height="30" fill="rgb(197,197,197)"><title>koota · Move 100 000: 4.2× baseline</title></rect><text class="cell" x="627.1999999999999" y="128" text-anchor="middle">4.2×</text><rect x="694.8" y="108" width="127.2" height="30" fill="rgb(128,128,128)"><title>becsy · Move 100 000: 24.8× baseline</title></rect><text class="cell" x="758.4" y="128" text-anchor="middle">24.8×</text><text class="row-label is-strong" x="154" y="164" text-anchor="end">26 archetypes</text><rect x="170.0" y="144" width="127.2" height="30" fill="var(--surface-2)"><title>hand-written · 26 archetypes: 1.0× baseline</title></rect><text class="cell" x="233.6" y="164" text-anchor="middle">1.0×</text><rect x="301.2" y="144" width="127.2" height="30" fill="rgb(226,240,255)"><title>apecs · 26 archetypes: 0.9× baseline, fastest in this row</title></rect><rect class="win" x="302.2" y="145" width="125.2" height="28" /><text class="cell" x="364.79999999999995" y="164" text-anchor="middle">0.9×</text><rect x="432.4" y="144" width="127.2" height="30" fill="rgb(248,248,248)"><title>bitECS · 26 archetypes: 1.1× baseline</title></rect><text class="cell" x="496" y="164" text-anchor="middle">1.1×</text><rect x="563.6" y="144" width="127.2" height="30" fill="rgb(205,205,205)"><title>koota · 26 archetypes: 3.4× baseline</title></rect><text class="cell" x="627.1999999999999" y="164" text-anchor="middle">3.4×</text><rect x="694.8" y="144" width="127.2" height="30" fill="rgb(203,203,203)"><title>becsy · 26 archetypes: 3.6× baseline</title></rect><text class="cell" x="758.4" y="164" text-anchor="middle">3.6×</text><text class="row-label is-strong" x="154" y="200" text-anchor="end">Spawn/despawn</text><rect x="170.0" y="180" width="127.2" height="30" fill="var(--surface-2)"><title>hand-written · Spawn/despawn: 1.0× baseline</title></rect><text class="cell" x="233.6" y="200" text-anchor="middle">1.0×</text><rect x="301.2" y="180" width="127.2" height="30" fill="rgb(78,131,178)"><title>apecs · Spawn/despawn: 58.7× baseline, fastest in this row</title></rect><text class="cell on-dark" x="364.79999999999995" y="200" text-anchor="middle">58.7×</text><rect x="432.4" y="180" width="127.2" height="30" fill="rgb(59,59,59)"><title>bitECS · Spawn/despawn: 147× baseline</title></rect><text class="cell on-dark" x="496" y="200" text-anchor="middle">147×</text><rect x="563.6" y="180" width="127.2" height="30" fill="rgb(32,32,32)"><title>koota · Spawn/despawn: 301× baseline</title></rect><text class="cell on-dark" x="627.1999999999999" y="200" text-anchor="middle">301×</text><rect x="694.8" y="180" width="127.2" height="30" fill="rgb(13,13,13)"><title>becsy · Spawn/despawn: 487× baseline</title></rect><text class="cell on-dark" x="758.4" y="200" text-anchor="middle">487×</text><text class="row-label is-strong" x="154" y="236" text-anchor="end">Add/remove trait</text><rect x="170.0" y="216" width="127.2" height="30" fill="var(--surface-2)"><title>hand-written · Add/remove trait: 1.0× baseline</title></rect><text class="cell" x="233.6" y="236" text-anchor="middle">1.0×</text><rect x="301.2" y="216" width="127.2" height="30" fill="rgb(57,57,57)"><title>apecs · Add/remove trait: 156× baseline</title></rect><text class="cell on-dark" x="364.79999999999995" y="236" text-anchor="middle">156×</text><rect x="432.4" y="216" width="127.2" height="30" fill="rgb(64,64,64)"><title>bitECS · Add/remove trait: 132× baseline</title></rect><text class="cell on-dark" x="496" y="236" text-anchor="middle">132×</text><rect x="563.6" y="216" width="127.2" height="30" fill="rgb(48,48,48)"><title>koota · Add/remove trait: 199× baseline</title></rect><text class="cell on-dark" x="627.1999999999999" y="236" text-anchor="middle">199×</text><rect x="694.8" y="216" width="127.2" height="30" fill="rgb(105,151,193)"><title>becsy · Add/remove trait: 27.5× baseline, fastest in this row</title></rect><text class="cell on-dark" x="758.4" y="236" text-anchor="middle">27.5×</text><text class="row-label is-strong" x="154" y="272" text-anchor="end">Query + exclusions</text><rect x="170.0" y="252" width="127.2" height="30" fill="var(--surface-2)"><title>hand-written · Query + exclusions: 1.0× baseline</title></rect><text class="cell" x="233.6" y="272" text-anchor="middle">1.0×</text><rect x="301.2" y="252" width="127.2" height="30" fill="rgb(226,240,255)"><title>apecs · Query + exclusions: 0.8× baseline, fastest in this row</title></rect><rect class="win" x="302.2" y="253" width="125.2" height="28" /><text class="cell" x="364.79999999999995" y="272" text-anchor="middle">0.8×</text><rect x="432.4" y="252" width="127.2" height="30" fill="rgb(252,252,252)"><title>bitECS · Query + exclusions: 1.0× baseline</title></rect><text class="cell" x="496" y="272" text-anchor="middle">1.0×</text><rect x="563.6" y="252" width="127.2" height="30" fill="rgb(210,210,210)"><title>koota · Query + exclusions: 3.0× baseline</title></rect><text class="cell" x="627.1999999999999" y="272" text-anchor="middle">3.0×</text><rect x="694.8" y="252" width="127.2" height="30" fill="rgb(208,208,208)"><title>becsy · Query + exclusions: 3.1× baseline</title></rect><text class="cell" x="758.4" y="272" text-anchor="middle">3.1×</text><text class="row-label is-strong" x="154" y="308" text-anchor="end">Look up by handle</text><rect x="170.0" y="288" width="127.2" height="30" fill="var(--surface-2)"><title>hand-written · Look up by handle: 1.0× baseline</title></rect><text class="cell" x="233.6" y="308" text-anchor="middle">1.0×</text><rect x="301.2" y="288" width="127.2" height="30" fill="rgb(154,154,154)"><title>apecs · Look up by handle: 12.6× baseline</title></rect><text class="cell" x="364.79999999999995" y="308" text-anchor="middle">12.6×</text><rect x="432.4" y="288" width="127.2" height="30" fill="rgb(218,234,251)"><title>bitECS · Look up by handle: 1.2× baseline, fastest in this row</title></rect><rect class="win" x="433.4" y="289" width="125.2" height="28" /><text class="cell" x="496" y="308" text-anchor="middle">1.2×</text><rect x="563.6" y="288" width="127.2" height="30" fill="rgb(226,226,226)"><title>koota · Look up by handle: 2.0× baseline</title></rect><text class="cell" x="627.1999999999999" y="308" text-anchor="middle">2.0×</text><rect x="694.8" y="288" width="127.2" height="30" fill="rgb(179,179,179)"><title>becsy · Look up by handle: 6.7× baseline</title></rect><text class="cell" x="758.4" y="308" text-anchor="middle">6.7×</text><text class="tick" x="168" y="342" text-anchor="start">lower is better · 1.0× is the hand-written typed-array loop</text></svg></div>
<figcaption>
Every cell as a multiple of the hand-written floor; the fastest library in each row is
the blue one. apecs beats the floor outright on three rows, which is what a column-shaped
query buys over an index-shaped one.
</figcaption>
</figure>
<div class="scroll">
<table>
<thead>
<tr>
<th>What it does</th>
<th>hand-written</th>
<th>apecs</th>
<th>bitECS</th>
<th>koota</th>
<th>becsy</th>
</tr>
</thead>
<tbody>
<tr><td class="lead">Iterate one trait</td><td>4.0</td><td class="lead">4.7<span class="mul">1.2×</span></td><td>5.6<span class="mul">1.4×</span></td><td>11.5<span class="mul">2.9×</span></td><td>19.1<span class="mul">4.8×</span></td></tr>
<tr><td class="lead">Five systems in a row</td><td>5.4</td><td class="lead">4.9<span class="mul">0.9×</span></td><td>7.6<span class="mul">1.4×</span></td><td>12.6<span class="mul">2.3×</span></td><td>22.3<span class="mul">4.1×</span></td></tr>
<tr><td class="lead">Move 100 000 entities</td><td>122</td><td class="lead">252<span class="mul">2.1×</span></td><td>161<span class="mul">1.3×</span></td><td>513<span class="mul">4.2×</span></td><td>3,021<span class="mul">24.8×</span></td></tr>
<tr><td class="lead">Iterate across 26 archetypes</td><td>105</td><td class="lead">94.1<span class="mul">0.9×</span></td><td>116<span class="mul">1.1×</span></td><td>355<span class="mul">3.4×</span></td><td>376<span class="mul">3.6×</span></td></tr>
<tr><td class="lead">Spawn and despawn</td><td>247</td><td class="lead">14,490<span class="mul">58.7×</span></td><td>36,413<span class="mul">147×</span></td><td>74,436<span class="mul">301×</span></td><td>120,303<span class="mul">487×</span></td></tr>
<tr><td class="lead">Add and remove a trait</td><td>132</td><td class="lead">20,572<span class="mul">156×</span></td><td>17,329<span class="mul">132×</span></td><td>26,158<span class="mul">199×</span></td><td>3,623<span class="mul">27.5×</span></td></tr>
<tr><td class="lead">Query with exclusions</td><td>60.4</td><td class="lead">47.4<span class="mul">0.8×</span></td><td>57.4<span class="mul">1.0×</span></td><td>180<span class="mul">3.0×</span></td><td>189<span class="mul">3.1×</span></td></tr>
<tr><td class="lead">Look up by entity handle</td><td>111</td><td class="lead">1,393<span class="mul">12.6×</span></td><td>137<span class="mul">1.2×</span></td><td>219<span class="mul">2.0×</span></td><td>737<span class="mul">6.7×</span></td></tr>
</tbody>
</table>
</div>
</section>
<section>
<div class="section-head">
<div class="label">SPEC §12.1</div>
<h2>Budgets</h2>
<p>
Each budget is a ratio between two benchmarks of the same run, never an absolute time: a
laptop's microseconds say nothing, but “<code>chunks</code> is within 1.1× of the
hand-written loop” holds anywhere. 8 of 9
hold; 1 does not.
</p>
</div>
<figure>
<div class="frame">
<svg viewBox="0 0 824 356" role="img" aria-label="Share of each budget spent"><line class="grid" x1="411.75" y1="8" x2="411.75" y2="326" /><text class="tick" x="411.75" y="342" text-anchor="middle">50%</text><line class="grid" x1="555.5" y1="8" x2="555.5" y2="326" /><text class="tick" x="555.5" y="342" text-anchor="middle">100%</text><line class="ref" x1="555.5" y1="4" x2="555.5" y2="326" /><text class="tick" x="563.5" y="14" text-anchor="start">the limit</text><text class="row-label" x="254" y="31" text-anchor="end">simple-iter · each vs baseline</text><rect class="bar" x="268" y="18" width="247.9" height="18"><title>simple-iter · each vs baseline: 86% of budget</title></rect /><text class="value" x="525.9479768785911" y="31" text-anchor="start">86%</text><text class="row-label" x="254" y="65" text-anchor="end">simple-iter · chunks vs baseline</text><rect class="bar" x="268" y="52" width="253.0" height="18"><title>simple-iter · chunks vs baseline: 88% of budget</title></rect /><text class="value" x="531.0120861798479" y="65" text-anchor="start">88%</text><text class="row-label" x="254" y="99" text-anchor="end">packed-5 · chunks vs baseline</text><rect class="bar" x="268" y="86" width="239.1" height="18"><title>packed-5 · chunks vs baseline: 83% of budget</title></rect /><text class="value" x="517.0852390887652" y="99" text-anchor="start">83%</text><text class="row-label" x="254" y="133" text-anchor="end">packed-5 · each vs baseline</text><rect class="bar" x="268" y="120" width="200.3" height="18"><title>packed-5 · each vs baseline: 70% of budget</title></rect /><text class="value" x="478.3072072065318" y="133" text-anchor="start">70%</text><text class="row-label" x="254" y="167" text-anchor="end">frag-iter · fixed cost per archetype</text><rect class="bar" x="268" y="154" width="0.0" height="18"><title>frag-iter · fixed cost per archetype: 0% of budget</title></rect /><text class="value" x="278" y="167" text-anchor="start">0%</text><text class="row-label" x="254" y="201" text-anchor="end">sorted-static · rebuild vs clean</text><rect class="bar" x="268" y="188" width="1.7" height="18"><title>sorted-static · rebuild vs clean: 1% of budget</title></rect /><text class="value" x="279.6814621367461" y="201" text-anchor="start">1%</text><text class="row-label" x="254" y="235" text-anchor="end">random-access · accessor vs baseline</text><rect class="bar is-over" x="268" y="222" width="430.7" height="18"><title>random-access · accessor vs baseline: 150% of budget</title></rect /><text class="value" x="708.6676013162506" y="235" text-anchor="start">150%</text><text class="row-label" x="254" y="269" text-anchor="end">ordered-iter · ordered chunks vs chunks</text><rect class="bar" x="268" y="256" width="261.5" height="18"><title>ordered-iter · ordered chunks vs chunks: 91% of budget</title></rect /><text class="value" x="539.4745854744124" y="269" text-anchor="start">91%</text><text class="row-label" x="254" y="303" text-anchor="end">sorted-drift · resort vs rebuild</text><rect class="bar" x="268" y="290" width="173.5" height="18"><title>sorted-drift · resort vs rebuild: 60% of budget</title></rect /><text class="value" x="451.52589207993225" y="303" text-anchor="start">60%</text><line class="axis-line" x1="268" y1="8" x2="268" y2="326" /><text class="tick" x="268" y="342" text-anchor="middle">0</text></svg>
</div>
<figcaption>
Share of the budget spent, so all nine sit on one axis — the two budgets stated as “at
least” are inverted to match. Anything past the dashed line is over.
</figcaption>
</figure>
<div class="scroll">
<table>
<thead>
<tr><th>Budget</th><th>Measured</th><th>Limit</th><th>§12.1 target</th><th>Verdict</th></tr>
</thead>
<tbody>
<tr>
<td class="lead">simple-iter · each vs baseline</td>
<td>2.16×</td>
<td>≤ 2.50×</td>
<td class="sub">1.5×</td>
<td><span class="status">within budget</span></td>
</tr>
<tr>
<td class="lead">simple-iter · chunks vs baseline</td>
<td>0.97×</td>
<td>≤ 1.10×</td>
<td class="sub">1.1×</td>
<td><span class="status">within budget</span></td>
</tr>
<tr>
<td class="lead">packed-5 · chunks vs baseline</td>
<td>1.08×</td>
<td>≤ 1.30×</td>
<td class="sub">parity</td>
<td><span class="status">within budget</span></td>
</tr>
<tr>
<td class="lead">packed-5 · each vs baseline</td>
<td>2.09×</td>
<td>≤ 3.00×</td>
<td class="sub">parity</td>
<td><span class="status">within budget</span></td>
</tr>
<tr>
<td class="lead">frag-iter · fixed cost per archetype</td>
<td>0ns</td>
<td>≤ 200ns</td>
<td class="sub">≤ 200ns</td>
<td><span class="status">within budget</span></td>
</tr>
<tr>
<td class="lead">sorted-static · rebuild vs clean</td>
<td>17098× cheaper</td>
<td>≥ 100× cheaper</td>
<td class="sub">zero work</td>
<td><span class="status">within budget</span></td>
</tr>
<tr class="over">
<td class="lead">random-access · accessor vs baseline</td>
<td>37.45×</td>
<td>≤ 25.00×</td>
<td class="sub">25×</td>
<td><span class="status over">over budget</span></td>
</tr>
<tr>
<td class="lead">ordered-iter · ordered chunks vs chunks</td>
<td>1.00×</td>
<td>≤ 1.10×</td>
<td class="sub">1.1×</td>
<td><span class="status">within budget</span></td>
</tr>
<tr>
<td class="lead">sorted-drift · resort vs rebuild</td>
<td>0.60×</td>
<td>≤ 1.00×</td>
<td class="sub">no full n log n</td>
<td><span class="status">within budget</span></td>
</tr>
</tbody>
</table>
</div>
<div class="note">
<span class="label lab">The one miss predates this work</span>
<code>world.accessor</code> resolves a field once and keeps a table per archetype, but three
dependent loads against one flat array load is the archetype tax, and 25× was always the
optimistic end of it. The iteration path is not involved and did not move.
</div>
</section>
<section>
<div class="section-head">
<div class="label">Tiers</div>
<h2>What the escape hatch buys</h2>
<p>
The ergonomic idiom against the raw one, same benchmark and same data, log scale. bitECS
has no ergonomic tier — the raw arrays are the API. becsy has no raw tier — everything
goes through a system.
</p>
</div>
<div class="legend">
<span><i class="key"></i>raw tier</span>
<span><i class="key hollow"></i>ergonomic tier</span>
</div>
<figure>
<div class="frame">
<svg viewBox="0 0 824 466" role="img" aria-label="Ergonomic tier against raw tier, microseconds"><line class="grid" x1="262.148777379002" y1="16" x2="262.148777379002" y2="436" /><text class="tick" x="262.148777379002" y="452" text-anchor="middle">10 µs</text><line class="grid" x1="365.70773415008" y1="16" x2="365.70773415008" y2="436" /><text class="tick" x="365.70773415008" y="452" text-anchor="middle">100 µs</text><line class="grid" x1="469.266690921158" y1="16" x2="469.266690921158" y2="436" /><text class="tick" x="469.266690921158" y="452" text-anchor="middle">1 ms</text><line class="grid" x1="572.825647692236" y1="16" x2="572.825647692236" y2="436" /><text class="tick" x="572.825647692236" y="452" text-anchor="middle">10 ms</text><text class="row-label is-strong" x="194" y="40" text-anchor="end">Iterate 1 trait · apecs</text><line class="link" x1="227.87036342509907" y1="36" x2="257.5475428089727" y2="36" /><circle class="dot is-strong" cx="227.9" cy="36.0" r="5"><title>Iterate 1 trait · apecs, raw: 5</title></circle><circle class="dot hollow is-strong" cx="257.5" cy="36.0" r="5"><title>Iterate 1 trait · apecs, ergonomic: 9</title></circle><text class="value" x="824" y="40" text-anchor="end">5 → 9</text><text class="row-label" x="194" y="74" text-anchor="end">Iterate 1 trait · koota</text><line class="link" x1="268.5880403732167" y1="70" x2="367.73927218141887" y2="70" /><circle class="dot" cx="268.6" cy="70.0" r="5"><title>Iterate 1 trait · koota, raw: 12</title></circle><circle class="dot hollow" cx="367.7" cy="70.0" r="5"><title>Iterate 1 trait · koota, ergonomic: 105</title></circle><text class="value" x="824" y="74" text-anchor="end">12 → 105</text><text class="row-label is-strong" x="194" y="108" text-anchor="end">5 systems · apecs</text><line class="link" x1="230.2566740334609" y1="104" x2="260.67918025996005" y2="104" /><circle class="dot is-strong" cx="230.3" cy="104.0" r="5"><title>5 systems · apecs, raw: 5</title></circle><circle class="dot hollow is-strong" cx="260.7" cy="104.0" r="5"><title>5 systems · apecs, ergonomic: 10</title></circle><text class="value" x="824" y="108" text-anchor="end">5 → 10</text><text class="row-label" x="194" y="142" text-anchor="end">5 systems · koota</text><line class="link" x1="272.67913007947453" y1="138" x2="391.3803678037981" y2="138" /><circle class="dot" cx="272.7" cy="138.0" r="5"><title>5 systems · koota, raw: 13</title></circle><circle class="dot hollow" cx="391.4" cy="138.0" r="5"><title>5 systems · koota, ergonomic: 177</title></circle><text class="value" x="824" y="142" text-anchor="end">13 → 177</text><text class="row-label is-strong" x="194" y="176" text-anchor="end">Move 100 000 · apecs</text><line class="link" x1="407.24918677957044" y1="172" x2="443.35363577425596" y2="172" /><circle class="dot is-strong" cx="407.2" cy="172.0" r="5"><title>Move 100 000 · apecs, raw: 252</title></circle><circle class="dot hollow is-strong" cx="443.4" cy="172.0" r="5"><title>Move 100 000 · apecs, ergonomic: 562</title></circle><text class="value" x="824" y="176" text-anchor="end">252 → 562</text><text class="row-label" x="194" y="210" text-anchor="end">Move 100 000 · koota</text><line class="link" x1="439.23537481117825" y1="206" x2="554.7725228626255" y2="206" /><circle class="dot" cx="439.2" cy="206.0" r="5"><title>Move 100 000 · koota, raw: 513</title></circle><circle class="dot hollow" cx="554.8" cy="206.0" r="5"><title>Move 100 000 · koota, ergonomic: 6,694</title></circle><text class="value" x="824" y="210" text-anchor="end">513 → 6,694</text><text class="row-label is-strong" x="194" y="244" text-anchor="end">26 archetypes · apecs</text><line class="link" x1="362.9755547197287" y1="240" x2="393.5501870948316" y2="240" /><circle class="dot is-strong" cx="363.0" cy="240.0" r="5"><title>26 archetypes · apecs, raw: 94</title></circle><circle class="dot hollow is-strong" cx="393.6" cy="240.0" r="5"><title>26 archetypes · apecs, ergonomic: 186</title></circle><text class="value" x="824" y="244" text-anchor="end">94 → 186</text><text class="row-label" x="194" y="278" text-anchor="end">26 archetypes · koota</text><line class="link" x1="422.6368016033978" y1="274" x2="505.2149070444636" y2="274" /><circle class="dot" cx="422.6" cy="274.0" r="5"><title>26 archetypes · koota, raw: 355</title></circle><circle class="dot hollow" cx="505.2" cy="274.0" r="5"><title>26 archetypes · koota, ergonomic: 2,224</title></circle><text class="value" x="824" y="278" text-anchor="end">355 → 2,224</text><text class="row-label is-strong" x="194" y="312" text-anchor="end">Query + exclusions · apecs</text><line class="link" x1="332.1111264365259" y1="308" x2="362.7372396072266" y2="308" /><circle class="dot is-strong" cx="332.1" cy="308.0" r="5"><title>Query + exclusions · apecs, raw: 47</title></circle><circle class="dot hollow is-strong" cx="362.7" cy="308.0" r="5"><title>Query + exclusions · apecs, ergonomic: 94</title></circle><text class="value" x="824" y="312" text-anchor="end">47 → 94</text><text class="row-label" x="194" y="346" text-anchor="end">Query + exclusions · koota</text><line class="link" x1="392.0569679112558" y1="342" x2="474.16894364500916" y2="342" /><circle class="dot" cx="392.1" cy="342.0" r="5"><title>Query + exclusions · koota, raw: 180</title></circle><circle class="dot hollow" cx="474.2" cy="342.0" r="5"><title>Query + exclusions · koota, ergonomic: 1,115</title></circle><text class="value" x="824" y="346" text-anchor="end">180 → 1,115</text><text class="row-label is-strong" x="194" y="380" text-anchor="end">Look up by handle · apecs</text><line class="link" x1="484.1622942109409" y1="376" x2="512.4717081699023" y2="376" /><circle class="dot is-strong" cx="484.2" cy="376.0" r="5"><title>Look up by handle · apecs, raw: 1,393</title></circle><circle class="dot hollow is-strong" cx="512.5" cy="376.0" r="5"><title>Look up by handle · apecs, ergonomic: 2,613</title></circle><text class="value" x="824" y="380" text-anchor="end">1,393 → 2,613</text><text class="row-label" x="194" y="414" text-anchor="end">Look up by handle · koota</text><line class="link" x1="400.9285983485979" y1="410" x2="576.0418336094128" y2="410" /><circle class="dot" cx="400.9" cy="410.0" r="5"><title>Look up by handle · koota, raw: 219</title></circle><circle class="dot hollow" cx="576.0" cy="410.0" r="5"><title>Look up by handle · koota, ergonomic: 10,741</title></circle><text class="value" x="824" y="414" text-anchor="end">219 → 10,741</text><line class="axis-line" x1="208" y1="16" x2="208" y2="436" /></svg>
</div>
<figcaption>
apecs pays about 2× to leave the raw arrays alone; koota pays between 6× and 58×. The
numbers are microseconds — raw first, ergonomic second.
</figcaption>
</figure>
</section>
<section>
<div class="section-head">
<div class="label">Trait count</div>
<h2>The five-trait cliff, and who pays it</h2>
<p>
Nanoseconds per entity visit against the number of distinct traits a program uses, one
trait count per process — sweeping inside one process lets earlier traits pollute the
dispatch site and manufactures a cliff whether or not it is there. Two of the four
libraries charge for trait count, and they break in different places: becsy at the fifth
trait, koota at the second.
</p>
</div>
<div class="legend">
<span><i class="key s-apecs"></i>apecs <code>each</code></span>
<span><i class="key s-apecs dash"></i>apecs <code>chunks</code></span>
<span><i class="key s-bitecs"></i>bitECS</span>
<span><i class="key s-koota"></i>koota</span>
<span><i class="key s-becsy"></i>becsy</span>
</div>
<figure>
<div class="frame"><svg viewBox="0 0 824 372" role="img" aria-label="Nanoseconds per entity visit against the number of distinct traits"><line class="grid" x1="56" y1="316" x2="656" y2="316" /><text class="tick" x="46" y="320" text-anchor="end">0</text><line class="grid" x1="56" y1="252" x2="656" y2="252" /><text class="tick" x="46" y="256" text-anchor="end">10</text><line class="grid" x1="56" y1="188" x2="656" y2="188" /><text class="tick" x="46" y="192" text-anchor="end">20</text><line class="grid" x1="56" y1="124" x2="656" y2="124" /><text class="tick" x="46" y="128" text-anchor="end">30</text><line class="grid" x1="56" y1="60" x2="656" y2="60" /><text class="tick" x="46" y="64" text-anchor="end">40</text><text class="tick" x="46" y="16" text-anchor="start">ns per entity visit</text><line class="ref" x1="406" y1="22" x2="406" y2="316" /><text class="tick" x="414" y="32" text-anchor="start">V8's four-shape limit</text><line class="axis-line" x1="56" y1="316" x2="656" y2="316" /><text class="tick" x="56" y="338" text-anchor="middle">1</text><text class="tick" x="156" y="338" text-anchor="middle">2</text><text class="tick" x="256" y="338" text-anchor="middle">3</text><text class="tick" x="356" y="338" text-anchor="middle">4</text><text class="tick" x="456" y="338" text-anchor="middle">5</text><text class="tick" x="556" y="338" text-anchor="middle">6</text><text class="tick" x="656" y="338" text-anchor="middle">8</text><text class="tick" x="356" y="360" text-anchor="middle">distinct traits used with the ergonomic API</text><polyline class="line s-apecs dashed" points="56.0,309.9 156.0,309.8 256.0,309.7 356.0,309.7 456.0,309.7 556.0,309.7 656.0,308.6" /><polyline class="line s-koota" points="56.0,180.5 156.0,110.9 256.0,110.8 356.0,111.1 456.0,110.4 556.0,110.9 656.0,111.5" /><circle class="mark s-koota" cx="56.0" cy="180.5" r="3"><title>koota, 1 traits: 21.17 ns</title></circle><circle class="mark s-koota" cx="156.0" cy="110.9" r="3"><title>koota, 2 traits: 32.05 ns</title></circle><circle class="mark s-koota" cx="256.0" cy="110.8" r="3"><title>koota, 3 traits: 32.06 ns</title></circle><circle class="mark s-koota" cx="356.0" cy="111.1" r="3"><title>koota, 4 traits: 32.02 ns</title></circle><circle class="mark s-koota" cx="456.0" cy="110.4" r="3"><title>koota, 5 traits: 32.13 ns</title></circle><circle class="mark s-koota" cx="556.0" cy="110.9" r="3"><title>koota, 6 traits: 32.04 ns</title></circle><circle class="mark s-koota" cx="656.0" cy="111.5" r="3"><title>koota, 8 traits: 31.96 ns</title></circle><polyline class="line s-becsy" points="56.0,280.5 156.0,255.3 256.0,253.2 356.0,250.1 456.0,68.7 556.0,71.5 656.0,73.8" /><circle class="mark s-becsy" cx="56.0" cy="280.5" r="3"><title>becsy, 1 traits: 5.54 ns</title></circle><circle class="mark s-becsy" cx="156.0" cy="255.3" r="3"><title>becsy, 2 traits: 9.49 ns</title></circle><circle class="mark s-becsy" cx="256.0" cy="253.2" r="3"><title>becsy, 3 traits: 9.82 ns</title></circle><circle class="mark s-becsy" cx="356.0" cy="250.1" r="3"><title>becsy, 4 traits: 10.30 ns</title></circle><circle class="mark s-becsy" cx="456.0" cy="68.7" r="3"><title>becsy, 5 traits: 38.64 ns</title></circle><circle class="mark s-becsy" cx="556.0" cy="71.5" r="3"><title>becsy, 6 traits: 38.20 ns</title></circle><circle class="mark s-becsy" cx="656.0" cy="73.8" r="3"><title>becsy, 8 traits: 37.85 ns</title></circle><polyline class="line s-bitecs" points="56.0,306.4 156.0,306.3 256.0,305.8 356.0,306.3 456.0,306.4 556.0,306.3 656.0,306.0" /><circle class="mark s-bitecs" cx="56.0" cy="306.4" r="3"><title>bitECS, 1 traits: 1.50 ns</title></circle><circle class="mark s-bitecs" cx="156.0" cy="306.3" r="3"><title>bitECS, 2 traits: 1.51 ns</title></circle><circle class="mark s-bitecs" cx="256.0" cy="305.8" r="3"><title>bitECS, 3 traits: 1.59 ns</title></circle><circle class="mark s-bitecs" cx="356.0" cy="306.3" r="3"><title>bitECS, 4 traits: 1.51 ns</title></circle><circle class="mark s-bitecs" cx="456.0" cy="306.4" r="3"><title>bitECS, 5 traits: 1.50 ns</title></circle><circle class="mark s-bitecs" cx="556.0" cy="306.3" r="3"><title>bitECS, 6 traits: 1.52 ns</title></circle><circle class="mark s-bitecs" cx="656.0" cy="306.0" r="3"><title>bitECS, 8 traits: 1.56 ns</title></circle><polyline class="line s-apecs" points="56.0,302.1 156.0,303.8 256.0,303.8 356.0,303.7 456.0,303.6 556.0,303.5 656.0,303.4" /><circle class="mark s-apecs" cx="56.0" cy="302.1" r="3"><title>apecs each, 1 traits: 2.17 ns</title></circle><circle class="mark s-apecs" cx="156.0" cy="303.8" r="3"><title>apecs each, 2 traits: 1.91 ns</title></circle><circle class="mark s-apecs" cx="256.0" cy="303.8" r="3"><title>apecs each, 3 traits: 1.91 ns</title></circle><circle class="mark s-apecs" cx="356.0" cy="303.7" r="3"><title>apecs each, 4 traits: 1.92 ns</title></circle><circle class="mark s-apecs" cx="456.0" cy="303.6" r="3"><title>apecs each, 5 traits: 1.94 ns</title></circle><circle class="mark s-apecs" cx="556.0" cy="303.5" r="3"><title>apecs each, 6 traits: 1.95 ns</title></circle><circle class="mark s-apecs" cx="656.0" cy="303.4" r="3"><title>apecs each, 8 traits: 1.97 ns</title></circle><circle class="mark s-becsy" cx="679.0" cy="69.8" r="4"></circle><text class="value" x="696" y="73.75999999999999" text-anchor="start">becsy 37.9</text><circle class="mark s-koota" cx="679.0" cy="107.5" r="4"></circle><text class="value" x="696" y="111.45600000000002" text-anchor="start">koota 32.0</text><circle class="mark s-apecs" cx="679.0" cy="299.4" r="4"></circle><text class="value" x="696" y="303.392" text-anchor="start">apecs each 2.0</text><circle class="mark s-bitecs" cx="679.0" cy="318.4" r="4"></circle><text class="value" x="696" y="322.392" text-anchor="start">bitECS 1.6</text><line class="line s-apecs dashed" x1="672" y1="337.392" x2="686" y2="337.392" /><text class="value" x="696" y="341.392" text-anchor="start">apecs chunks 1.2</text></svg></div>
<figcaption>
Nanoseconds per entity visit against the number of distinct traits a program uses with the
ergonomic API. apecs is the fastest ergonomic tier at every trait count — bitECS's flat
~1.5 ns is its raw array API, not an ergonomic
one. becsy steps at the fifth trait, koota at the second, and both are flat either side of
the step.
</figcaption>
</figure>
</section>
<section>
<div class="section-head">
<div class="label">Capability</div>
<h2>Iterating in key order, every frame</h2>
<p>
100 000 entities, microseconds per frame, log scale. bitECS and becsy ship no sorting, so
their honest cost is the hand-written one: copy the ids out and sort them. Every apecs
frame includes the <code>world.step()</code> a real frame pays — the dirty check is
tick-based, and a view settles only once the tick has moved past the writes that built it.
</p>
</div>
<div class="legend">
<span><i class="key"></i>keys never change</span>
<span><i class="key hollow"></i>1% of keys change per frame</span>
</div>
<figure>
<div class="frame">
<svg viewBox="0 0 824 228" role="img" aria-label="Sorted iteration, microseconds per frame"><line class="grid" x1="239.90760980319237" y1="16" x2="239.90760980319237" y2="198" /><text class="tick" x="239.90760980319237" y="214" text-anchor="middle">100</text><line class="grid" x1="383.7335716730262" y1="16" x2="383.7335716730262" y2="198" /><text class="tick" x="383.7335716730262" y="214" text-anchor="middle">1,000</text><line class="grid" x1="527.55953354286" y1="16" x2="527.55953354286" y2="198" /><text class="tick" x="527.55953354286" y="214" text-anchor="middle">10,000</text><text class="row-label is-strong" x="194" y="40" text-anchor="end">apecs orderBy + chunks</text><line class="link" x1="232.48805498628764" y1="36" x2="414.49643054845376" y2="36" /><circle class="dot is-strong" cx="232.5" cy="36.0" r="5"><title>apecs orderBy + chunks, clean: 89</title></circle><circle class="dot hollow is-strong" cx="414.5" cy="36.0" r="5"><title>apecs orderBy + chunks, drift: 1,636</title></circle><text class="value" x="824" y="40" text-anchor="end">89 → 1,636</text><text class="row-label is-strong" x="194" y="74" text-anchor="end">apecs orderBy + each</text><line class="link" x1="282.1643044701418" y1="70" x2="419.89067778916683" y2="70" /><circle class="dot is-strong" cx="282.2" cy="70.0" r="5"><title>apecs orderBy + each, clean: 197</title></circle><circle class="dot hollow is-strong" cx="419.9" cy="70.0" r="5"><title>apecs orderBy + each, drift: 1,784</title></circle><text class="value" x="824" y="74" text-anchor="end">197 → 1,784</text><text class="row-label is-strong" x="194" y="108" text-anchor="end">apecs sortBy + each</text><line class="link" x1="444.039038336162" y1="104" x2="450.5670874466594" y2="104" /><circle class="dot is-strong" cx="444.0" cy="104.0" r="5"><title>apecs sortBy + each, clean: 2,626</title></circle><circle class="dot hollow is-strong" cx="450.6" cy="104.0" r="5"><title>apecs sortBy + each, drift: 2,915</title></circle><text class="value" x="824" y="108" text-anchor="end">2,626 → 2,915</text><text class="row-label" x="194" y="142" text-anchor="end">hand-written sort</text><line class="link" x1="404.8575644059856" y1="138" x2="405.39418607509197" y2="138" /><circle class="dot" cx="405.4" cy="138.0" r="5"><title>hand-written sort, clean: 1,415</title></circle><circle class="dot hollow" cx="404.9" cy="138.0" r="5"><title>hand-written sort, drift: 1,402</title></circle><text class="value" x="824" y="142" text-anchor="end">1,415 → 1,402</text><text class="row-label" x="194" y="176" text-anchor="end">koota sort()</text><line class="link" x1="562.4822255510024" y1="172" x2="585.6070809885077" y2="172" /><circle class="dot" cx="585.6" cy="172.0" r="5"><title>koota sort(), clean: 25,328</title></circle><circle class="dot hollow" cx="562.5" cy="172.0" r="5"><title>koota sort(), drift: 17,491</title></circle><text class="value" x="824" y="176" text-anchor="end">25,328 → 17,491</text><line class="axis-line" x1="208" y1="16" x2="208" y2="198" /></svg>
</div>
<figcaption>
The distance between the two dots is what caching the order buys. koota's barely moves,
because it re-sorts either way; apecs's <code>orderBy</code> is nearly two decades apart.
</figcaption>
</figure>
<p>
<strong>Sorted iteration is free; sorting is not.</strong> On a clean frame the rows are
already in key order, so there is no gather and no side array to walk — 29.6×
faster than apecs's own <code>sortBy</code>. When 1% of keys drift, <code>orderBy</code>
extracts the keys, sorts, and applies the permutation to every column:
1.2× the hand-written sort,
which moves one id array where apecs moves five columns of real data.
</p>
<figure>
<div class="frame">
<svg viewBox="0 0 824 152" role="img" aria-label="Nanoseconds per entity to walk 100 000 rows in key order"><line class="grid" x1="365" y1="8" x2="365" y2="122" /><text class="tick" x="365" y="138" text-anchor="middle">10</text><line class="grid" x1="530" y1="8" x2="530" y2="122" /><text class="tick" x="530" y="138" text-anchor="middle">20</text><line class="grid" x1="695" y1="8" x2="695" y2="122" /><text class="tick" x="695" y="138" text-anchor="middle">30</text><text class="row-label" x="186" y="31" text-anchor="end">chunks, unsorted</text><rect class="bar" x="200" y="18" width="15.9" height="18"><title>chunks, unsorted: 0.97 ns per entity</title></rect /><text class="value" x="225.93636000013248" y="31" text-anchor="start">0.97</text><text class="row-label is-strong" x="186" y="65" text-anchor="end">orderBy + chunks</text><rect class="bar is-strong" x="200" y="52" width="15.9" height="18"><title>orderBy + chunks: 0.97 ns per entity</title></rect /><text class="value" x="225.9431250000216" y="65" text-anchor="start">0.97</text><text class="row-label" x="186" y="99" text-anchor="end">sortBy + each</text><rect class="bar" x="200" y="86" width="483.3" height="18"><title>sortBy + each: 29.29 ns per entity</title></rect /><text class="value" x="693.346874999952" y="99" text-anchor="start">29.29</text><line class="axis-line" x1="200" y1="8" x2="200" y2="122" /><text class="tick" x="200" y="138" text-anchor="middle">0</text></svg>
</div>
<figcaption>
Nanoseconds per entity, all three from the same run. <code>sortBy</code> materialises the
order into a side array and pays for the indirection on every row;
<code>orderBy</code> leaves the rows in order and pays nothing.
</figcaption>
</figure>
<div class="note">
<span class="label lab">The guarantee is per archetype</span>
Rows are in key order within each matching archetype, not across them — chunk boundaries are
archetype boundaries, and a merge cannot cross one without materialising.
<code>sortBy</code> remains the answer when the order must be total.
</div>
</section>
<section>
<div class="section-head">
<div class="label">Footprint</div>
<h2>Bytes per entity</h2>
<p>
A world of entities carrying Position + Velocity — two <code>f32</code> fields each, so 16
bytes is the payload. Everything above it is ids, masks, archetype bookkeeping and query
caches. Measured as absolute resident bytes at five world sizes and least-squares fitted;
the check that the fit works is that the hand-written baseline lands on 16.
</p>
</div>
<figure>
<div class="frame">
<svg viewBox="0 0 824 220" role="img" aria-label="Bytes per entity"><line class="grid" x1="365" y1="8" x2="365" y2="190" /><text class="tick" x="365" y="206" text-anchor="middle">100</text><line class="grid" x1="530" y1="8" x2="530" y2="190" /><text class="tick" x="530" y="206" text-anchor="middle">200</text><line class="grid" x1="695" y1="8" x2="695" y2="190" /><text class="tick" x="695" y="206" text-anchor="middle">300</text><line class="ref" x1="226.4" y1="4" x2="226.4" y2="190" /><text class="tick" x="234.4" y="14" text-anchor="start">16 B payload</text><text class="row-label" x="186" y="31" text-anchor="end">hand-written</text><rect class="bar" x="200" y="18" width="26.4" height="18"><title>hand-written: 16.0 bytes per entity</title></rect /><text class="value" x="236.4170148" y="31" text-anchor="start">16.0</text><text class="row-label is-strong" x="186" y="65" text-anchor="end">apecs</text><rect class="bar is-strong" x="200" y="52" width="62.8" height="18"><title>apecs: 38.1 bytes per entity</title></rect /><text class="value" x="272.847708" y="65" text-anchor="start">38.1</text><text class="row-label" x="186" y="99" text-anchor="end">bitECS</text><rect class="bar" x="200" y="86" width="390.5" height="18"><title>bitECS: 236.7 bytes per entity</title></rect /><text class="value" x="600.5123904" y="99" text-anchor="start">236.7</text><text class="row-label" x="186" y="133" text-anchor="end">koota</text><rect class="bar" x="200" y="120" width="484.7" height="18"><title>koota: 293.8 bytes per entity</title></rect /><text class="value" x="694.7373432" y="133" text-anchor="start">293.8</text><text class="row-label" x="186" y="167" text-anchor="end">becsy</text><rect class="bar" x="200" y="154" width="254.7" height="18"><title>becsy: 154.4 bytes per entity</title></rect /><text class="value" x="464.67979679999996" y="167" text-anchor="start">154.4</text><line class="axis-line" x1="200" y1="8" x2="200" y2="190" /><text class="tick" x="200" y="206" text-anchor="middle">0</text></svg>
</div>
<figcaption>
At a million entities: 38 MB for apecs
against 237 MB for bitECS and
294 MB for koota.
</figcaption>
</figure>
<p class="note">
<span class="label lab">Reading the fit</span>
It spans an empty world, so a fixed cost that moves between builds leaks into the slope. The
marginal bytes between the two largest sizes cannot:
16.0 for hand-written, 36.9 for apecs, 239.8 for bitECS, 304.7 for koota, 153.4 for becsy.
</p>
</section>
<section>
<div class="section-head">
<div class="label">Method</div>
<h2>What would have made this wrong</h2>
</div>
<div class="method">
<div class="mcell">
<h3>One process per library, per benchmark</h3>
<p>Four ECS libraries in one isolate make the shared iteration call sites megamorphic, and
V8 deoptimises whichever warmed up second — a larger effect than most of the differences
being measured.</p>
</div>
<div class="mcell">
<h3>A fairness census before any timing</h3>
<p>Every library must match the same entity count on every query-shaped benchmark or the
run aborts. It caught apecs's <code>Not()</code> silently ignoring all but its first
argument, and becsy's perf build silently creating only some of the entities asked for.</p>
</div>
<div class="mcell">
<h3>Minimum of 3 full runs</h3>
<p>One pass is not trustworthy on a laptop: across runs some competitor cells moved by
tens of percent while apecs's moved under 2%. Worst spread observed here:
packed_1 · koota · raw at 172%.</p>
</div>
<div class="mcell">
<h3>Production builds only</h3>
<p>apecs from its built <code>dist/</code> with <code>__DEV__</code> compiled out; becsy
from its <code>perf</code> build, whose default carries runtime validation.</p>
</div>
</div>
</section>
<section>
<div class="section-head">
<div class="label">SPEC §12.1</div>
<h2>Every measurement</h2>
<p>The full suite behind the budgets above, as vitest reported it.</p>
</div>
<div class="scroll">
<table>
<thead>
<tr><th>Benchmark</th><th>Median</th><th>Mean</th><th>ops/s</th><th>±rme</th></tr>
</thead>
<tbody>
<tr>
<td class="lead">add-remove · apecs add/remove</td>
<td>41.5655 ms</td>
<td>42.4343 ms</td>
<td>24</td>
<td>2.9%</td>
</tr>
<tr>
<td class="lead">add-remove · apecs addMany/removeMany</td>
<td>39.7341 ms</td>
<td>41.0643 ms</td>
<td>24</td>
<td>5.8%</td>
</tr>
<tr>
<td class="lead">entity-cycle · baseline</td>
<td>0.2392 ms</td>
<td>0.2383 ms</td>
<td>4,196</td>
<td>0.1%</td>
</tr>
<tr>
<td class="lead">entity-cycle · apecs spawn/despawn</td>
<td>40.6979 ms</td>
<td>40.7483 ms</td>
<td>25</td>
<td>0.4%</td>
</tr>
<tr>
<td class="lead">frag-iter · apecs each x26</td>
<td>0.1857 ms</td>
<td>0.1877 ms</td>
<td>5,329</td>
<td>0.1%</td>
</tr>
<tr>
<td class="lead">frag-iter · apecs each x1</td>
<td>0.2118 ms</td>
<td>0.2129 ms</td>
<td>4,697</td>
<td>0.1%</td>
</tr>
<tr>
<td class="lead">packed-5 · baseline</td>
<td>0.0046 ms</td>
<td>0.0046 ms</td>
<td>215,063</td>
<td>0.0%</td>
</tr>
<tr>
<td class="lead">packed-5 · apecs each</td>
<td>0.0097 ms</td>
<td>0.0097 ms</td>
<td>102,588</td>
<td>0.2%</td>
</tr>
<tr>
<td class="lead">packed-5 · apecs chunks</td>
<td>0.0050 ms</td>
<td>0.0050 ms</td>
<td>198,022</td>
<td>0.1%</td>
</tr>
<tr>
<td class="lead">random-access · baseline</td>
<td>0.0842 ms</td>
<td>0.0846 ms</td>
<td>11,823</td>
<td>0.1%</td>
</tr>
<tr>
<td class="lead">random-access · apecs get/set</td>
<td>12.4407 ms</td>
<td>12.4721 ms</td>
<td>80</td>
<td>0.3%</td>
</tr>
<tr>
<td class="lead">random-access · apecs accessor</td>
<td>3.1520 ms</td>
<td>3.1589 ms</td>
<td>317</td>
<td>0.4%</td>
</tr>
<tr>
<td class="lead">schedule-dispatch · hand-written frame (16 systems)</td>
<td>0.0000 ms</td>
<td>0.0000 ms</td>
<td>28,339,632</td>
<td>0.2%</td>
</tr>
<tr>
<td class="lead">schedule-dispatch · schedule.run (16 systems)</td>
<td>0.0000 ms</td>
<td>0.0000 ms</td>
<td>26,345,802</td>
<td>0.5%</td>
</tr>
<tr>
<td class="lead">schedule-frame · hand-written frame (8 working systems)</td>
<td>0.2023 ms</td>
<td>0.2032 ms</td>
<td>4,922</td>
<td>0.1%</td>
</tr>
<tr>
<td class="lead">schedule-frame · schedule.run (8 working systems)</td>
<td>0.2047 ms</td>
<td>0.2061 ms</td>
<td>4,852</td>
<td>0.1%</td>
</tr>
<tr>
<td class="lead">simple-iter · baseline</td>
<td>0.2595 ms</td>
<td>0.2603 ms</td>
<td>3,841</td>
<td>0.1%</td>
</tr>
<tr>
<td class="lead">simple-iter · apecs each</td>
<td>0.5595 ms</td>
<td>0.5612 ms</td>
<td>1,782</td>
<td>0.1%</td>
</tr>
<tr>
<td class="lead">simple-iter · apecs chunks</td>
<td>0.2512 ms</td>
<td>0.2527 ms</td>
<td>3,958</td>
<td>0.1%</td>
</tr>
<tr>
<td class="lead">sorted-static · apecs sorted (clean)</td>
<td>0.0000 ms</td>
<td>0.0000 ms</td>
<td>27,172,006</td>
<td>0.0%</td>
</tr>
<tr>
<td class="lead">sorted-static · apecs sorted (rebuild)</td>
<td>0.7181 ms</td>
<td>0.7196 ms</td>
<td>1,390</td>
<td>0.1%</td>
</tr>
<tr>
<td class="lead">sorted-drift · apecs sorted (1% drift)</td>
<td>0.5092 ms</td>
<td>1.1373 ms</td>
<td>879</td>
<td>11.5%</td>
</tr>
<tr>
<td class="lead">sorted-drift · apecs drift writes</td>
<td>0.0757 ms</td>
<td>0.0760 ms</td>
<td>13,163</td>
<td>0.0%</td>
</tr>
<tr>
<td class="lead">ordered-iter · apecs ordered chunks</td>
<td>0.0966 ms</td>
<td>0.0970 ms</td>
<td>10,308</td>
<td>0.1%</td>
</tr>
<tr>
<td class="lead">ordered-iter · apecs sorted each</td>
<td>2.9294 ms</td>
<td>2.9734 ms</td>
<td>336</td>
<td>0.7%</td>
</tr>
<tr>
<td class="lead">ordered-iter · apecs chunks</td>
<td>0.0966 ms</td>
<td>0.0968 ms</td>
<td>10,334</td>
<td>0.1%</td>
</tr>
</tbody>
</table>
</div>
</section>
</main>
<footer>
<div class="label">Reproducing</div>
<pre>npm run bench:ci # SPEC §12.1 suite and budgets
cd bench/compare && node run.mjs # census, timings, memory
node capability.mjs apecs-ordered-chunks static
node ../../scripts/bench-report-html.mjs</pre>
<p class="note">
apecs 0.1.0 (dist) · bitECS 0.4.0 · koota 0.6.6 · becsy 0.15.5 (perf build). Rendered from
<code>bench/results.json</code> and <code>bench/compare/results.json</code>; every figure on
this page is read from a results file, none is typed in.
</p>
</footer>
</div>
</body>
</html>