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>
/// Adds a command to the end of the queue.
/// Uses a constrained generic to avoid boxing struct commands.
/// </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);
}

View File

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

View File

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

View File

@ -71,6 +71,14 @@ public static class WorldSerializer
{
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)
{
var type = Type.GetType(ce.TypeName)

View File

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