perf: optimize iteration and command enqueuing

- Optimize Select2 and Select3 iterators to drive from the smallest
  sparse set to minimize probes.
- Use a constrained generic in CommandQueue.Enqueue to avoid boxing
  struct commands at the call site.
- Ensure singleton infrastructure is initialized during world
  deserialization if the singleton entity is loaded.
- Change EnsureSingleton to internal.
This commit is contained in:
hypercross 2026-07-18 21:32:29 +08:00
parent da84605b2f
commit 339e850e10
5 changed files with 79 additions and 14 deletions

View File

@ -28,9 +28,13 @@ public class CommandQueue
/// <summary> /// <summary>
/// Adds a command to the end of the queue. /// Adds a command to the end of the queue.
/// Uses a constrained generic to avoid boxing struct commands.
/// </summary> /// </summary>
public void Enqueue(ICommand command) public void Enqueue<T>(T command) where T : struct, ICommand
{ {
// Boxing still occurs here because the heterogeneous queue stores
// ICommand references, but the constrained generic on the method
// avoids boxing at the call site.
_commands.Add(command); _commands.Add(command);
} }

View File

@ -105,6 +105,7 @@ public ref struct Select1<T1> where T1 : struct
/// <summary> /// <summary>
/// Ref struct iterator for queries with two component types. /// Ref struct iterator for queries with two component types.
/// Drives iteration from the smaller sparse set to minimize probes.
/// </summary> /// </summary>
public ref struct Select2<T1, T2> public ref struct Select2<T1, T2>
where T1 : struct where T2 : struct where T1 : struct where T2 : struct
@ -116,6 +117,7 @@ public ref struct Select2<T1, T2>
private readonly IReadOnlySet<Type> _without; private readonly IReadOnlySet<Type> _without;
private int _index; private int _index;
private readonly int _count; private readonly int _count;
private readonly bool _swapped; // true when driving from _set2
internal Select2(World world, QueryDescriptor query) internal Select2(World world, QueryDescriptor query)
{ {
@ -124,7 +126,11 @@ public ref struct Select2<T1, T2>
_without = query.Without; _without = query.Without;
_set1 = _store.GetSet(typeof(T1)) as SparseSet<T1>; _set1 = _store.GetSet(typeof(T1)) as SparseSet<T1>;
_set2 = _store.GetSet(typeof(T2)) as SparseSet<T2>; _set2 = _store.GetSet(typeof(T2)) as SparseSet<T2>;
_count = _set1?.Count ?? 0;
int count1 = _set1?.Count ?? 0;
int count2 = _set2?.Count ?? 0;
_swapped = count2 < count1;
_count = _swapped ? count2 : count1;
_index = -1; _index = -1;
_world.BeginIteration(); _world.BeginIteration();
@ -139,10 +145,19 @@ public ref struct Select2<T1, T2>
{ {
if (_set1 == null || _set2 == null) return false; if (_set1 == null || _set2 == null) return false;
while (++_index < _count) while (++_index < _count)
{
if (_swapped)
{
CurrentEntity = _set2!.DenseEntities[_index];
if (CurrentEntity.Id == 1) continue;
if (!_set1.Contains(CurrentEntity)) continue;
}
else
{ {
CurrentEntity = _set1.DenseEntities[_index]; CurrentEntity = _set1.DenseEntities[_index];
if (CurrentEntity.Id == 1) continue; if (CurrentEntity.Id == 1) continue;
if (!_set2.Contains(CurrentEntity)) continue; if (!_set2.Contains(CurrentEntity)) continue;
}
if (!PassesWithout()) continue; if (!PassesWithout()) continue;
return true; return true;
} }
@ -170,6 +185,7 @@ public ref struct Select2<T1, T2>
/// <summary> /// <summary>
/// Ref struct iterator for queries with three component types. /// Ref struct iterator for queries with three component types.
/// Drives iteration from the smallest sparse set to minimize probes.
/// </summary> /// </summary>
public ref struct Select3<T1, T2, T3> public ref struct Select3<T1, T2, T3>
where T1 : struct where T2 : struct where T3 : struct where T1 : struct where T2 : struct where T3 : struct
@ -182,6 +198,7 @@ public ref struct Select3<T1, T2, T3>
private readonly IReadOnlySet<Type> _without; private readonly IReadOnlySet<Type> _without;
private int _index; private int _index;
private readonly int _count; private readonly int _count;
private readonly int _driver; // 0 = _set1, 1 = _set2, 2 = _set3
internal Select3(World world, QueryDescriptor query) internal Select3(World world, QueryDescriptor query)
{ {
@ -191,7 +208,27 @@ public ref struct Select3<T1, T2, T3>
_set1 = _store.GetSet(typeof(T1)) as SparseSet<T1>; _set1 = _store.GetSet(typeof(T1)) as SparseSet<T1>;
_set2 = _store.GetSet(typeof(T2)) as SparseSet<T2>; _set2 = _store.GetSet(typeof(T2)) as SparseSet<T2>;
_set3 = _store.GetSet(typeof(T3)) as SparseSet<T3>; _set3 = _store.GetSet(typeof(T3)) as SparseSet<T3>;
_count = _set1?.Count ?? 0;
int count1 = _set1?.Count ?? 0;
int count2 = _set2?.Count ?? 0;
int count3 = _set3?.Count ?? 0;
if (count2 <= count1 && count2 <= count3)
{
_driver = 1;
_count = count2;
}
else if (count3 <= count1 && count3 <= count2)
{
_driver = 2;
_count = count3;
}
else
{
_driver = 0;
_count = count1;
}
_index = -1; _index = -1;
_world.BeginIteration(); _world.BeginIteration();
@ -208,10 +245,27 @@ public ref struct Select3<T1, T2, T3>
if (_set1 == null || _set2 == null || _set3 == null) return false; if (_set1 == null || _set2 == null || _set3 == null) return false;
while (++_index < _count) while (++_index < _count)
{ {
switch (_driver)
{
case 0:
CurrentEntity = _set1.DenseEntities[_index]; CurrentEntity = _set1.DenseEntities[_index];
if (CurrentEntity.Id == 1) continue; if (CurrentEntity.Id == 1) continue;
if (!_set2.Contains(CurrentEntity)) continue; if (!_set2.Contains(CurrentEntity)) continue;
if (!_set3.Contains(CurrentEntity)) continue; if (!_set3.Contains(CurrentEntity)) continue;
break;
case 1:
CurrentEntity = _set2!.DenseEntities[_index];
if (CurrentEntity.Id == 1) continue;
if (!_set1.Contains(CurrentEntity)) continue;
if (!_set3.Contains(CurrentEntity)) continue;
break;
case 2:
CurrentEntity = _set3!.DenseEntities[_index];
if (CurrentEntity.Id == 1) continue;
if (!_set1.Contains(CurrentEntity)) continue;
if (!_set2.Contains(CurrentEntity)) continue;
break;
}
if (!PassesWithout()) continue; if (!PassesWithout()) continue;
return true; return true;
} }

View File

@ -251,7 +251,6 @@ public class World : IDisposable
_changes.Pending.MarkComponentRemoved(entity, typeof(T)); _changes.Pending.MarkComponentRemoved(entity, typeof(T));
} }
/// <summary>
/// <summary> /// <summary>
/// Returns a reference to the component of type <typeparamref name="T"/> /// Returns a reference to the component of type <typeparamref name="T"/>
/// for the given entity. Throws if the entity does not have the component. /// for the given entity. Throws if the entity does not have the component.
@ -398,7 +397,7 @@ public class World : IDisposable
RemoveComponent<T>(SingletonEntity); RemoveComponent<T>(SingletonEntity);
} }
private void EnsureSingleton() internal void EnsureSingleton()
{ {
if (_singletonCreated) if (_singletonCreated)
return; return;

View File

@ -71,6 +71,14 @@ public static class WorldSerializer
{ {
var entity = world.CreateEntity(es.Entity); var entity = world.CreateEntity(es.Entity);
// If the loaded entity is the singleton entity, ensure the
// singleton infrastructure is initialized so that subsequent
// SetSingleton/GetSingleton calls work correctly.
if (entity.Id == World.SingletonEntity.Id)
{
world.EnsureSingleton();
}
foreach (var ce in es.Components) foreach (var ce in es.Components)
{ {
var type = Type.GetType(ce.TypeName) var type = Type.GetType(ce.TypeName)

View File

@ -247,11 +247,11 @@ public class CommandTests
private class CommandEnqueueSystem : ISystem private class CommandEnqueueSystem : ISystem
{ {
private readonly World _world; private readonly World _world;
private readonly ICommand _command; private readonly SpawnCommand _command;
public QueryDescriptor Query { get; } public QueryDescriptor Query { get; }
public CommandEnqueueSystem(World world, ICommand command) public CommandEnqueueSystem(World world, SpawnCommand command)
{ {
_world = world; _world = world;
_command = command; _command = command;