Skip to content

Commit 2159ff6

Browse files
release v3.1.0
- [feat] Count and IsEmpty properties on GroupResultEnumerator for O(k) query counting - [feat] CountOf<T>() and AnyOf<T>() convenience methods on Context (1-9 type params) - [feat] Tag.SetBits/ClearBits bulk operations with params ReadOnlySpan<int> - [feat] Tag.ContainsAll using SIMD AndNot for faster archetype matching - [opt] GetFragmentationStats is now lock-free (removed per-frame lock + dictionary iteration) - [opt] Update() reuses cached task buffer instead of per-bucket ArrayPool Rent/Return - [opt] ExecuteFrequency cached as local variable to avoid repeated interface property access - [opt] AggressiveInlining on GroupOf, CountOf, AnyOf methods - [opt] All archetype iterations use index-based List traversal instead of Dictionary enumeration - [fix] Avx2.TestZ corrected to Avx.TestZ (static member access via derived type) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 50ff46e commit 2159ff6

31 files changed

Lines changed: 895 additions & 172 deletions

EasyEcs.Core/Archetype.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ internal class Archetype
1515
{
1616
public Tag ComponentMask;
1717
public int[] EntityIds;
18-
public int Count; // Total slots (including tombstones)
19-
public int AliveCount; // Living entities only
18+
public int Count; // Total slots (including tombstones)
19+
public int AliveCount; // Living entities only
2020

2121
// Free list for O(1) tombstone reuse (stack-based)
2222
private int[] _freeSlots;
@@ -96,6 +96,7 @@ public void Remove(int entityId)
9696
{
9797
Array.Resize(ref _freeSlots, _freeSlots.Length * 2);
9898
}
99+
99100
_freeSlots[_freeCount++] = idx;
100101

101102
// Note: Auto-compaction is disabled to avoid unpredictable performance spikes mid-frame.
@@ -122,7 +123,7 @@ public void Compact()
122123
if (entityId != Tombstone)
123124
{
124125
EntityIds[writeIdx] = entityId;
125-
_entityToIndex[entityId] = writeIdx; // Rebuild reverse lookup
126+
_entityToIndex[entityId] = writeIdx; // Rebuild reverse lookup
126127
writeIdx++;
127128
}
128129
}
@@ -149,4 +150,4 @@ public float GetFragmentation()
149150
{
150151
return Count > 0 ? 1.0f - (float)AliveCount / Count : 0f;
151152
}
152-
}
153+
}

EasyEcs.Core/Components/ComponentRef.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ namespace EasyEcs.Core.Components;
1212
{
1313
// Pack entity ID (lower 32 bits) + version (upper 32 bits)
1414
private readonly long _packed;
15-
private readonly ushort _componentIndex; // Supports up to 65536 component types
15+
private readonly ushort _componentIndex; // Supports up to 65536 component types
1616
private readonly Context _context;
1717

1818
public ComponentRef(int entityId, int version, ushort componentIndex, Context context)

EasyEcs.Core/Components/Tag.cs

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,16 @@ public void SetBit(int bitIndex)
4646
}
4747
}
4848

49+
/// <summary>
50+
/// Set multiple bits at once.
51+
/// </summary>
52+
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
53+
public void SetBits(params ReadOnlySpan<int> bitIndices)
54+
{
55+
for (int i = 0; i < bitIndices.Length; i++)
56+
SetBit(bitIndices[i]);
57+
}
58+
4959
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
5060
public void ClearBit(int bitIndex)
5161
{
@@ -64,6 +74,16 @@ public void ClearBit(int bitIndex)
6474
}
6575
}
6676

77+
/// <summary>
78+
/// Clear multiple bits at once.
79+
/// </summary>
80+
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
81+
public void ClearBits(params ReadOnlySpan<int> bitIndices)
82+
{
83+
for (int i = 0; i < bitIndices.Length; i++)
84+
ClearBit(bitIndices[i]);
85+
}
86+
6787
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
6888
public readonly bool HasBit(int bitIndex)
6989
{
@@ -85,6 +105,28 @@ public readonly bool HasBit(int bitIndex)
85105
return (Unsafe.Add(ref longValue, bitOffset >> 6) & (1L << (bitOffset & 63))) != 0;
86106
}
87107

108+
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
109+
public readonly bool ContainsAll(in Tag query)
110+
{
111+
// query & ~this == 0 means this contains all bits of query
112+
// Vector256.AndNot(a, b) = a & ~b → maps to x86 VPANDN, ARM BIC
113+
if (!IsVectorZero(Vector256.AndNot(query._bits, _bits)))
114+
return false;
115+
116+
int queryLen = query._overflow?.Length ?? 0;
117+
if (queryLen == 0) return true;
118+
119+
int thisLen = _overflow?.Length ?? 0;
120+
for (int i = 0; i < queryLen; i++)
121+
{
122+
var thisVec = i < thisLen ? _overflow![i] : Vector256<long>.Zero;
123+
if (!IsVectorZero(Vector256.AndNot(query._overflow![i], thisVec)))
124+
return false;
125+
}
126+
127+
return true;
128+
}
129+
88130
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
89131
public static Tag operator &(in Tag a, in Tag b)
90132
{
@@ -207,10 +249,10 @@ public readonly bool Equals(in Tag other)
207249
[MethodImpl(MethodImplOptions.AggressiveInlining)]
208250
private static bool IsVectorZero(Vector256<long> vec)
209251
{
210-
// x86 AVX2: Single TestZ instruction (~1 cycle)
211-
if (Avx2.IsSupported)
252+
// x86 AVX: Single TestZ instruction (~1 cycle)
253+
if (Avx.IsSupported)
212254
{
213-
return Avx2.TestZ(vec, vec);
255+
return Avx.TestZ(vec, vec);
214256
}
215257

216258
// ARM NEON: Vector256 is emulated on ARM, so work with 128-bit halves (~2 cycles)

EasyEcs.Core/Components/TagRegistry.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ private interface ITypeBitIndex
1818
private class TypeBitIndex<T> : ITypeBitIndex
1919
{
2020
// Volatile fields for cross-thread visibility
21-
internal volatile ushort BitIndex; // Supports up to 65536 component types
21+
internal volatile ushort BitIndex; // Supports up to 65536 component types
2222
internal volatile bool IsRegistered;
2323
public static readonly TypeBitIndex<T> Instance = new();
2424

@@ -116,7 +116,7 @@ public static ushort GetOrRegisterTag<T>() where T : struct
116116

117117
ushort bitIndex = (ushort)_tagCount;
118118
instance.BitIndex = bitIndex;
119-
instance.IsRegistered = true; // Volatile write ensures visibility
119+
instance.IsRegistered = true; // Volatile write ensures visibility
120120
_tagCount++;
121121
Tags.Add(instance);
122122

EasyEcs.Core/Context.Archetypes.cs

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Collections.Concurrent;
22
using System.Collections.Generic;
3+
using System.Diagnostics.CodeAnalysis;
34
using System.Runtime.CompilerServices;
45
using EasyEcs.Core.Components;
56

@@ -10,6 +11,9 @@ public partial class Context
1011
// Archetype storage: Tag -> Archetype
1112
internal readonly Dictionary<Tag, Archetype> Archetypes = new();
1213

14+
// Flat list of all archetypes for lock-free iteration (only grows, never shrinks except on Dispose)
15+
private readonly List<Archetype> _allArchetypes = new();
16+
1317
// Query cache: QueryTag -> List of matching archetypes (O(1) lookup after first query)
1418
// Using ConcurrentDictionary for lock-free reads on cache hits
1519
private readonly ConcurrentDictionary<Tag, List<Archetype>> _queryCache = new();
@@ -30,6 +34,7 @@ internal Archetype GetOrCreateArchetype(in Tag componentMask)
3034
{
3135
archetype = new Archetype(in componentMask, initialCapacity: 1024);
3236
Archetypes[componentMask] = archetype;
37+
_allArchetypes.Add(archetype);
3338

3439
// Incrementally update query cache: add new archetype to matching queries
3540
// This is smarter than clearing the entire cache
@@ -39,7 +44,7 @@ internal Archetype GetOrCreateArchetype(in Tag componentMask)
3944
var cachedList = kvp.Value;
4045

4146
// Check if new archetype matches this cached query
42-
if ((componentMask & queryTag) == queryTag)
47+
if (componentMask.ContainsAll(in queryTag))
4348
{
4449
cachedList.Add(archetype);
4550
}
@@ -74,13 +79,11 @@ internal List<Archetype> GetMatchingArchetypes(in Tag queryTag)
7479
// Build list of matching archetypes
7580
var matching = new List<Archetype>(16);
7681

77-
// Use direct enumeration to avoid allocating Dictionary.Values collection
78-
foreach (var kvp in Archetypes)
82+
for (int i = 0; i < _allArchetypes.Count; i++)
7983
{
80-
var archetype = kvp.Value;
81-
// SIMD-accelerated bitwise AND
82-
// An archetype matches if it contains all required components
83-
if ((archetype.ComponentMask & queryTag) == queryTag)
84+
var archetype = _allArchetypes[i];
85+
// SIMD-accelerated ContainsAll check
86+
if (archetype.ComponentMask.ContainsAll(in queryTag))
8487
{
8588
matching.Add(archetype);
8689
}
@@ -100,10 +103,9 @@ public void CompactArchetypes()
100103
{
101104
lock (_structuralLock)
102105
{
103-
// Use direct enumeration to avoid allocating Dictionary.Values collection
104-
foreach (var kvp in Archetypes)
106+
for (int i = 0; i < _allArchetypes.Count; i++)
105107
{
106-
var archetype = kvp.Value;
108+
var archetype = _allArchetypes[i];
107109
if (archetype.AliveCount < archetype.Count)
108110
{
109111
archetype.Compact();
@@ -115,23 +117,21 @@ public void CompactArchetypes()
115117
/// <summary>
116118
/// Get archetype fragmentation statistics for monitoring.
117119
/// </summary>
120+
[SuppressMessage("ReSharper", "InconsistentlySynchronizedField")]
118121
public (int TotalSlots, int AliveEntities, float FragmentationRatio) GetFragmentationStats()
119122
{
120123
int totalSlots = 0;
121124
int aliveEntities = 0;
122125

123-
lock (_structuralLock)
126+
// Lock-free: _allArchetypes only grows, index-based for loop is safe during concurrent append.
127+
// Individual int reads (Count, AliveCount) are atomic on all .NET platforms.
128+
for (int i = 0; i < _allArchetypes.Count; i++)
124129
{
125-
// Use direct enumeration to avoid allocating Dictionary.Values collection
126-
foreach (var kvp in Archetypes)
127-
{
128-
var archetype = kvp.Value;
129-
totalSlots += archetype.Count;
130-
aliveEntities += archetype.AliveCount;
131-
}
130+
totalSlots += _allArchetypes[i].Count;
131+
aliveEntities += _allArchetypes[i].AliveCount;
132132
}
133133

134134
float fragmentation = totalSlots > 0 ? 1.0f - (float)aliveEntities / totalSlots : 0f;
135135
return (totalSlots, aliveEntities, fragmentation);
136136
}
137-
}
137+
}

EasyEcs.Core/Context.Components.cs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,8 @@ public void RemoveComponent<T>(Entity entity) where T : struct, IComponent
191191
/// Thread-safe. More efficient than calling AddComponent multiple times.
192192
/// </summary>
193193
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
194-
public (ComponentRef<T1>, ComponentRef<T2>, ComponentRef<T3>, ComponentRef<T4>) AddComponents<T1, T2, T3, T4>(Entity entity)
194+
public (ComponentRef<T1>, ComponentRef<T2>, ComponentRef<T3>, ComponentRef<T4>) AddComponents<T1, T2, T3, T4>(
195+
Entity entity)
195196
where T1 : struct, IComponent
196197
where T2 : struct, IComponent
197198
where T3 : struct, IComponent
@@ -251,7 +252,8 @@ public void RemoveComponent<T>(Entity entity) where T : struct, IComponent
251252
/// Thread-safe. More efficient than calling AddComponent multiple times.
252253
/// </summary>
253254
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
254-
public (ComponentRef<T1>, ComponentRef<T2>, ComponentRef<T3>, ComponentRef<T4>, ComponentRef<T5>) AddComponents<T1, T2, T3, T4, T5>(Entity entity)
255+
public (ComponentRef<T1>, ComponentRef<T2>, ComponentRef<T3>, ComponentRef<T4>, ComponentRef<T5>) AddComponents<T1,
256+
T2, T3, T4, T5>(Entity entity)
255257
where T1 : struct, IComponent
256258
where T2 : struct, IComponent
257259
where T3 : struct, IComponent
@@ -318,7 +320,8 @@ public void RemoveComponent<T>(Entity entity) where T : struct, IComponent
318320
/// Thread-safe. More efficient than calling AddComponent multiple times.
319321
/// </summary>
320322
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
321-
public (ComponentRef<T1>, ComponentRef<T2>, ComponentRef<T3>, ComponentRef<T4>, ComponentRef<T5>, ComponentRef<T6>) AddComponents<T1, T2, T3, T4, T5, T6>(Entity entity)
323+
public (ComponentRef<T1>, ComponentRef<T2>, ComponentRef<T3>, ComponentRef<T4>, ComponentRef<T5>, ComponentRef<T6>)
324+
AddComponents<T1, T2, T3, T4, T5, T6>(Entity entity)
322325
where T1 : struct, IComponent
323326
where T2 : struct, IComponent
324327
where T3 : struct, IComponent
@@ -392,7 +395,8 @@ public void RemoveComponent<T>(Entity entity) where T : struct, IComponent
392395
/// Thread-safe. More efficient than calling AddComponent multiple times.
393396
/// </summary>
394397
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
395-
public (ComponentRef<T1>, ComponentRef<T2>, ComponentRef<T3>, ComponentRef<T4>, ComponentRef<T5>, ComponentRef<T6>, ComponentRef<T7>) AddComponents<T1, T2, T3, T4, T5, T6, T7>(Entity entity)
398+
public (ComponentRef<T1>, ComponentRef<T2>, ComponentRef<T3>, ComponentRef<T4>, ComponentRef<T5>, ComponentRef<T6>,
399+
ComponentRef<T7>) AddComponents<T1, T2, T3, T4, T5, T6, T7>(Entity entity)
396400
where T1 : struct, IComponent
397401
where T2 : struct, IComponent
398402
where T3 : struct, IComponent
@@ -473,7 +477,8 @@ public void RemoveComponent<T>(Entity entity) where T : struct, IComponent
473477
/// Thread-safe. More efficient than calling AddComponent multiple times.
474478
/// </summary>
475479
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
476-
public (ComponentRef<T1>, ComponentRef<T2>, ComponentRef<T3>, ComponentRef<T4>, ComponentRef<T5>, ComponentRef<T6>, ComponentRef<T7>, ComponentRef<T8>) AddComponents<T1, T2, T3, T4, T5, T6, T7, T8>(Entity entity)
480+
public (ComponentRef<T1>, ComponentRef<T2>, ComponentRef<T3>, ComponentRef<T4>, ComponentRef<T5>, ComponentRef<T6>,
481+
ComponentRef<T7>, ComponentRef<T8>) AddComponents<T1, T2, T3, T4, T5, T6, T7, T8>(Entity entity)
477482
where T1 : struct, IComponent
478483
where T2 : struct, IComponent
479484
where T3 : struct, IComponent
@@ -561,7 +566,9 @@ public void RemoveComponent<T>(Entity entity) where T : struct, IComponent
561566
/// Thread-safe. More efficient than calling AddComponent multiple times.
562567
/// </summary>
563568
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
564-
public (ComponentRef<T1>, ComponentRef<T2>, ComponentRef<T3>, ComponentRef<T4>, ComponentRef<T5>, ComponentRef<T6>, ComponentRef<T7>, ComponentRef<T8>, ComponentRef<T9>) AddComponents<T1, T2, T3, T4, T5, T6, T7, T8, T9>(Entity entity)
569+
public (ComponentRef<T1>, ComponentRef<T2>, ComponentRef<T3>, ComponentRef<T4>, ComponentRef<T5>, ComponentRef<T6>,
570+
ComponentRef<T7>, ComponentRef<T8>, ComponentRef<T9>) AddComponents<T1, T2, T3, T4, T5, T6, T7, T8, T9>(
571+
Entity entity)
565572
where T1 : struct, IComponent
566573
where T2 : struct, IComponent
567574
where T3 : struct, IComponent
@@ -1155,4 +1162,4 @@ internal T[] EnsureComponentArrayInitialized<T>(ushort componentIdx) where T : s
11551162
return (T[])Components[componentIdx];
11561163
}
11571164
}
1158-
}
1165+
}

0 commit comments

Comments
 (0)