From ba91759365acb414edb34bd772686da3010f86a7 Mon Sep 17 00:00:00 2001 From: Kirill Bazhaykin Date: Mon, 29 Dec 2025 17:19:01 +0300 Subject: [PATCH 01/11] 0.4.3.3 actor dictionary --- .../Common/Visibles/VisibleSystem.cs | 1 - .../Generate/GenerateTerrainHandler.cs | 30 +-- .../Terrains/TerrainDrawSystem.cs | 1 - .../Terrains/TerrainInstaller.cs | 9 +- .../Terrains/TerrainSpriteAtlas.cs | 1 - .../Utils/PointExtensions.cs | 5 +- .../Actors/ActorComponentShould.cs | 1 + .../Actors/ActorConstraintShould.cs | 1 + src/Hexecs.Tests/Actors/ActorContextShould.cs | 2 + .../Actors/ActorDictionaryShould.cs | 172 ------------------ src/Hexecs.Tests/Actors/ActorFilter1Should.cs | 1 + src/Hexecs.Tests/Actors/ActorFilter2Should.cs | 1 + src/Hexecs.Tests/Actors/ActorFilter3Should.cs | 1 + src/Hexecs.Tests/Actors/ActorListShould.cs | 1 + src/Hexecs.Tests/Actors/ActorMarshalShould.cs | 1 + .../Actors/ActorRelationShould.cs | 1 + src/Hexecs.Tests/Actors/ActorShould.cs | 2 + src/Hexecs.Tests/Actors/ActorSystemShould.cs | 1 + src/Hexecs.Tests/Actors/ActorTestFixture.cs | 2 + .../Assets/AssetConstraintShould.cs | 1 + src/Hexecs.Tests/Assets/AssetFilter1Should.cs | 1 + src/Hexecs.Tests/Assets/AssetFilter2Should.cs | 1 + src/Hexecs.Tests/Assets/AssetFilter3Should.cs | 1 + src/Hexecs.Tests/Assets/AssetTestFixture.cs | 1 + src/Hexecs.Tests/Hexecs.Tests.csproj | 9 + .../Mocks/{ => ActorComponents}/Attack.cs | 2 +- .../{ => ActorComponents}/AttackBuilder.cs | 3 +- .../Mocks/{ => ActorComponents}/Defence.cs | 2 +- .../{ => ActorComponents}/DefenceBuilder.cs | 3 +- .../{ => ActorComponents}/Description.cs | 2 +- .../DisposableComponent.cs | 2 +- .../NonExistentComponent.cs | 2 +- .../Mocks/{ => ActorComponents}/Speed.cs | 2 +- .../Mocks/{ => Assets}/BuildingAsset.cs | 2 +- .../Mocks/{ => Assets}/CarAsset.cs | 2 +- .../Mocks/{ => Assets}/DecisionAsset.cs | 2 +- .../Mocks/{ => Assets}/NonExistentAsset.cs | 2 +- .../Mocks/{ => Assets}/SubjectAsset.cs | 2 +- .../Mocks/{ => Assets}/UnitAsset.cs | 2 +- src/Hexecs/Actors/ActorDictionary.cs | 84 ++++----- .../ActorComponentPool.Dictionary.cs | 2 +- src/Hexecs/Assets/AssetContext.Entry.cs | 3 +- 42 files changed, 115 insertions(+), 252 deletions(-) delete mode 100644 src/Hexecs.Tests/Actors/ActorDictionaryShould.cs rename src/Hexecs.Tests/Mocks/{ => ActorComponents}/Attack.cs (58%) rename src/Hexecs.Tests/Mocks/{ => ActorComponents}/AttackBuilder.cs (77%) rename src/Hexecs.Tests/Mocks/{ => ActorComponents}/Defence.cs (58%) rename src/Hexecs.Tests/Mocks/{ => ActorComponents}/DefenceBuilder.cs (77%) rename src/Hexecs.Tests/Mocks/{ => ActorComponents}/Description.cs (59%) rename src/Hexecs.Tests/Mocks/{ => ActorComponents}/DisposableComponent.cs (75%) rename src/Hexecs.Tests/Mocks/{ => ActorComponents}/NonExistentComponent.cs (69%) rename src/Hexecs.Tests/Mocks/{ => ActorComponents}/Speed.cs (56%) rename src/Hexecs.Tests/Mocks/{ => Assets}/BuildingAsset.cs (68%) rename src/Hexecs.Tests/Mocks/{ => Assets}/CarAsset.cs (87%) rename src/Hexecs.Tests/Mocks/{ => Assets}/DecisionAsset.cs (82%) rename src/Hexecs.Tests/Mocks/{ => Assets}/NonExistentAsset.cs (69%) rename src/Hexecs.Tests/Mocks/{ => Assets}/SubjectAsset.cs (68%) rename src/Hexecs.Tests/Mocks/{ => Assets}/UnitAsset.cs (88%) diff --git a/src/Hexecs.Benchmarks.City/Common/Visibles/VisibleSystem.cs b/src/Hexecs.Benchmarks.City/Common/Visibles/VisibleSystem.cs index 32cc10e..9d8e5b4 100644 --- a/src/Hexecs.Benchmarks.City/Common/Visibles/VisibleSystem.cs +++ b/src/Hexecs.Benchmarks.City/Common/Visibles/VisibleSystem.cs @@ -2,7 +2,6 @@ using Hexecs.Benchmarks.Map.Common.Positions; using Hexecs.Benchmarks.Map.Terrains; using Hexecs.Benchmarks.Map.Utils; -using Hexecs.Benchmarks.Map.Utils.Sprites; using Hexecs.Threading; using Hexecs.Worlds; diff --git a/src/Hexecs.Benchmarks.City/Terrains/Commands/Generate/GenerateTerrainHandler.cs b/src/Hexecs.Benchmarks.City/Terrains/Commands/Generate/GenerateTerrainHandler.cs index 39f3d6b..34a4803 100644 --- a/src/Hexecs.Benchmarks.City/Terrains/Commands/Generate/GenerateTerrainHandler.cs +++ b/src/Hexecs.Benchmarks.City/Terrains/Commands/Generate/GenerateTerrainHandler.cs @@ -1,6 +1,8 @@ using Hexecs.Actors.Pipelines; +using Hexecs.Benchmarks.Map.Common.Positions; using Hexecs.Benchmarks.Map.Terrains.Assets; using Hexecs.Benchmarks.Map.Terrains.ValueTypes; +using Hexecs.Benchmarks.Map.Utils; using Hexecs.Pipelines; namespace Hexecs.Benchmarks.Map.Terrains.Commands.Generate; @@ -9,7 +11,9 @@ internal sealed class GenerateTerrainHandler : ActorCommandHandler 45 and < 55) // river { - // river - > 45 and < 55 => Context.BuildActor(river, args - .Set(nameof(Terrain.Elevation), Elevation.FromValue(-10)) - .Set(nameof(Terrain.Moisture), Moisture.FromValue(35))), - // urban concrete - < 10 when y < 10 => Context.BuildActor(urbanConcrete, args), - // just ground - _ => Context.BuildActor(ground, args) - }; + Context.BuildActor(river, + args.Set(nameof(Terrain.Elevation), Elevation.FromValue(-10)) + .Set(nameof(Terrain.Moisture), Moisture.FromValue(35))); + } + else if (x < 10 && y < 10) // urban concrete + { + Context.BuildActor(urbanConcrete, args); + } + + else // just ground + { + Context.BuildActor(ground, args); + } } } diff --git a/src/Hexecs.Benchmarks.City/Terrains/TerrainDrawSystem.cs b/src/Hexecs.Benchmarks.City/Terrains/TerrainDrawSystem.cs index e763f7e..6cbda7e 100644 --- a/src/Hexecs.Benchmarks.City/Terrains/TerrainDrawSystem.cs +++ b/src/Hexecs.Benchmarks.City/Terrains/TerrainDrawSystem.cs @@ -2,7 +2,6 @@ using Hexecs.Benchmarks.Map.Common.Positions; using Hexecs.Benchmarks.Map.Common.Visibles; using Hexecs.Benchmarks.Map.Utils; -using Hexecs.Benchmarks.Map.Utils.Sprites; using Hexecs.Worlds; using Microsoft.Xna.Framework.Graphics; diff --git a/src/Hexecs.Benchmarks.City/Terrains/TerrainInstaller.cs b/src/Hexecs.Benchmarks.City/Terrains/TerrainInstaller.cs index a692019..f7e7967 100644 --- a/src/Hexecs.Benchmarks.City/Terrains/TerrainInstaller.cs +++ b/src/Hexecs.Benchmarks.City/Terrains/TerrainInstaller.cs @@ -1,10 +1,10 @@ +using Hexecs.Benchmarks.Map.Common.Positions; using Hexecs.Benchmarks.Map.Terrains.Assets; using Hexecs.Benchmarks.Map.Terrains.Commands.Generate; using Hexecs.Configurations; using Hexecs.Dependencies; using Hexecs.Worlds; using Microsoft.Xna.Framework.Content; -using Microsoft.Xna.Framework.Graphics; namespace Hexecs.Benchmarks.Map.Terrains; @@ -23,7 +23,7 @@ public static ActorContextBuilder AddTerrain(this ActorContextBuilder builder) builder.CreateCommandHandler(); builder.CreateDrawSystem(); - + return builder; } @@ -32,6 +32,11 @@ public static WorldBuilder UseTerrain(this WorldBuilder builder) builder .UseAddAssetSource(new TerrainAssetSource()); + builder + .UseScoped(ctx => new ActorDictionary( + context: ctx.GetRequiredService(), + keyExtractor: terrain => terrain.Grid)); + builder .UseSingleton(ctx => new TerrainSpriteAtlas( contentManager: ctx.GetRequiredService(), diff --git a/src/Hexecs.Benchmarks.City/Terrains/TerrainSpriteAtlas.cs b/src/Hexecs.Benchmarks.City/Terrains/TerrainSpriteAtlas.cs index d5a6998..fce1f14 100644 --- a/src/Hexecs.Benchmarks.City/Terrains/TerrainSpriteAtlas.cs +++ b/src/Hexecs.Benchmarks.City/Terrains/TerrainSpriteAtlas.cs @@ -1,7 +1,6 @@ using Hexecs.Benchmarks.Map.Terrains.ValueTypes; using Hexecs.Benchmarks.Map.Utils.Sprites; using Microsoft.Xna.Framework.Content; -using Microsoft.Xna.Framework.Graphics; namespace Hexecs.Benchmarks.Map.Terrains; diff --git a/src/Hexecs.Benchmarks.City/Utils/PointExtensions.cs b/src/Hexecs.Benchmarks.City/Utils/PointExtensions.cs index 77958bc..a3c46ea 100644 --- a/src/Hexecs.Benchmarks.City/Utils/PointExtensions.cs +++ b/src/Hexecs.Benchmarks.City/Utils/PointExtensions.cs @@ -2,8 +2,11 @@ namespace Hexecs.Benchmarks.Map.Utils; public static class PointExtensions { - public static void GetNeighborPoints(int x, int y, ref Span neighbors) + public static void GetNeighborPoints(this in Point point, ref Span neighbors) { + var x = point.X; + var y = point.Y; + neighbors[0] = new Point(x - 1, y); neighbors[1] = new Point(x + 1, y); neighbors[2] = new Point(x, y - 1); diff --git a/src/Hexecs.Tests/Actors/ActorComponentShould.cs b/src/Hexecs.Tests/Actors/ActorComponentShould.cs index 3817ab5..362987a 100644 --- a/src/Hexecs.Tests/Actors/ActorComponentShould.cs +++ b/src/Hexecs.Tests/Actors/ActorComponentShould.cs @@ -1,4 +1,5 @@ using Hexecs.Tests.Mocks; +using Hexecs.Tests.Mocks.ActorComponents; namespace Hexecs.Tests.Actors; diff --git a/src/Hexecs.Tests/Actors/ActorConstraintShould.cs b/src/Hexecs.Tests/Actors/ActorConstraintShould.cs index 913a4ee..1663309 100644 --- a/src/Hexecs.Tests/Actors/ActorConstraintShould.cs +++ b/src/Hexecs.Tests/Actors/ActorConstraintShould.cs @@ -1,4 +1,5 @@ using Hexecs.Tests.Mocks; +using Hexecs.Tests.Mocks.ActorComponents; namespace Hexecs.Tests.Actors; diff --git a/src/Hexecs.Tests/Actors/ActorContextShould.cs b/src/Hexecs.Tests/Actors/ActorContextShould.cs index 5da620e..4ae2ddc 100644 --- a/src/Hexecs.Tests/Actors/ActorContextShould.cs +++ b/src/Hexecs.Tests/Actors/ActorContextShould.cs @@ -1,6 +1,8 @@ using System.Runtime.CompilerServices; using Hexecs.Assets; using Hexecs.Tests.Mocks; +using Hexecs.Tests.Mocks.ActorComponents; +using Hexecs.Tests.Mocks.Assets; namespace Hexecs.Tests.Actors; diff --git a/src/Hexecs.Tests/Actors/ActorDictionaryShould.cs b/src/Hexecs.Tests/Actors/ActorDictionaryShould.cs deleted file mode 100644 index c469940..0000000 --- a/src/Hexecs.Tests/Actors/ActorDictionaryShould.cs +++ /dev/null @@ -1,172 +0,0 @@ -using Hexecs.Tests.Mocks; - -namespace Hexecs.Tests.Actors; - -public sealed class ActorDictionaryShould(ActorTestFixture fixture) : IClassFixture -{ - [Fact(DisplayName = "Должен автоматически добавлять актора в словарь при добавлении компонента")] - public void Should_Add_Actor_When_Component_Added() - { - // Arrange - // Используем Name как ключ - using var dict = new ActorDictionary( - fixture.Actors, - c => c.Name); - - // Act - var actor = fixture.CreateActor(); - var expectedName = actor.Get().Name; - - // Assert - dict - .ContainsKey(expectedName) - .Should() - .BeTrue(); - - dict[expectedName] - .Id.Should() - .Be(actor.Id); - } - - [Fact(DisplayName = "Должен обновлять ключ в словаре при изменении компонента")] - public void Should_Update_Key_When_Component_Updated() - { - // Arrange - var actor = fixture.CreateActor(); - actor.Add(new Description { Name = "OldName" }); - - using var dict = new ActorDictionary( - fixture.Actors, - c => c.Name); - - // Act - // Обновляем компонент, что должно вызвать OnComponentUpdating - actor.Update(new Description { Name = "NewName" }); - - // Assert - dict - .ContainsKey("OldName") - .Should() - .BeFalse(); - - dict - .ContainsKey("NewName") - .Should() - .BeTrue(); - - dict["NewName"] - .Id - .Should() - .Be(actor.Id); - } - - [Fact(DisplayName = "Должен удалять актора из словаря при удалении компонента")] - public void Should_Remove_Actor_When_Component_Removed() - { - // Arrange - var actor = fixture.CreateActor(); - actor.Add(new Description { Name = "ToRemove" }); - - using var dict = new ActorDictionary( - fixture.Actors, - c => c.Name); - - // Act - actor.Remove(); - - // Assert - dict - .ContainsKey("ToRemove") - .Should() - .BeFalse(); - - dict - .Length - .Should() - .Be(0); - } - - [Fact(DisplayName = "Должен генерировать события при добавлении и удалении")] - public void Should_Raise_Events_On_Changes() - { - // Arrange - using var dict = new ActorDictionary( - fixture.Actors, - c => c.Name); - - var addedRaised = false; - var removedRaised = false; - dict.Added += _ => addedRaised = true; - dict.Removed += _ => removedRaised = true; - - // Act - var actor = fixture.CreateActor(); - actor.Add(new Description { Name = "EventTest" }); - actor.Remove(); - - // Assert - addedRaised - .Should() - .BeTrue(); - - removedRaised - .Should() - .BeTrue(); - } - - [Fact(DisplayName = "Должен возвращать ActorRef через TryGetActorRef")] - public void Should_TryGet_ActorRef() - { - // Arrange - var actor = fixture.CreateActor(); - actor.Add(new Description { Name = "Target" }); - - using var dict = new ActorDictionary( - fixture.Actors, - c => c.Name); - - // Act - var found = dict.TryGetActorRef("Target", out var actorRef); - var notFound = dict.TryGetActorRef("Unknown", out _); - - // Assert - found.Should().BeTrue(); - actorRef.Id.Should().Be(actor.Id); - notFound.Should().BeFalse(); - } - - [Fact(DisplayName = "Должен выбрасывать исключение при обращении по несуществующему ключу через индексер")] - public void Should_Throw_When_Key_Not_Found_In_Indexer() - { - // Arrange - using var dict = new ActorDictionary( - fixture.Actors, - c => c.Name); - - // Act - var action = () => { _ = dict["Missing"]; }; - - // Assert - // Исходя из кода ActorError.KeyNotFound() - action.Should().Throw(); - } - - [Fact(DisplayName = "Должен очищаться при очистке контекста")] - public void Should_Clear_When_Context_Cleared() - { - // Arrange - var actor = fixture.CreateActor(); - actor.Add(new Description { Name = "ClearMe" }); - - using var dict = new ActorDictionary( - fixture.Actors, - c => c.Name); - - // Act - fixture.Actors.Clear(); - - // Assert - dict.Length.Should().Be(0); - dict.ContainsKey("ClearMe").Should().BeFalse(); - } -} \ No newline at end of file diff --git a/src/Hexecs.Tests/Actors/ActorFilter1Should.cs b/src/Hexecs.Tests/Actors/ActorFilter1Should.cs index 8a95f45..357cd38 100644 --- a/src/Hexecs.Tests/Actors/ActorFilter1Should.cs +++ b/src/Hexecs.Tests/Actors/ActorFilter1Should.cs @@ -1,4 +1,5 @@ using Hexecs.Tests.Mocks; +using Hexecs.Tests.Mocks.ActorComponents; using Hexecs.Utils; namespace Hexecs.Tests.Actors; diff --git a/src/Hexecs.Tests/Actors/ActorFilter2Should.cs b/src/Hexecs.Tests/Actors/ActorFilter2Should.cs index cf2691b..b37c729 100644 --- a/src/Hexecs.Tests/Actors/ActorFilter2Should.cs +++ b/src/Hexecs.Tests/Actors/ActorFilter2Should.cs @@ -1,4 +1,5 @@ using Hexecs.Tests.Mocks; +using Hexecs.Tests.Mocks.ActorComponents; namespace Hexecs.Tests.Actors; diff --git a/src/Hexecs.Tests/Actors/ActorFilter3Should.cs b/src/Hexecs.Tests/Actors/ActorFilter3Should.cs index 2304d26..18564ae 100644 --- a/src/Hexecs.Tests/Actors/ActorFilter3Should.cs +++ b/src/Hexecs.Tests/Actors/ActorFilter3Should.cs @@ -1,4 +1,5 @@ using Hexecs.Tests.Mocks; +using Hexecs.Tests.Mocks.ActorComponents; namespace Hexecs.Tests.Actors; diff --git a/src/Hexecs.Tests/Actors/ActorListShould.cs b/src/Hexecs.Tests/Actors/ActorListShould.cs index 6fbd27b..bdb98d1 100644 --- a/src/Hexecs.Tests/Actors/ActorListShould.cs +++ b/src/Hexecs.Tests/Actors/ActorListShould.cs @@ -1,4 +1,5 @@ using Hexecs.Tests.Mocks; +using Hexecs.Tests.Mocks.ActorComponents; namespace Hexecs.Tests.Actors; diff --git a/src/Hexecs.Tests/Actors/ActorMarshalShould.cs b/src/Hexecs.Tests/Actors/ActorMarshalShould.cs index 2d03e36..115745d 100644 --- a/src/Hexecs.Tests/Actors/ActorMarshalShould.cs +++ b/src/Hexecs.Tests/Actors/ActorMarshalShould.cs @@ -1,6 +1,7 @@ using System.Runtime.CompilerServices; using Hexecs.Actors.Components; using Hexecs.Tests.Mocks; +using Hexecs.Tests.Mocks.ActorComponents; namespace Hexecs.Tests.Actors; diff --git a/src/Hexecs.Tests/Actors/ActorRelationShould.cs b/src/Hexecs.Tests/Actors/ActorRelationShould.cs index 163c993..171cfe8 100644 --- a/src/Hexecs.Tests/Actors/ActorRelationShould.cs +++ b/src/Hexecs.Tests/Actors/ActorRelationShould.cs @@ -1,4 +1,5 @@ using Hexecs.Tests.Mocks; +using Hexecs.Tests.Mocks.ActorComponents; namespace Hexecs.Tests.Actors; diff --git a/src/Hexecs.Tests/Actors/ActorShould.cs b/src/Hexecs.Tests/Actors/ActorShould.cs index d6b984c..6e8cc89 100644 --- a/src/Hexecs.Tests/Actors/ActorShould.cs +++ b/src/Hexecs.Tests/Actors/ActorShould.cs @@ -1,5 +1,7 @@ using System.Runtime.CompilerServices; using Hexecs.Tests.Mocks; +using Hexecs.Tests.Mocks.ActorComponents; +using Hexecs.Tests.Mocks.Assets; namespace Hexecs.Tests.Actors; diff --git a/src/Hexecs.Tests/Actors/ActorSystemShould.cs b/src/Hexecs.Tests/Actors/ActorSystemShould.cs index b28c92d..5bab88f 100644 --- a/src/Hexecs.Tests/Actors/ActorSystemShould.cs +++ b/src/Hexecs.Tests/Actors/ActorSystemShould.cs @@ -1,6 +1,7 @@ using Hexecs.Actors.Systems; using Hexecs.Dependencies; using Hexecs.Tests.Mocks; +using Hexecs.Tests.Mocks.ActorComponents; using Hexecs.Threading; using Hexecs.Worlds; diff --git a/src/Hexecs.Tests/Actors/ActorTestFixture.cs b/src/Hexecs.Tests/Actors/ActorTestFixture.cs index 3f9d71a..bc25dc8 100644 --- a/src/Hexecs.Tests/Actors/ActorTestFixture.cs +++ b/src/Hexecs.Tests/Actors/ActorTestFixture.cs @@ -1,6 +1,8 @@ using Hexecs.Assets; using Hexecs.Assets.Sources; using Hexecs.Tests.Mocks; +using Hexecs.Tests.Mocks.ActorComponents; +using Hexecs.Tests.Mocks.Assets; using Hexecs.Worlds; namespace Hexecs.Tests.Actors; diff --git a/src/Hexecs.Tests/Assets/AssetConstraintShould.cs b/src/Hexecs.Tests/Assets/AssetConstraintShould.cs index 62c6e53..f510eec 100644 --- a/src/Hexecs.Tests/Assets/AssetConstraintShould.cs +++ b/src/Hexecs.Tests/Assets/AssetConstraintShould.cs @@ -1,6 +1,7 @@ using Hexecs.Assets; using Hexecs.Tests.Actors; using Hexecs.Tests.Mocks; +using Hexecs.Tests.Mocks.Assets; namespace Hexecs.Tests.Assets; diff --git a/src/Hexecs.Tests/Assets/AssetFilter1Should.cs b/src/Hexecs.Tests/Assets/AssetFilter1Should.cs index 4ddd7a8..c90c659 100644 --- a/src/Hexecs.Tests/Assets/AssetFilter1Should.cs +++ b/src/Hexecs.Tests/Assets/AssetFilter1Should.cs @@ -1,4 +1,5 @@ using Hexecs.Tests.Mocks; +using Hexecs.Tests.Mocks.Assets; namespace Hexecs.Tests.Assets; diff --git a/src/Hexecs.Tests/Assets/AssetFilter2Should.cs b/src/Hexecs.Tests/Assets/AssetFilter2Should.cs index 02a3c9f..6aee3c5 100644 --- a/src/Hexecs.Tests/Assets/AssetFilter2Should.cs +++ b/src/Hexecs.Tests/Assets/AssetFilter2Should.cs @@ -1,4 +1,5 @@ using Hexecs.Tests.Mocks; +using Hexecs.Tests.Mocks.Assets; namespace Hexecs.Tests.Assets; diff --git a/src/Hexecs.Tests/Assets/AssetFilter3Should.cs b/src/Hexecs.Tests/Assets/AssetFilter3Should.cs index 93d3769..d81b730 100644 --- a/src/Hexecs.Tests/Assets/AssetFilter3Should.cs +++ b/src/Hexecs.Tests/Assets/AssetFilter3Should.cs @@ -1,4 +1,5 @@ using Hexecs.Tests.Mocks; +using Hexecs.Tests.Mocks.Assets; namespace Hexecs.Tests.Assets; diff --git a/src/Hexecs.Tests/Assets/AssetTestFixture.cs b/src/Hexecs.Tests/Assets/AssetTestFixture.cs index 4f7e646..a0aa2ca 100644 --- a/src/Hexecs.Tests/Assets/AssetTestFixture.cs +++ b/src/Hexecs.Tests/Assets/AssetTestFixture.cs @@ -1,6 +1,7 @@ using Hexecs.Assets; using Hexecs.Assets.Sources; using Hexecs.Tests.Mocks; +using Hexecs.Tests.Mocks.Assets; using Hexecs.Worlds; namespace Hexecs.Tests.Assets; diff --git a/src/Hexecs.Tests/Hexecs.Tests.csproj b/src/Hexecs.Tests/Hexecs.Tests.csproj index 1bb875f..572b682 100644 --- a/src/Hexecs.Tests/Hexecs.Tests.csproj +++ b/src/Hexecs.Tests/Hexecs.Tests.csproj @@ -12,4 +12,13 @@ + + + Attack.cs + + + Defence.cs + + + diff --git a/src/Hexecs.Tests/Mocks/Attack.cs b/src/Hexecs.Tests/Mocks/ActorComponents/Attack.cs similarity index 58% rename from src/Hexecs.Tests/Mocks/Attack.cs rename to src/Hexecs.Tests/Mocks/ActorComponents/Attack.cs index 0fd35cd..67651a7 100644 --- a/src/Hexecs.Tests/Mocks/Attack.cs +++ b/src/Hexecs.Tests/Mocks/ActorComponents/Attack.cs @@ -1,4 +1,4 @@ -namespace Hexecs.Tests.Mocks; +namespace Hexecs.Tests.Mocks.ActorComponents; public struct Attack : IActorComponent { diff --git a/src/Hexecs.Tests/Mocks/AttackBuilder.cs b/src/Hexecs.Tests/Mocks/ActorComponents/AttackBuilder.cs similarity index 77% rename from src/Hexecs.Tests/Mocks/AttackBuilder.cs rename to src/Hexecs.Tests/Mocks/ActorComponents/AttackBuilder.cs index bf06d23..05917d2 100644 --- a/src/Hexecs.Tests/Mocks/AttackBuilder.cs +++ b/src/Hexecs.Tests/Mocks/ActorComponents/AttackBuilder.cs @@ -1,7 +1,8 @@ using Hexecs.Assets; +using Hexecs.Tests.Mocks.Assets; using Hexecs.Utils; -namespace Hexecs.Tests.Mocks; +namespace Hexecs.Tests.Mocks.ActorComponents; internal sealed class AttackBuilder : IActorBuilder { diff --git a/src/Hexecs.Tests/Mocks/Defence.cs b/src/Hexecs.Tests/Mocks/ActorComponents/Defence.cs similarity index 58% rename from src/Hexecs.Tests/Mocks/Defence.cs rename to src/Hexecs.Tests/Mocks/ActorComponents/Defence.cs index 602610d..078593e 100644 --- a/src/Hexecs.Tests/Mocks/Defence.cs +++ b/src/Hexecs.Tests/Mocks/ActorComponents/Defence.cs @@ -1,4 +1,4 @@ -namespace Hexecs.Tests.Mocks; +namespace Hexecs.Tests.Mocks.ActorComponents; public struct Defence : IActorComponent { diff --git a/src/Hexecs.Tests/Mocks/DefenceBuilder.cs b/src/Hexecs.Tests/Mocks/ActorComponents/DefenceBuilder.cs similarity index 77% rename from src/Hexecs.Tests/Mocks/DefenceBuilder.cs rename to src/Hexecs.Tests/Mocks/ActorComponents/DefenceBuilder.cs index b3832ff..b6192ef 100644 --- a/src/Hexecs.Tests/Mocks/DefenceBuilder.cs +++ b/src/Hexecs.Tests/Mocks/ActorComponents/DefenceBuilder.cs @@ -1,7 +1,8 @@ using Hexecs.Assets; +using Hexecs.Tests.Mocks.Assets; using Hexecs.Utils; -namespace Hexecs.Tests.Mocks; +namespace Hexecs.Tests.Mocks.ActorComponents; internal sealed class DefenceBuilder : IActorBuilder { diff --git a/src/Hexecs.Tests/Mocks/Description.cs b/src/Hexecs.Tests/Mocks/ActorComponents/Description.cs similarity index 59% rename from src/Hexecs.Tests/Mocks/Description.cs rename to src/Hexecs.Tests/Mocks/ActorComponents/Description.cs index f3c9bd1..7f86556 100644 --- a/src/Hexecs.Tests/Mocks/Description.cs +++ b/src/Hexecs.Tests/Mocks/ActorComponents/Description.cs @@ -1,4 +1,4 @@ -namespace Hexecs.Tests.Mocks; +namespace Hexecs.Tests.Mocks.ActorComponents; public struct Description : IActorComponent { diff --git a/src/Hexecs.Tests/Mocks/DisposableComponent.cs b/src/Hexecs.Tests/Mocks/ActorComponents/DisposableComponent.cs similarity index 75% rename from src/Hexecs.Tests/Mocks/DisposableComponent.cs rename to src/Hexecs.Tests/Mocks/ActorComponents/DisposableComponent.cs index 91acf29..8419a0d 100644 --- a/src/Hexecs.Tests/Mocks/DisposableComponent.cs +++ b/src/Hexecs.Tests/Mocks/ActorComponents/DisposableComponent.cs @@ -1,4 +1,4 @@ -namespace Hexecs.Tests.Mocks; +namespace Hexecs.Tests.Mocks.ActorComponents; public readonly struct DisposableComponent(IDisposable monitor) : IActorComponent, IDisposable { diff --git a/src/Hexecs.Tests/Mocks/NonExistentComponent.cs b/src/Hexecs.Tests/Mocks/ActorComponents/NonExistentComponent.cs similarity index 69% rename from src/Hexecs.Tests/Mocks/NonExistentComponent.cs rename to src/Hexecs.Tests/Mocks/ActorComponents/NonExistentComponent.cs index 98df604..3cd4af9 100644 --- a/src/Hexecs.Tests/Mocks/NonExistentComponent.cs +++ b/src/Hexecs.Tests/Mocks/ActorComponents/NonExistentComponent.cs @@ -1,4 +1,4 @@ -namespace Hexecs.Tests.Mocks; +namespace Hexecs.Tests.Mocks.ActorComponents; public struct NonExistentComponent : IActorComponent; diff --git a/src/Hexecs.Tests/Mocks/Speed.cs b/src/Hexecs.Tests/Mocks/ActorComponents/Speed.cs similarity index 56% rename from src/Hexecs.Tests/Mocks/Speed.cs rename to src/Hexecs.Tests/Mocks/ActorComponents/Speed.cs index 410a5e5..0ad5bec 100644 --- a/src/Hexecs.Tests/Mocks/Speed.cs +++ b/src/Hexecs.Tests/Mocks/ActorComponents/Speed.cs @@ -1,4 +1,4 @@ -namespace Hexecs.Tests.Mocks; +namespace Hexecs.Tests.Mocks.ActorComponents; public struct Speed: IActorComponent { diff --git a/src/Hexecs.Tests/Mocks/BuildingAsset.cs b/src/Hexecs.Tests/Mocks/Assets/BuildingAsset.cs similarity index 68% rename from src/Hexecs.Tests/Mocks/BuildingAsset.cs rename to src/Hexecs.Tests/Mocks/Assets/BuildingAsset.cs index a124a1b..7a34d29 100644 --- a/src/Hexecs.Tests/Mocks/BuildingAsset.cs +++ b/src/Hexecs.Tests/Mocks/Assets/BuildingAsset.cs @@ -1,5 +1,5 @@ using Hexecs.Assets; -namespace Hexecs.Tests.Mocks; +namespace Hexecs.Tests.Mocks.Assets; public readonly struct BuildingAsset : IAssetComponent; \ No newline at end of file diff --git a/src/Hexecs.Tests/Mocks/CarAsset.cs b/src/Hexecs.Tests/Mocks/Assets/CarAsset.cs similarity index 87% rename from src/Hexecs.Tests/Mocks/CarAsset.cs rename to src/Hexecs.Tests/Mocks/Assets/CarAsset.cs index ac26ddc..845f48b 100644 --- a/src/Hexecs.Tests/Mocks/CarAsset.cs +++ b/src/Hexecs.Tests/Mocks/Assets/CarAsset.cs @@ -1,6 +1,6 @@ using Hexecs.Assets; -namespace Hexecs.Tests.Mocks; +namespace Hexecs.Tests.Mocks.Assets; public readonly struct CarAsset(int price, int speed) : IAssetComponent { diff --git a/src/Hexecs.Tests/Mocks/DecisionAsset.cs b/src/Hexecs.Tests/Mocks/Assets/DecisionAsset.cs similarity index 82% rename from src/Hexecs.Tests/Mocks/DecisionAsset.cs rename to src/Hexecs.Tests/Mocks/Assets/DecisionAsset.cs index 040d776..172bbd0 100644 --- a/src/Hexecs.Tests/Mocks/DecisionAsset.cs +++ b/src/Hexecs.Tests/Mocks/Assets/DecisionAsset.cs @@ -1,6 +1,6 @@ using Hexecs.Assets; -namespace Hexecs.Tests.Mocks; +namespace Hexecs.Tests.Mocks.Assets; public readonly struct DecisionAsset(int min, int max) : IAssetComponent { diff --git a/src/Hexecs.Tests/Mocks/NonExistentAsset.cs b/src/Hexecs.Tests/Mocks/Assets/NonExistentAsset.cs similarity index 69% rename from src/Hexecs.Tests/Mocks/NonExistentAsset.cs rename to src/Hexecs.Tests/Mocks/Assets/NonExistentAsset.cs index 39b823a..d3cf1a1 100644 --- a/src/Hexecs.Tests/Mocks/NonExistentAsset.cs +++ b/src/Hexecs.Tests/Mocks/Assets/NonExistentAsset.cs @@ -1,5 +1,5 @@ using Hexecs.Assets; -namespace Hexecs.Tests.Mocks; +namespace Hexecs.Tests.Mocks.Assets; public readonly struct NonExistentAsset : IAssetComponent; \ No newline at end of file diff --git a/src/Hexecs.Tests/Mocks/SubjectAsset.cs b/src/Hexecs.Tests/Mocks/Assets/SubjectAsset.cs similarity index 68% rename from src/Hexecs.Tests/Mocks/SubjectAsset.cs rename to src/Hexecs.Tests/Mocks/Assets/SubjectAsset.cs index 38fef95..7636156 100644 --- a/src/Hexecs.Tests/Mocks/SubjectAsset.cs +++ b/src/Hexecs.Tests/Mocks/Assets/SubjectAsset.cs @@ -1,5 +1,5 @@ using Hexecs.Assets; -namespace Hexecs.Tests.Mocks; +namespace Hexecs.Tests.Mocks.Assets; public readonly struct SubjectAsset : IAssetComponent; \ No newline at end of file diff --git a/src/Hexecs.Tests/Mocks/UnitAsset.cs b/src/Hexecs.Tests/Mocks/Assets/UnitAsset.cs similarity index 88% rename from src/Hexecs.Tests/Mocks/UnitAsset.cs rename to src/Hexecs.Tests/Mocks/Assets/UnitAsset.cs index e13be7a..247a54e 100644 --- a/src/Hexecs.Tests/Mocks/UnitAsset.cs +++ b/src/Hexecs.Tests/Mocks/Assets/UnitAsset.cs @@ -1,6 +1,6 @@ using Hexecs.Assets; -namespace Hexecs.Tests.Mocks; +namespace Hexecs.Tests.Mocks.Assets; public readonly struct UnitAsset(int attack, int defence) : IAssetComponent { diff --git a/src/Hexecs/Actors/ActorDictionary.cs b/src/Hexecs/Actors/ActorDictionary.cs index 10d7ba2..9cfc59f 100644 --- a/src/Hexecs/Actors/ActorDictionary.cs +++ b/src/Hexecs/Actors/ActorDictionary.cs @@ -3,14 +3,10 @@ namespace Hexecs.Actors; [DebuggerDisplay("Length = {Length}")] -public sealed class ActorDictionary : IDisposable +public sealed class ActorDictionary : IDisposable where TKey : IEquatable - where T : struct, IActorComponent + where T1 : struct, IActorComponent { - public event Action? Added; - public event Action? Cleared; - public event Action? Removed; - public readonly ActorContext Context; public int Length @@ -20,14 +16,14 @@ public int Length } private readonly Dictionary _actors; - private readonly Func _keyExtractor; - private readonly ActorComponentPool _pool; + private readonly Func _keyExtractor; + private readonly ActorComponentPool _pool; private bool _disposed; public ActorDictionary( ActorContext context, - Func keyExtractor, + Func keyExtractor, IEqualityComparer? comparer = null, int capacity = 16) { @@ -38,17 +34,22 @@ public ActorDictionary( : new Dictionary(capacity, comparer); _keyExtractor = keyExtractor; - _pool = context.GetOrCreateComponentPool(); + _pool = context.GetOrCreateComponentPool(); - _pool.ComponentAdded += OnComponentAdded; _pool.ComponentRemoving += OnComponentRemoving; _pool.ComponentUpdating += OnComponentUpdating; context.Cleared += OnCleared; + } - _actors.EnsureCapacity(_pool.Length); + public void Add(Actor actor) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + var actorId = actor.Id; + var index = _pool.TryGetIndex(actorId); - Update(); + _actors.Add(_keyExtractor(actor.Component1), new Entry(actorId, index)); } public bool ContainsKey(TKey key) => !_disposed && _actors.ContainsKey(key); @@ -60,49 +61,50 @@ public void Dispose() _actors.Clear(); - _pool.ComponentAdded -= OnComponentAdded; _pool.ComponentRemoving -= OnComponentRemoving; _pool.ComponentUpdating -= OnComponentUpdating; Context.Cleared -= OnCleared; } - public ActorRef GetActorRef(TKey key) + public void Fill(bool clearBefore = true) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + if (clearBefore) OnCleared(); + + var poolEnumerator = _pool.GetEnumerator(); + while (poolEnumerator.MoveNext()) + { + var actor = poolEnumerator.Current; + _actors.Add(_keyExtractor(actor.Component1), new Entry(actor.Id, poolEnumerator.Index)); + } + } + + public ActorRef GetActorRef(TKey key) { if (!_disposed && _actors.TryGetValue(key, out var entry)) { - return new ActorRef(Context, entry.Id, ref _pool.GetByIndex(entry.Index)); + return new ActorRef(Context, entry.Id, ref _pool.GetByIndex(entry.Index)); } ActorError.KeyNotFound(); - return ActorRef.Empty; + return ActorRef.Empty; } - public bool TryGetActorRef(TKey key, out ActorRef actor) + public bool TryGetActorRef(TKey key, out ActorRef actor) { if (!_disposed && _actors.TryGetValue(key, out var entry)) { - actor = new ActorRef(Context, entry.Id, ref _pool.GetByIndex(entry.Index)); + actor = new ActorRef(Context, entry.Id, ref _pool.GetByIndex(entry.Index)); return true; } - actor = ActorRef.Empty; + actor = ActorRef.Empty; return false; } - public void Update(bool clearBefore = true) - { - if (clearBefore) OnCleared(); - - var poolEnumerator = _pool.GetEnumerator(); - while (poolEnumerator.MoveNext()) - { - var actor = poolEnumerator.Current; - OnComponentAdded(actor.Id, poolEnumerator.Index, ref actor.Component1); - } - } - - public ActorRef this[TKey key] + public ActorRef this[TKey key] { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => GetActorRef(key); @@ -111,24 +113,14 @@ public ActorRef this[TKey key] private void OnCleared() { _actors.Clear(); - Cleared?.Invoke(); - } - - private void OnComponentAdded(uint actorId, int index, ref T component) - { - _actors.Add(_keyExtractor(component), new Entry(actorId, index)); - Added?.Invoke(actorId); } - private void OnComponentRemoving(uint actorId, ref T component) + private void OnComponentRemoving(uint actorId, ref T1 component) { - if (_actors.Remove(_keyExtractor(component))) - { - Removed?.Invoke(actorId); - } + _actors.Remove(_keyExtractor(component)); } - private void OnComponentUpdating(uint actorId, ref T exists, in T expected) + private void OnComponentUpdating(uint actorId, ref T1 exists, in T1 expected) { _actors.Remove(_keyExtractor(exists), out var entry); _actors.Add(_keyExtractor(expected), new Entry(actorId, entry.Index)); diff --git a/src/Hexecs/Actors/Components/ActorComponentPool.Dictionary.cs b/src/Hexecs/Actors/Components/ActorComponentPool.Dictionary.cs index 0746de4..7076fdb 100644 --- a/src/Hexecs/Actors/Components/ActorComponentPool.Dictionary.cs +++ b/src/Hexecs/Actors/Components/ActorComponentPool.Dictionary.cs @@ -194,7 +194,7 @@ private AddResult TryAddEntrySlow(uint ownerId) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int TryGetEntryIndex(uint ownerId) + private int TryGetEntryIndex(uint ownerId) { var pageIndex = (int)(ownerId >> PageBits); var pages = _sparsePages; diff --git a/src/Hexecs/Assets/AssetContext.Entry.cs b/src/Hexecs/Assets/AssetContext.Entry.cs index ab8378d..579e0c5 100644 --- a/src/Hexecs/Assets/AssetContext.Entry.cs +++ b/src/Hexecs/Assets/AssetContext.Entry.cs @@ -21,7 +21,7 @@ public int Length public void Add(ushort item) { if (_length < InlineArraySize) _inlineArray[_length] = item; - else ArrayUtils.Insert(ref _array, ArrayPool.Shared, _length - InlineArraySize, item); + else ArrayUtils.Insert(ref _array, _length - InlineArraySize, item); _length++; } @@ -31,7 +31,6 @@ public void Add(ushort item) public void Dispose() { - if (_array is { Length: > 0 }) ArrayPool.Shared.Return(_array); _array = []; _length = 0; } From 39759b547620110cbf2701c808f110d542b3c4ff Mon Sep 17 00:00:00 2001 From: Kirill Bazhaikin Date: Tue, 6 Jan 2026 23:55:02 +0300 Subject: [PATCH 02/11] ActorContext to sparse pages --- src/Hexecs/Actors/ActorContext.Components.cs | 27 +- src/Hexecs/Actors/ActorContext.Dictionary.cs | 278 ++++++++++-------- src/Hexecs/Actors/ActorContext.Entry.cs | 85 +++--- src/Hexecs/Actors/ActorContext.Enumerator.cs | 47 ++- .../Actors/ActorContext.Serialization.cs | 50 ++-- src/Hexecs/Actors/ActorContext.cs | 25 +- src/Hexecs/Actors/ActorFilter1.Dictionary.cs | 3 +- src/Hexecs/Actors/ActorFilter2.Dictionary.cs | 1 + src/Hexecs/Actors/ActorFilter3.Dictionary.cs | 10 +- src/Hexecs/Assets/AssetContext.Entry.cs | 8 +- 10 files changed, 278 insertions(+), 256 deletions(-) diff --git a/src/Hexecs/Actors/ActorContext.Components.cs b/src/Hexecs/Actors/ActorContext.Components.cs index f068d55..00899b5 100644 --- a/src/Hexecs/Actors/ActorContext.Components.cs +++ b/src/Hexecs/Actors/ActorContext.Components.cs @@ -27,7 +27,7 @@ public ref T AddComponent(uint actorId, in T component) ref var result = ref pool.Add(actorId, in component); ref var entry = ref GetEntryExact(actorId); - entry.Components.Add(ActorComponentType.Id); + entry.Add(ActorComponentType.Id); return ref result; } @@ -47,7 +47,7 @@ public ref T CloneComponent(uint ownerId, uint cloneId) where T : struct, IAc if (pool == null) ActorError.ComponentNotFound(ownerId); ref var clone = ref GetEntryExact(cloneId); - if (clone.Components.TryAdd(ActorComponentType.Id)) + if (clone.TryAdd(ActorComponentType.Id)) { return ref pool.Clone(ownerId, cloneId); } @@ -65,10 +65,9 @@ public ref T CloneComponent(uint ownerId, uint cloneId) where T : struct, IAc public ComponentEnumerator Components(uint actorId) { ref var entry = ref GetEntry(actorId); - if (Unsafe.IsNullRef(ref entry)) return ComponentEnumerator.Empty; - - ref readonly var entryComponents = ref entry.Components; - return new ComponentEnumerator(actorId, _componentPools, entryComponents.ToArray()); + return Unsafe.IsNullRef(ref entry) + ? ComponentEnumerator.Empty + : new ComponentEnumerator(actorId, _componentPools, entry.ToArray()); } /// @@ -102,7 +101,7 @@ public ref T GetOrAddComponent(uint actorId, Func factory) if (added) { ref var entry = ref GetEntryExact(actorId); - entry.Components.Add(ActorComponentType.Id); + entry.Add(ActorComponentType.Id); } return ref component; @@ -194,7 +193,7 @@ public bool RemoveComponent(uint actorId) if (pool == null || !pool.Remove(actorId)) return false; ref var entry = ref GetEntryExact(actorId); - entry.Components.Remove(ActorComponentType.Id); + entry.Remove(ActorComponentType.Id); return true; } @@ -217,7 +216,7 @@ public bool RemoveComponent(uint actorId, out T component) } ref var entry = ref GetEntryExact(actorId); - entry.Components.Remove(ActorComponentType.Id); + entry.Remove(ActorComponentType.Id); return true; } @@ -238,7 +237,7 @@ public bool TryAdd(uint actorId, in T component) if (!result) return false; ref var entry = ref GetEntryExact(actorId); - entry.Components.Add(ActorComponentType.Id); + entry.Add(ActorComponentType.Id); return true; } @@ -253,8 +252,12 @@ public ref T TryGetComponentRef(uint actorId) where T : struct, IActorComponent { var pool = GetComponentPool(); - if (pool == null) return ref Unsafe.NullRef(); - return ref pool.TryGet(actorId); + if (pool != null) + { + return ref pool.TryGet(actorId); + } + + return ref Unsafe.NullRef(); } /// diff --git a/src/Hexecs/Actors/ActorContext.Dictionary.cs b/src/Hexecs/Actors/ActorContext.Dictionary.cs index 12a719a..8a24567 100644 --- a/src/Hexecs/Actors/ActorContext.Dictionary.cs +++ b/src/Hexecs/Actors/ActorContext.Dictionary.cs @@ -5,70 +5,36 @@ namespace Hexecs.Actors; [SuppressMessage("ReSharper", "InvertIf")] public sealed partial class ActorContext { + private const int PageBits = 12; + private const int PageSize = 1 << PageBits; // 4096 + private const int PageMask = PageSize - 1; + public int Length { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _length - _freeCount; + get => _count; } - private int[] _buckets; - private Entry[] _entries; - private int _freeCount; - private int _freeList; - private int _length; + private uint[]?[] _sparsePages; + private uint[] _dense; + private Entry[] _values; + private int _count; - private ref Entry AddEntry(uint key) + private ref Entry AddEntry(uint actorId) { - if (_buckets.Length == 0) ResizeEntries(); - - ref var bucket = ref GetBucket(key); - var entries = _entries; - var i = bucket - 1; - - while ((uint)i < (uint)entries.Length) - { - ref readonly var existsEntry = ref entries[i]; - - if (existsEntry.Key == key) ActorError.AlreadyExists(key); - i = existsEntry.Next; - } - - int index; - if (_freeCount > 0) + ref var entry = ref TryAddEntry(actorId); + if (!Unsafe.IsNullRef(ref entry)) { - index = _freeList; - _freeList = CollectionUtils.StartOfFreeList - entries[index].Next; - _freeCount--; + Created?.Invoke(actorId); + return ref entry; } - else - { - index = _length; - if (index == entries.Length) - { - ResizeEntries(); - bucket = ref GetBucket(key); - entries = _entries; - } - - _length++; - } - - ref var entry = ref entries[index]; - - entry.Key = key; - entry.Next = bucket - 1; - - bucket = index + 1; - Created?.Invoke(key); - - return ref entry; + ActorError.AlreadyExists(actorId); // выбрасывает ошибку + return ref Unsafe.NullRef(); } - private void ClearEntry(ref Entry entry) + private void ClearEntry(uint actorId, ref Entry entry) { - var actorId = entry.Key; - ref var relationsComponent = ref TryGetComponentRef(actorId); if (!Unsafe.IsNullRef(ref relationsComponent)) { @@ -78,54 +44,73 @@ private void ClearEntry(ref Entry entry) relationPool?.Remove(actorId); } } - - ref var components = ref entry.Components; - foreach (var componentId in components) + + foreach (var componentId in entry) { var componentPool = _componentPools[componentId]; componentPool?.Remove(actorId); } - components.Dispose(); + entry.Dispose(); } private void ClearEntries() { - var index = 0; - while ((uint)index < (uint)_length) + var dense = _dense; + var values = _values; + var sparsePages = _sparsePages; + + for (var i = 0; i < _count; i++) { - ref var entry = ref _entries[index]; - if (entry.Next >= -1) - { - entry.Components.Dispose(); - } + var key = dense[i]; - index++; + ref var entry = ref values[i]; + entry.Dispose(); + + var pageIndex = (int)(key >> PageBits); + sparsePages[pageIndex]![key & PageMask] = 0; } - ArrayUtils.Clear(_buckets); - ArrayUtils.Clear(_entries, _length); + _count = 0; + } - _freeCount = 0; - _freeList = 0; - _length = 0; + private void EnsureDenseCapacity() + { + if (_count >= _dense.Length) + { + var newSize = _dense.Length * 2; + Array.Resize(ref _dense, newSize); + Array.Resize(ref _values, newSize); + } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private ref int GetBucket(uint keyHash) => ref _buckets[keyHash % (uint)_buckets.Length]; + private void EnsurePageArraySize(int pageIndex) + { + if (pageIndex >= _sparsePages.Length) + { + var newSize = Math.Max(_sparsePages.Length * 2, pageIndex + 1); + Array.Resize(ref _sparsePages, newSize); + } + } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private ref Entry GetEntry(uint key) + private ref Entry GetEntry(uint actorId) { - if (_length > 0) + var pageIndex = (int)(actorId >> PageBits); + if ((uint)pageIndex < (uint)_sparsePages.Length) { - var i = _buckets[key % (uint)_buckets.Length] - 1; - var entries = _entries; - while ((uint)i < (uint)entries.Length) + var page = _sparsePages[pageIndex]; + if (page != null) { - ref var entry = ref entries[i]; - if (entry.Key == key) return ref entry; - i = entry.Next; + var denseIndexPlusOne = page[actorId & PageMask]; + if (denseIndexPlusOne != 0) + { + var index = (int)denseIndexPlusOne - 1; + if (_dense[index] == actorId) + { + return ref _values[index]; + } + } } } @@ -135,66 +120,123 @@ private ref Entry GetEntry(uint key) private ref Entry GetEntryExact(uint key) { ref var entry = ref GetEntry(key); - if (Unsafe.IsNullRef(ref entry)) ActorError.NotFound(key); - return ref entry; + if (!Unsafe.IsNullRef(ref entry)) + { + return ref entry; + } + + ActorError.NotFound(key); // exception + return ref Unsafe.NullRef(); } - private bool RemoveEntry(uint key) + private bool RemoveEntry(uint actorId) { - if (_length == 0) return false; + var pageIndex = (int)(actorId >> PageBits); + if ((uint)pageIndex >= (uint)_sparsePages.Length) return false; - ref var bucket = ref GetBucket(key); - var entries = _entries; - var i = bucket - 1; - var last = -1; + var page = _sparsePages[pageIndex]; + if (page == null) return false; - while (i >= 0) - { - ref var entry = ref entries[i]; - if (entry.Key == key) - { - Destroying?.Invoke(key); - ClearEntry(ref entry); + var offset = (int)(actorId & PageMask); + var denseIndexPlusOne = page[offset]; + if (denseIndexPlusOne == 0) return false; - if (last < 0) bucket = entry.Next + 1; - else entries[last].Next = entry.Next; + var denseIndex = (int)denseIndexPlusOne - 1; + if (_dense[denseIndex] != actorId) return false; - entry.Next = CollectionUtils.StartOfFreeList - _freeList; + // 1. Уведомляем системы + Destroying?.Invoke(actorId); - _freeCount++; - _freeList = i; - return true; - } + // 2. Получаем ссылку ОДИН раз и работаем через неё + ref var entryToRemove = ref _values[denseIndex]; + ClearEntry(actorId, ref entryToRemove); + + var lastIndex = _count - 1; + if (denseIndex != lastIndex) + { + var lastKey = _dense[lastIndex]; - last = i; - i = entry.Next; + // Переносим ключ + _dense[denseIndex] = lastKey; + + // Копируем данные из последней ячейки в текущую (удаляемую) ссылку + // Это заменяет _values[denseIndex] = _values[lastIndex] + entryToRemove = _values[lastIndex]; + + // Обновляем индекс перемещенного ключа в sparse-страницах + var lastKeyPageIndex = (int)(lastKey >> PageBits); + _sparsePages[lastKeyPageIndex]![lastKey & PageMask] = (uint)denseIndex + 1; } - return false; + // 3. Зачищаем хвост + page[offset] = 0; + _count = lastIndex; + + return true; } - private void ResizeEntries() + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private ref Entry TryAddEntry(uint actorId) { - var length = _length; - var newSize = HashHelper.GetPrime(length == 0 ? 4 : length << 1); + var pageIndex = (int)(actorId >> PageBits); + var pages = _sparsePages; - var buckets = new int[newSize]; - var entries = new Entry[newSize]; + // Максимально компактная проверка на готовность страницы и места + if ((uint)pageIndex < (uint)pages.Length) + { + var page = pages[pageIndex]; + if (page != null && (uint)_count < (uint)_dense.Length) + { + ref var slot = ref page[actorId & PageMask]; + if (slot == 0) // Чистая вставка (самый частый случай в ECS) + { + var idx = (uint)_count; + slot = idx + 1; + _dense[idx] = actorId; + _count++; + + return ref _values[idx]; + } + + // Если не 0, проверяем на дубликат (чуть медленнее) + if (_dense[slot - 1] == actorId) + { + return ref Unsafe.NullRef(); + } + } + } - Array.Copy(_entries, entries, length); + return ref TryAddEntrySlow(actorId); + } - for (var i = 0; i < length; i++) - { - ref var entry = ref entries[i]; + [MethodImpl(MethodImplOptions.NoInlining)] + private ref Entry TryAddEntrySlow(uint actorId) + { + EnsureDenseCapacity(); + var pageIndex = (int)(actorId >> PageBits); + EnsurePageArraySize(pageIndex); - if (entry.Next < -1) continue; + ref var page = ref _sparsePages[pageIndex]; + if (page == null) + { + page = ArrayUtils.Create(PageSize); + Array.Clear(page, 0, page.Length); + } - ref var bucket = ref buckets[entry.Key % (uint)buckets.Length]; - entry.Next = bucket - 1; - bucket = i + 1; + ref var denseIndexPlusOne = ref page[actorId & PageMask]; + if (denseIndexPlusOne != 0) + { + if (_dense[denseIndexPlusOne - 1] == actorId) + { + return ref Unsafe.NullRef(); + } } - _buckets = buckets; - _entries = entries; + var denseIndex = (uint)_count; + denseIndexPlusOne = denseIndex + 1; + _dense[denseIndex] = actorId; + _count++; + + return ref _values[denseIndex]; } } \ No newline at end of file diff --git a/src/Hexecs/Actors/ActorContext.Entry.cs b/src/Hexecs/Actors/ActorContext.Entry.cs index 166021b..b3fef8e 100644 --- a/src/Hexecs/Actors/ActorContext.Entry.cs +++ b/src/Hexecs/Actors/ActorContext.Entry.cs @@ -5,51 +5,30 @@ namespace Hexecs.Actors; public sealed partial class ActorContext { - private struct Entry - { - public uint Key; - public int Next; - public ComponentBucket Components; - - public readonly void Serialize(Utf8JsonWriter writer) - { - writer.WriteStartObject(); - - writer.WriteProperty("Key", Key); - writer.WritePropertyName("Components"); - - writer.WriteStartArray(); - foreach (var component in Components) - { - writer.WriteNumberValue(component); - } - - writer.WriteStartArray(); - - writer.WriteEndObject(); - } - } - [DebuggerDisplay("Length = {Length}")] [method: MethodImpl(MethodImplOptions.AggressiveInlining)] - internal struct ComponentBucket() + private struct Entry() { private const int InlineArraySize = 6; + private InlineItemArray _inlineArray; + private int _length = 0; + private ushort[]? _array; + public int Length { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _length; } - private InlineItemArray _inlineArray; - private int _length = 0; - private ushort[] _array = []; - public void Add(ushort item) { if (_length < InlineArraySize) _inlineArray[_length] = item; - else ArrayUtils.Insert(ref _array, ArrayPool.Shared, _length - InlineArraySize, item); + else + { + if (_array == null) _array = ArrayPool.Shared.Rent(InlineArraySize); + else ArrayUtils.Insert(ref _array, ArrayPool.Shared, _length - InlineArraySize, item); + } _length++; } @@ -60,15 +39,16 @@ public void Add(ushort item) public void Dispose() { if (_array is { Length: > 0 }) ArrayPool.Shared.Return(_array); - _array = []; + _array = null; _length = 0; } - public ComponentBucketEnumerator GetEnumerator() + public readonly EntryComponentEnumerator GetEnumerator() { - ref var reference = ref Unsafe.As(ref _inlineArray); + ref var inlineRef = ref Unsafe.AsRef(in _inlineArray); + ref var reference = ref Unsafe.As(ref inlineRef); var span = MemoryMarshal.CreateSpan(ref reference, InlineArraySize); - return new ComponentBucketEnumerator(span, _array, _length); + return new EntryComponentEnumerator(span, _array ?? [], _length); } public readonly int IndexOf(ushort item) @@ -116,7 +96,7 @@ public bool Remove(ushort item) if (arraySize > 0) { const int lastInlineIndex = InlineArraySize - 1; - _inlineArray[lastInlineIndex] = _array[0]; + _inlineArray[lastInlineIndex] = _array![0]; ArrayUtils.Cut(_array, 0, arraySize); } @@ -124,7 +104,7 @@ public bool Remove(ushort item) return true; } - if (_array.Length == 0 || arraySize <= 0) return false; + if (_array == null || _array.Length == 0 || arraySize <= 0) return false; var span = _array.AsSpan(0, arraySize); for (var i = 0; i < span.Length; i++) @@ -139,15 +119,30 @@ public bool Remove(ushort item) return false; } - public ushort this[int index] + public readonly void Serialize(uint actorId, Utf8JsonWriter writer) { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - readonly get => index < InlineArraySize ? _inlineArray[index] : _array[index - InlineArraySize]; - set + writer.WriteStartObject(); + + writer.WriteProperty("Key", actorId); + writer.WritePropertyName("Components"); + + writer.WriteStartArray(); + foreach (var component in this) { - if (index < InlineArraySize) _inlineArray[index] = value; - else _array[index - InlineArraySize] = value; + writer.WriteNumberValue(component); } + + writer.WriteStartArray(); + + writer.WriteEndObject(); + } + + public readonly ushort this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => index < InlineArraySize + ? _inlineArray[index] + : _array![index - InlineArraySize]; } public readonly ushort[] ToArray() @@ -172,7 +167,7 @@ public bool TryAdd(ushort item) return true; } - public ref struct ComponentBucketEnumerator + public ref struct EntryComponentEnumerator { public readonly ref ushort Current { @@ -188,7 +183,7 @@ public readonly ref ushort Current private int _index; [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal ComponentBucketEnumerator(Span inlineArray, ushort[] array, int length) + internal EntryComponentEnumerator(Span inlineArray, ushort[] array, int length) { _inlineArray = inlineArray; _array = array; diff --git a/src/Hexecs/Actors/ActorContext.Enumerator.cs b/src/Hexecs/Actors/ActorContext.Enumerator.cs index d2e10cd..b8afbb5 100644 --- a/src/Hexecs/Actors/ActorContext.Enumerator.cs +++ b/src/Hexecs/Actors/ActorContext.Enumerator.cs @@ -2,51 +2,44 @@ public sealed partial class ActorContext { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Enumerator GetEnumerator() => new(this); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + [SuppressMessage("ReSharper", "MemberHidesStaticFromOuterClass")] public struct Enumerator : IEnumerator, IEnumerable { public readonly Actor Current { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => new(_context, _context._entries[_index].Key); + get => new(_context, _dense[_index]); } public readonly int Length { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _context.Length; + get => _count; } private int _index; private readonly ActorContext _context; + private readonly uint[] _dense; + private readonly int _count; [MethodImpl(MethodImplOptions.AggressiveInlining)] internal Enumerator(ActorContext context) { _index = -1; _context = context; + _dense = context._dense; + _count = context._count; } - public bool MoveNext() - { - var index = _index + 1; - var length = _context._length; - var entries = _context._entries; - while ((uint)index < (uint)length) - { - ref readonly var entry = ref entries[index]; - if (entry.Next >= -1) - { - _index = index; - return true; - } - - index++; - } - - _index = length; - return false; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() => ++_index < _count; [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly Enumerator GetEnumerator() => this; @@ -67,17 +60,11 @@ readonly void IDisposable.Dispose() readonly IEnumerator IEnumerable.GetEnumerator() => this; - readonly void IEnumerator.Reset() + void IEnumerator.Reset() { + _index = -1; } #endregion } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Enumerator GetEnumerator() => new(this); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } \ No newline at end of file diff --git a/src/Hexecs/Actors/ActorContext.Serialization.cs b/src/Hexecs/Actors/ActorContext.Serialization.cs index 8e444d5..53190a7 100644 --- a/src/Hexecs/Actors/ActorContext.Serialization.cs +++ b/src/Hexecs/Actors/ActorContext.Serialization.cs @@ -26,32 +26,32 @@ public void Serialize(Utf8JsonWriter writer) private void SerializeComponents(Utf8JsonWriter writer) { writer.WriteStartArray(); - + foreach (var pool in _componentPools) { pool?.Serialize(writer); } - + writer.WriteEndArray(); } - + private void SerializeComponentTypes(Utf8JsonWriter writer) { writer.WriteStartArray(); - + foreach (var pool in _componentPools) { if (pool == null) continue; - + writer.WriteStartObject(); writer .WriteProperty(nameof(IActorComponentPool.Id), pool.Id) .WriteProperty(nameof(IActorComponentPool.Type), pool.Type); - + writer.WriteEndObject(); } - + writer.WriteEndArray(); } @@ -59,18 +59,18 @@ private void SerializeEntries(Utf8JsonWriter writer) { writer.WriteStartArray(); - var index = 0; - var length = _length; - var entries = _entries; - while ((uint)index < (uint)length) - { - ref readonly var entry = ref entries[index]; - if (entry.Next >= -1) - { - entry.Serialize(writer); - } + var count = _count; + var dense = _dense; + var values = _values; - index++; + for (var i = 0; i < count; i++) + { + // Извлекаем ID из плотного массива ключей + var actorId = dense[i]; + // Получаем ссылку на данные по тому же индексу + ref readonly var entry = ref values[i]; + + entry.Serialize(actorId, writer); } writer.WriteEndArray(); @@ -79,32 +79,32 @@ private void SerializeEntries(Utf8JsonWriter writer) private void SerializeRelations(Utf8JsonWriter writer) { writer.WriteStartArray(); - + foreach (var pool in _relationPools) { pool?.Serialize(writer); } - + writer.WriteEndArray(); } - + private void SerializeRelationTypes(Utf8JsonWriter writer) { writer.WriteStartArray(); - + foreach (var pool in _relationPools) { if (pool == null) continue; - + writer.WriteStartObject(); writer .WriteProperty(nameof(IActorRelationPool.Id), pool.Id) .WriteProperty(nameof(IActorRelationPool.Type), pool.Type); - + writer.WriteEndObject(); } - + writer.WriteEndArray(); } } \ No newline at end of file diff --git a/src/Hexecs/Actors/ActorContext.cs b/src/Hexecs/Actors/ActorContext.cs index 027a955..52fe545 100644 --- a/src/Hexecs/Actors/ActorContext.cs +++ b/src/Hexecs/Actors/ActorContext.cs @@ -63,11 +63,9 @@ internal ActorContext(bool isDefault, capacity = HashHelper.GetPrime(capacity); - _buckets = new int[capacity]; - _entries = new Entry[capacity]; - _freeCount = 0; - _freeList = 0; - _length = 0; + _sparsePages = new uint[16][]; + _dense = new uint[capacity]; + _values = new Entry[capacity]; _builders = []; @@ -102,11 +100,7 @@ internal ActorContext(bool isDefault, /// Идентификатор актёра для проверки /// Возвращает true, если актёр существует, иначе false [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool ActorAlive(uint actorId) - { - ref var entry = ref GetEntry(actorId); - return !Unsafe.IsNullRef(ref entry) && entry.Key == actorId; - } + public bool ActorAlive(uint actorId) => !Unsafe.IsNullRef(ref GetEntry(actorId)); /// /// Очищает контекст актёров, удаляя всех актёров и их компоненты. @@ -145,12 +139,12 @@ public Actor Clone(uint actorId, bool withParent = true) ref var cloneEntry = ref AddEntry(cloneId); ref var entry = ref GetEntryExact(actorId); - foreach (var componentId in entry.Components) + foreach (var componentId in entry) { var componentPool = _componentPools[componentId]!; componentPool.Clone(actorId, cloneId); - cloneEntry.Components.Add(componentId); + cloneEntry.Add(componentId); } // ReSharper disable once InvertIf @@ -362,9 +356,8 @@ public void GetDescription(uint actorId, ref ValueStringBuilder builder, int max builder.Append("Id = "); builder.Append(actorId); - - ref var components = ref entry.Components; - var componentsLength = components.Length; + + var componentsLength = entry.Length; if (componentsLength == 0) return; builder.Append(" ("); @@ -375,7 +368,7 @@ public void GetDescription(uint actorId, ref ValueStringBuilder builder, int max var printMore = false; // ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator - foreach (var componentId in components) + foreach (var componentId in entry) { if (maxComponentDescription == index) { diff --git a/src/Hexecs/Actors/ActorFilter1.Dictionary.cs b/src/Hexecs/Actors/ActorFilter1.Dictionary.cs index 0fba59c..baf0e22 100644 --- a/src/Hexecs/Actors/ActorFilter1.Dictionary.cs +++ b/src/Hexecs/Actors/ActorFilter1.Dictionary.cs @@ -1,5 +1,6 @@ namespace Hexecs.Actors; +[SuppressMessage("ReSharper", "InvertIf")] public sealed partial class ActorFilter { private const int PageBits = 12; @@ -62,7 +63,7 @@ private bool ContainsEntry(uint actorId) [MethodImpl(MethodImplOptions.AggressiveInlining)] private ref int GetEntryRef(uint actorId) { - var pageIndex = actorId >> PageBits; + var pageIndex = (int)(actorId >> PageBits); if ((uint)pageIndex < (uint)_sparsePages.Length) { var page = _sparsePages[pageIndex]; diff --git a/src/Hexecs/Actors/ActorFilter2.Dictionary.cs b/src/Hexecs/Actors/ActorFilter2.Dictionary.cs index 38f981c..a6ff455 100644 --- a/src/Hexecs/Actors/ActorFilter2.Dictionary.cs +++ b/src/Hexecs/Actors/ActorFilter2.Dictionary.cs @@ -1,5 +1,6 @@ namespace Hexecs.Actors; +[SuppressMessage("ReSharper", "InvertIf")] public sealed partial class ActorFilter { private const int PageBits = 12; diff --git a/src/Hexecs/Actors/ActorFilter3.Dictionary.cs b/src/Hexecs/Actors/ActorFilter3.Dictionary.cs index d9cf057..049d146 100644 --- a/src/Hexecs/Actors/ActorFilter3.Dictionary.cs +++ b/src/Hexecs/Actors/ActorFilter3.Dictionary.cs @@ -1,5 +1,6 @@ namespace Hexecs.Actors; +[SuppressMessage("ReSharper", "InvertIf")] public sealed partial class ActorFilter { private const int PageBits = 12; @@ -41,9 +42,9 @@ private void ClearEntries() } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool ContainsEntry(uint key) + private bool ContainsEntry(uint actorId) { - var pageIndex = (int)(key >> PageBits); + var pageIndex = (int)(actorId >> PageBits); var pages = _sparsePages; if ((uint)pageIndex < (uint)pages.Length) @@ -51,8 +52,8 @@ private bool ContainsEntry(uint key) var page = pages[pageIndex]; if (page != null) { - var denseIndexPlusOne = page[key & PageMask]; - return denseIndexPlusOne != 0 && _dense[denseIndexPlusOne - 1] == key; + var denseIndexPlusOne = page[actorId & PageMask]; + return denseIndexPlusOne != 0 && _dense[denseIndexPlusOne - 1] == actorId; } } @@ -105,7 +106,6 @@ private ref Entry GetEntryRef(uint actorId) private bool RemoveEntry(uint actorId) { var pageIndex = (int)(actorId >> PageBits); - // Используем вашу оригинальную "плоскую" логику из коммита if ((uint)pageIndex >= (uint)_sparsePages.Length) return false; var page = _sparsePages[pageIndex]; diff --git a/src/Hexecs/Assets/AssetContext.Entry.cs b/src/Hexecs/Assets/AssetContext.Entry.cs index 579e0c5..53bdc66 100644 --- a/src/Hexecs/Assets/AssetContext.Entry.cs +++ b/src/Hexecs/Assets/AssetContext.Entry.cs @@ -35,11 +35,11 @@ public void Dispose() _length = 0; } - public ComponentBucketEnumerator GetEnumerator() + public EntryComponentEnumerator GetEnumerator() { ref var reference = ref Unsafe.As(ref _inlineArray); var span = MemoryMarshal.CreateSpan(ref reference, InlineArraySize); - return new ComponentBucketEnumerator(span, _array, _length); + return new EntryComponentEnumerator(span, _array, _length); } public readonly int IndexOf(ushort item) @@ -90,7 +90,7 @@ public readonly ushort[] ToArray() return result; } - public ref struct ComponentBucketEnumerator + public ref struct EntryComponentEnumerator { public readonly ref ushort Current { @@ -106,7 +106,7 @@ public readonly ref ushort Current private int _index; [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal ComponentBucketEnumerator(Span inlineArray, ushort[] array, int length) + internal EntryComponentEnumerator(Span inlineArray, ushort[] array, int length) { _inlineArray = inlineArray; _array = array; From f9394ffd80137efcf82ffdb18843a9ab74cb213d Mon Sep 17 00:00:00 2001 From: Kirill Bazhaikin Date: Wed, 7 Jan 2026 00:56:19 +0300 Subject: [PATCH 03/11] ActorContext to sparse pages --- .../ActorFilter2EnumerationBenchmark.cs | 23 ++++++++++++++--- .../ActorFilter3EnumerationBenchmark.cs | 21 ++++++++++++++-- .../Actors/CheckComponentExistsBenchmark.cs | 25 +++++++++++++++++-- .../CreateAddComponentsDestroyBenchmark.cs | 3 +-- ...UpdateSystemWithParallelWorkerBenchmark.cs | 21 ++++++++++++++-- src/Hexecs.Benchmarks/Program.cs | 4 ++- src/Hexecs.Tests/Actors/ActorContextShould.cs | 3 +-- src/Hexecs/Actors/ActorContext.Components.cs | 14 +++++------ src/Hexecs/Actors/ActorContext.Dictionary.cs | 6 ++--- src/Hexecs/Actors/ActorContext.cs | 6 ++--- 10 files changed, 99 insertions(+), 27 deletions(-) diff --git a/src/Hexecs.Benchmarks/Actors/ActorFilter2EnumerationBenchmark.cs b/src/Hexecs.Benchmarks/Actors/ActorFilter2EnumerationBenchmark.cs index adef7f3..0cd6a36 100644 --- a/src/Hexecs.Benchmarks/Actors/ActorFilter2EnumerationBenchmark.cs +++ b/src/Hexecs.Benchmarks/Actors/ActorFilter2EnumerationBenchmark.cs @@ -15,6 +15,24 @@ namespace Hexecs.Benchmarks.Actors; // |----------- |---------:|------:|----------:|------------:| // | DefaultEcs | 165.8 us | 0.70 | - | NA | // | Hexecs | 236.0 us | 1.00 | - | NA | +// +// ------------------------------------------------------------------------------------ +// +// BenchmarkDotNet v0.15.8, macOS Tahoe 26.2 (25C56) [Darwin 25.2.0] +// Apple M3 Max, 1 CPU, 16 logical and 16 physical cores +// .NET SDK 10.0.101 +// [Host] : .NET 10.0.1 (10.0.1, 10.0.125.57005), Arm64 RyuJIT armv8.0-a +// .NET 10.0 : .NET 10.0.1 (10.0.1, 10.0.125.57005), Arm64 RyuJIT armv8.0-a +// +// Job=.NET 10.0 Runtime=.NET 10.0 +// +// | Method | Count | Mean | Ratio | Allocated | Alloc Ratio | +// |----------- |------- |-----------:|------:|----------:|------------:| +// | DefaultEcs | 10000 | 9.140 us | 0.88 | - | NA | +// | Hexecs | 10000 | 10.444 us | 1.00 | - | NA | +// | | | | | | | +// | DefaultEcs | 100000 | 89.176 us | 0.88 | - | NA | +// | Hexecs | 100000 | 101.793 us | 1.00 | - | NA | [SimpleJob(RuntimeMoniker.Net10_0)] [Orderer(SummaryOrderPolicy.FastestToSlowest)] @@ -25,8 +43,7 @@ namespace Hexecs.Benchmarks.Actors; [BenchmarkCategory("Actors")] public class ActorFilter2EnumerationBenchmark { - [Params(10_000, 100_000)] - public int Count; + [Params(10_000, 100_000)] public int Count; private ActorFilter _filter = null!; private World _world = null!; @@ -81,7 +98,7 @@ public void Setup() _defaultEntitySet = _defaultWorld.GetEntities().With().With().AsSet(); _filter = _world.Actors.Filter(); - + var context = _world.Actors; for (var i = 0; i < Count; i++) { diff --git a/src/Hexecs.Benchmarks/Actors/ActorFilter3EnumerationBenchmark.cs b/src/Hexecs.Benchmarks/Actors/ActorFilter3EnumerationBenchmark.cs index a97f5a9..115677c 100644 --- a/src/Hexecs.Benchmarks/Actors/ActorFilter3EnumerationBenchmark.cs +++ b/src/Hexecs.Benchmarks/Actors/ActorFilter3EnumerationBenchmark.cs @@ -16,6 +16,24 @@ namespace Hexecs.Benchmarks.Actors; // |----------- |---------:|------:|----------:|------------:| // | Hexecs | 69.55 us | 1.00 | - | NA | // | DefaultEcs | 69.89 us | 1.00 | - | NA | +// +// ------------------------------------------------------------------------------------ +// +// BenchmarkDotNet v0.15.8, macOS Tahoe 26.2 (25C56) [Darwin 25.2.0] +// Apple M3 Max, 1 CPU, 16 logical and 16 physical cores +// .NET SDK 10.0.101 +// [Host] : .NET 10.0.1 (10.0.1, 10.0.125.57005), Arm64 RyuJIT armv8.0-a +// .NET 10.0 : .NET 10.0.1 (10.0.1, 10.0.125.57005), Arm64 RyuJIT armv8.0-a +// +// Job=.NET 10.0 Runtime=.NET 10.0 +// +// | Method | Count | Mean | Ratio | Allocated | Alloc Ratio | +// |----------- |------- |----------:|------:|----------:|------------:| +// | DefaultEcs | 10000 | 13.52 us | 0.95 | - | NA | +// | Hexecs | 10000 | 14.28 us | 1.00 | - | NA | +// | | | | | | | +// | DefaultEcs | 100000 | 125.76 us | 0.90 | - | NA | +// | Hexecs | 100000 | 139.85 us | 1.00 | - | NA | [SimpleJob(RuntimeMoniker.Net10_0)] [Orderer(SummaryOrderPolicy.FastestToSlowest)] @@ -26,8 +44,7 @@ namespace Hexecs.Benchmarks.Actors; [BenchmarkCategory("Actors")] public class ActorFilter3EnumerationBenchmark { - [Params(10_000, 100_000)] - public int Count; + [Params(10_000, 100_000)] public int Count; private ActorFilter _filter = null!; private World _world = null!; diff --git a/src/Hexecs.Benchmarks/Actors/CheckComponentExistsBenchmark.cs b/src/Hexecs.Benchmarks/Actors/CheckComponentExistsBenchmark.cs index c49b9d8..bd8ad5c 100644 --- a/src/Hexecs.Benchmarks/Actors/CheckComponentExistsBenchmark.cs +++ b/src/Hexecs.Benchmarks/Actors/CheckComponentExistsBenchmark.cs @@ -19,6 +19,28 @@ namespace Hexecs.Benchmarks.Actors; // | Hexecs_Has | 342.4 us | 1.00 | - | NA | // | Hexecs_Reference | 380.7 us | 1.11 | - | NA | // | DefaultEcs_Has | 713.7 us | 2.08 | - | NA | +// +// ------------------------------------------------------------------------------------ +// +// BenchmarkDotNet v0.15.8, macOS Tahoe 26.2 (25C56) [Darwin 25.2.0] +// Apple M3 Max, 1 CPU, 16 logical and 16 physical cores +// .NET SDK 10.0.101 +// [Host] : .NET 10.0.1 (10.0.1, 10.0.125.57005), Arm64 RyuJIT armv8.0-a +// .NET 10.0 : .NET 10.0.1 (10.0.1, 10.0.125.57005), Arm64 RyuJIT armv8.0-a +// +// Job=.NET 10.0 Runtime=.NET 10.0 +// +// | Method | Mean | Ratio | Allocated | Alloc Ratio | +// |----------------- |----------:|------:|----------:|------------:| +// | Hexecs_Is | 12.76 us | 0.93 | - | NA | +// | Hexecs_Has | 13.79 us | 1.00 | - | NA | +// | Hexecs_Reference | 15.44 us | 1.12 | - | NA | +// | DefaultEcs_Has | 25.32 us | 1.84 | - | NA | +// | | | | | | +// | Hexecs_Is | 127.64 us | 0.92 | - | NA | +// | Hexecs_Has | 139.17 us | 1.00 | - | NA | +// | Hexecs_Reference | 155.12 us | 1.11 | - | NA | +// | DefaultEcs_Has | 255.36 us | 1.83 | - | NA | [SimpleJob(RuntimeMoniker.Net10_0)] [Orderer(SummaryOrderPolicy.FastestToSlowest)] @@ -29,8 +51,7 @@ namespace Hexecs.Benchmarks.Actors; [BenchmarkCategory("Actors")] public class CheckComponentExistsBenchmark { - [Params(10_000, 100_000)] - public int Count; + [Params(10_000, 100_000)] public int Count; private ActorContext _context = null!; private DefaultEcs.World _defaultWorld = null!; diff --git a/src/Hexecs.Benchmarks/Actors/CreateAddComponentsDestroyBenchmark.cs b/src/Hexecs.Benchmarks/Actors/CreateAddComponentsDestroyBenchmark.cs index 7c23f91..5c84a05 100644 --- a/src/Hexecs.Benchmarks/Actors/CreateAddComponentsDestroyBenchmark.cs +++ b/src/Hexecs.Benchmarks/Actors/CreateAddComponentsDestroyBenchmark.cs @@ -31,8 +31,7 @@ namespace Hexecs.Benchmarks.Actors; [BenchmarkCategory("Actors")] public class CreateAddComponentsDestroyBenchmark { - [Params(1_000, 100_000, 500_000)] - public int Count; + [Params(1_000)] public int Count; private List _defaultSets = null!; private List _defaultEntities = null!; diff --git a/src/Hexecs.Benchmarks/Actors/UpdateSystemWithParallelWorkerBenchmark.cs b/src/Hexecs.Benchmarks/Actors/UpdateSystemWithParallelWorkerBenchmark.cs index 2e96d88..461e8b1 100644 --- a/src/Hexecs.Benchmarks/Actors/UpdateSystemWithParallelWorkerBenchmark.cs +++ b/src/Hexecs.Benchmarks/Actors/UpdateSystemWithParallelWorkerBenchmark.cs @@ -18,6 +18,24 @@ namespace Hexecs.Benchmarks.Actors; // |-------------------- |-----------:|------:|----------:|------------:| // | Hexecs_Parallel | 769.8 us | 1.00 | - | NA | // | DefaultEcs_Parallel | 1,576.7 us | 2.05 | - | NA | +// +// ------------------------------------------------------------------------------------ +// +// BenchmarkDotNet v0.15.8, macOS Tahoe 26.2 (25C56) [Darwin 25.2.0] +// Apple M3 Max, 1 CPU, 16 logical and 16 physical cores +// .NET SDK 10.0.101 +// [Host] : .NET 10.0.1 (10.0.1, 10.0.125.57005), Arm64 RyuJIT armv8.0-a +// .NET 10.0 : .NET 10.0.1 (10.0.1, 10.0.125.57005), Arm64 RyuJIT armv8.0-a +// +// Job=.NET 10.0 Runtime=.NET 10.0 +// +// | Method | Count | Mean | Ratio | Allocated | Alloc Ratio | +// |-------------------- |-------- |----------:|------:|----------:|------------:| +// | Hexecs_Parallel | 100000 | 51.38 us | 1.00 | - | NA | +// | DefaultEcs_Parallel | 100000 | 77.63 us | 1.51 | - | NA | +// | | | | | | | +// | Hexecs_Parallel | 1000000 | 390.26 us | 1.00 | - | NA | +// | DefaultEcs_Parallel | 1000000 | 804.39 us | 2.06 | - | NA | [SimpleJob(RuntimeMoniker.Net10_0)] [Orderer(SummaryOrderPolicy.FastestToSlowest)] @@ -28,8 +46,7 @@ namespace Hexecs.Benchmarks.Actors; [BenchmarkCategory("Actors")] public class UpdateSystemWithParallelWorkerBenchmark { - [Params(100_000, 1_000_000)] - public int Count; + [Params(100_000, 1_000_000)] public int Count; private DefaultEcs.World _defaultWorld = null!; private DefaultEcsParallelSystem _defaultSystem = null!; diff --git a/src/Hexecs.Benchmarks/Program.cs b/src/Hexecs.Benchmarks/Program.cs index af5ba4f..fa338e8 100644 --- a/src/Hexecs.Benchmarks/Program.cs +++ b/src/Hexecs.Benchmarks/Program.cs @@ -1,3 +1,5 @@ using BenchmarkDotNet.Running; +using Hexecs.Benchmarks.Actors; -BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); \ No newline at end of file +BenchmarkRunner.Run(); +//BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); \ No newline at end of file diff --git a/src/Hexecs.Tests/Actors/ActorContextShould.cs b/src/Hexecs.Tests/Actors/ActorContextShould.cs index 4ae2ddc..54d5dee 100644 --- a/src/Hexecs.Tests/Actors/ActorContextShould.cs +++ b/src/Hexecs.Tests/Actors/ActorContextShould.cs @@ -1,6 +1,5 @@ using System.Runtime.CompilerServices; using Hexecs.Assets; -using Hexecs.Tests.Mocks; using Hexecs.Tests.Mocks.ActorComponents; using Hexecs.Tests.Mocks.Assets; @@ -266,7 +265,7 @@ public void AddComponentIfNotExists() var actor = fixture.Actors.CreateActor(); // act - ref var component = ref fixture.Actors.GetOrAddComponent(actor.Id, id => new Attack { Value = 30 }); + ref var component = ref fixture.Actors.GetOrAddComponent(actor.Id, _ => new Attack { Value = 30 }); // assert component.Value.Should().Be(30); diff --git a/src/Hexecs/Actors/ActorContext.Components.cs b/src/Hexecs/Actors/ActorContext.Components.cs index 00899b5..0c58200 100644 --- a/src/Hexecs/Actors/ActorContext.Components.cs +++ b/src/Hexecs/Actors/ActorContext.Components.cs @@ -26,7 +26,7 @@ public ref T AddComponent(uint actorId, in T component) var pool = GetOrCreateComponentPool(); ref var result = ref pool.Add(actorId, in component); - ref var entry = ref GetEntryExact(actorId); + ref var entry = ref GetEntryRefExact(actorId); entry.Add(ActorComponentType.Id); return ref result; @@ -46,7 +46,7 @@ public ref T CloneComponent(uint ownerId, uint cloneId) where T : struct, IAc var pool = GetComponentPool(); if (pool == null) ActorError.ComponentNotFound(ownerId); - ref var clone = ref GetEntryExact(cloneId); + ref var clone = ref GetEntryRefExact(cloneId); if (clone.TryAdd(ActorComponentType.Id)) { return ref pool.Clone(ownerId, cloneId); @@ -64,7 +64,7 @@ public ref T CloneComponent(uint ownerId, uint cloneId) where T : struct, IAc /// Перечислитель компонентов. Возвращает , если актёр не найден. public ComponentEnumerator Components(uint actorId) { - ref var entry = ref GetEntry(actorId); + ref var entry = ref GetEntryRef(actorId); return Unsafe.IsNullRef(ref entry) ? ComponentEnumerator.Empty : new ComponentEnumerator(actorId, _componentPools, entry.ToArray()); @@ -100,7 +100,7 @@ public ref T GetOrAddComponent(uint actorId, Func factory) // ReSharper disable once InvertIf if (added) { - ref var entry = ref GetEntryExact(actorId); + ref var entry = ref GetEntryRefExact(actorId); entry.Add(ActorComponentType.Id); } @@ -192,7 +192,7 @@ public bool RemoveComponent(uint actorId) var pool = GetComponentPool(); if (pool == null || !pool.Remove(actorId)) return false; - ref var entry = ref GetEntryExact(actorId); + ref var entry = ref GetEntryRefExact(actorId); entry.Remove(ActorComponentType.Id); return true; @@ -215,7 +215,7 @@ public bool RemoveComponent(uint actorId, out T component) return false; } - ref var entry = ref GetEntryExact(actorId); + ref var entry = ref GetEntryRefExact(actorId); entry.Remove(ActorComponentType.Id); return true; @@ -236,7 +236,7 @@ public bool TryAdd(uint actorId, in T component) if (!result) return false; - ref var entry = ref GetEntryExact(actorId); + ref var entry = ref GetEntryRefExact(actorId); entry.Add(ActorComponentType.Id); return true; } diff --git a/src/Hexecs/Actors/ActorContext.Dictionary.cs b/src/Hexecs/Actors/ActorContext.Dictionary.cs index 8a24567..0b85cc4 100644 --- a/src/Hexecs/Actors/ActorContext.Dictionary.cs +++ b/src/Hexecs/Actors/ActorContext.Dictionary.cs @@ -94,7 +94,7 @@ private void EnsurePageArraySize(int pageIndex) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private ref Entry GetEntry(uint actorId) + private ref Entry GetEntryRef(uint actorId) { var pageIndex = (int)(actorId >> PageBits); if ((uint)pageIndex < (uint)_sparsePages.Length) @@ -117,9 +117,9 @@ private ref Entry GetEntry(uint actorId) return ref Unsafe.NullRef(); } - private ref Entry GetEntryExact(uint key) + private ref Entry GetEntryRefExact(uint key) { - ref var entry = ref GetEntry(key); + ref var entry = ref GetEntryRef(key); if (!Unsafe.IsNullRef(ref entry)) { return ref entry; diff --git a/src/Hexecs/Actors/ActorContext.cs b/src/Hexecs/Actors/ActorContext.cs index 52fe545..13dc4dc 100644 --- a/src/Hexecs/Actors/ActorContext.cs +++ b/src/Hexecs/Actors/ActorContext.cs @@ -100,7 +100,7 @@ internal ActorContext(bool isDefault, /// Идентификатор актёра для проверки /// Возвращает true, если актёр существует, иначе false [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool ActorAlive(uint actorId) => !Unsafe.IsNullRef(ref GetEntry(actorId)); + public bool ActorAlive(uint actorId) => !Unsafe.IsNullRef(ref GetEntryRef(actorId)); /// /// Очищает контекст актёров, удаляя всех актёров и их компоненты. @@ -138,7 +138,7 @@ public Actor Clone(uint actorId, bool withParent = true) var cloneId = GetNextActorId(); ref var cloneEntry = ref AddEntry(cloneId); - ref var entry = ref GetEntryExact(actorId); + ref var entry = ref GetEntryRefExact(actorId); foreach (var componentId in entry) { var componentPool = _componentPools[componentId]!; @@ -346,7 +346,7 @@ public string GetDescription(uint actorId, int maxComponentDescription = 5) public void GetDescription(uint actorId, ref ValueStringBuilder builder, int maxComponentDescription = 5) { - ref var entry = ref GetEntry(actorId); + ref var entry = ref GetEntryRef(actorId); if (Unsafe.IsNullRef(ref entry)) { builder.Append('\''); From eb4e0dc1ce93f9005d13093d260797cdce23a174 Mon Sep 17 00:00:00 2001 From: Kirill Bazhaikin Date: Wed, 7 Jan 2026 01:25:51 +0300 Subject: [PATCH 04/11] ActorContext to sparse pages --- .../CreateAddComponentsDestroyBenchmark.cs | 24 ++++++++++++++++++- src/Hexecs/Actors/ActorContext.Dictionary.cs | 18 ++++++++------ 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/src/Hexecs.Benchmarks/Actors/CreateAddComponentsDestroyBenchmark.cs b/src/Hexecs.Benchmarks/Actors/CreateAddComponentsDestroyBenchmark.cs index 5c84a05..c06f936 100644 --- a/src/Hexecs.Benchmarks/Actors/CreateAddComponentsDestroyBenchmark.cs +++ b/src/Hexecs.Benchmarks/Actors/CreateAddComponentsDestroyBenchmark.cs @@ -21,6 +21,28 @@ namespace Hexecs.Benchmarks.Actors; // | | | | | | | | // | DefaultEcs_CreateAddDestroy | 500000 | 470,864.8 us | 0.86 | - | 16000040 B | 400,001.00 | // | Hexecs_CreateAddDestroy | 500000 | 548,102.0 us | 1.00 | - | 40 B | 1.00 | +// +// ------------------------------------------------------------------------------------ +// +// BenchmarkDotNet v0.15.8, macOS Tahoe 26.2 (25C56) [Darwin 25.2.0] +// Apple M3 Max, 1 CPU, 16 logical and 16 physical cores +// .NET SDK 10.0.101 +// [Host] : .NET 10.0.1 (10.0.1, 10.0.125.57005), Arm64 RyuJIT armv8.0-a +// .NET 10.0 : .NET 10.0.1 (10.0.1, 10.0.125.57005), Arm64 RyuJIT armv8.0-a +// +// Job=.NET 10.0 Runtime=.NET 10.0 +// +// | Method | Count | Mean | Ratio | Gen0 | Gen1 | Allocated | Alloc Ratio | +// |---------------------------- |------- |-------------:|------:|----------:|---------:|-----------:|------------:| +// | Hexecs_CreateAddDestroy | 1000 | 176.2 us | 1.00 | - | - | - | NA | +// | DefaultEcs_CreateAddDestroy | 1000 | 206.9 us | 1.17 | 3.6621 | - | 32000 B | NA | +// | | | | | | | | | +// | Hexecs_CreateAddDestroy | 100000 | 19,361.3 us | 1.00 | - | - | 40 B | 1.00 | +// | DefaultEcs_CreateAddDestroy | 100000 | 22,897.9 us | 1.18 | 375.0000 | 156.2500 | 3200040 B | 80,001.00 | +// | | | | | | | | | +// | Hexecs_CreateAddDestroy | 500000 | 113,317.3 us | 1.00 | - | - | 40 B | 1.00 | +// | DefaultEcs_CreateAddDestroy | 500000 | 124,066.4 us | 1.09 | 1800.0000 | 800.0000 | 16000040 B | 400,001.00 | + [SimpleJob(RuntimeMoniker.Net10_0)] [Orderer(SummaryOrderPolicy.FastestToSlowest)] @@ -31,7 +53,7 @@ namespace Hexecs.Benchmarks.Actors; [BenchmarkCategory("Actors")] public class CreateAddComponentsDestroyBenchmark { - [Params(1_000)] public int Count; + [Params(1_000, 100_000, 500_000)] public int Count; private List _defaultSets = null!; private List _defaultEntities = null!; diff --git a/src/Hexecs/Actors/ActorContext.Dictionary.cs b/src/Hexecs/Actors/ActorContext.Dictionary.cs index 0b85cc4..d5ebc85 100644 --- a/src/Hexecs/Actors/ActorContext.Dictionary.cs +++ b/src/Hexecs/Actors/ActorContext.Dictionary.cs @@ -144,10 +144,10 @@ private bool RemoveEntry(uint actorId) var denseIndex = (int)denseIndexPlusOne - 1; if (_dense[denseIndex] != actorId) return false; - // 1. Уведомляем системы + // 1. Уведомляем системы ПЕРЕД какими-либо изменениями структуры Destroying?.Invoke(actorId); - // 2. Получаем ссылку ОДИН раз и работаем через неё + // 2. Очищаем данные сущности (пулы компонентов и т.д.) ref var entryToRemove = ref _values[denseIndex]; ClearEntry(actorId, ref entryToRemove); @@ -159,16 +159,20 @@ private bool RemoveEntry(uint actorId) // Переносим ключ _dense[denseIndex] = lastKey; - // Копируем данные из последней ячейки в текущую (удаляемую) ссылку - // Это заменяет _values[denseIndex] = _values[lastIndex] - entryToRemove = _values[lastIndex]; + // ВАЖНО: Переносим данные. + // Поскольку мы уже вызвали ClearEntry для entryToRemove, + // мы можем просто перезаписать её. + _values[denseIndex] = _values[lastIndex]; - // Обновляем индекс перемещенного ключа в sparse-страницах + // Обновляем индекс перемещенного ключа var lastKeyPageIndex = (int)(lastKey >> PageBits); _sparsePages[lastKeyPageIndex]![lastKey & PageMask] = (uint)denseIndex + 1; } - // 3. Зачищаем хвост + // 3. ОБЯЗАТЕЛЬНО обнуляем хвост, чтобы не было дубликатов ссылок на массивы + _values[lastIndex] = default; + + // 4. Финализируем удаление page[offset] = 0; _count = lastIndex; From caa361617152502c0f1ffb922bd3546d12326c5e Mon Sep 17 00:00:00 2001 From: Kirill Bazhaikin Date: Wed, 7 Jan 2026 01:35:53 +0300 Subject: [PATCH 05/11] ActorContext to sparse pages --- .../Actors/CreateAddComponentsDestroyBenchmark.cs | 1 - src/Hexecs.Benchmarks/Program.cs | 4 ++-- src/Hexecs/Actors/ActorContext.Entry.cs | 10 +++------- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/Hexecs.Benchmarks/Actors/CreateAddComponentsDestroyBenchmark.cs b/src/Hexecs.Benchmarks/Actors/CreateAddComponentsDestroyBenchmark.cs index c06f936..528d662 100644 --- a/src/Hexecs.Benchmarks/Actors/CreateAddComponentsDestroyBenchmark.cs +++ b/src/Hexecs.Benchmarks/Actors/CreateAddComponentsDestroyBenchmark.cs @@ -43,7 +43,6 @@ namespace Hexecs.Benchmarks.Actors; // | Hexecs_CreateAddDestroy | 500000 | 113,317.3 us | 1.00 | - | - | 40 B | 1.00 | // | DefaultEcs_CreateAddDestroy | 500000 | 124,066.4 us | 1.09 | 1800.0000 | 800.0000 | 16000040 B | 400,001.00 | - [SimpleJob(RuntimeMoniker.Net10_0)] [Orderer(SummaryOrderPolicy.FastestToSlowest)] [MeanColumn, MemoryDiagnoser] diff --git a/src/Hexecs.Benchmarks/Program.cs b/src/Hexecs.Benchmarks/Program.cs index fa338e8..299c31e 100644 --- a/src/Hexecs.Benchmarks/Program.cs +++ b/src/Hexecs.Benchmarks/Program.cs @@ -1,5 +1,5 @@ using BenchmarkDotNet.Running; using Hexecs.Benchmarks.Actors; -BenchmarkRunner.Run(); -//BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); \ No newline at end of file +//BenchmarkRunner.Run(); +BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); \ No newline at end of file diff --git a/src/Hexecs/Actors/ActorContext.Entry.cs b/src/Hexecs/Actors/ActorContext.Entry.cs index b3fef8e..03afa77 100644 --- a/src/Hexecs/Actors/ActorContext.Entry.cs +++ b/src/Hexecs/Actors/ActorContext.Entry.cs @@ -33,9 +33,6 @@ public void Add(ushort item) _length++; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly bool Contains(ushort item) => IndexOf(item) > -1; - public void Dispose() { if (_array is { Length: > 0 }) ArrayPool.Shared.Return(_array); @@ -55,7 +52,6 @@ public readonly int IndexOf(ushort item) { if (_length == 0) return -1; - var inlineLength = Math.Min(_length, InlineArraySize); for (var i = 0; i < inlineLength; i++) { @@ -140,8 +136,8 @@ public readonly void Serialize(uint actorId, Utf8JsonWriter writer) public readonly ushort this[int index] { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => index < InlineArraySize - ? _inlineArray[index] + get => index < InlineArraySize + ? _inlineArray[index] : _array![index - InlineArraySize]; } @@ -160,7 +156,7 @@ public readonly ushort[] ToArray() public bool TryAdd(ushort item) { - var has = Contains(item); + var has = IndexOf(item) > -1; if (has) return false; Add(item); From 510dc887ed20916da3c99761d4d2fbf7ef0bc170 Mon Sep 17 00:00:00 2001 From: Kirill Bazhaikin Date: Wed, 7 Jan 2026 01:51:51 +0300 Subject: [PATCH 06/11] ActorContext to sparse pages --- .../CreateAddComponentsDestroyBenchmark.cs | 13 ++--- src/Hexecs.Benchmarks/Program.cs | 4 +- src/Hexecs/Actors/ActorContext.Components.cs | 56 +++++++++++++------ 3 files changed, 48 insertions(+), 25 deletions(-) diff --git a/src/Hexecs.Benchmarks/Actors/CreateAddComponentsDestroyBenchmark.cs b/src/Hexecs.Benchmarks/Actors/CreateAddComponentsDestroyBenchmark.cs index 528d662..601c404 100644 --- a/src/Hexecs.Benchmarks/Actors/CreateAddComponentsDestroyBenchmark.cs +++ b/src/Hexecs.Benchmarks/Actors/CreateAddComponentsDestroyBenchmark.cs @@ -23,7 +23,6 @@ namespace Hexecs.Benchmarks.Actors; // | Hexecs_CreateAddDestroy | 500000 | 548,102.0 us | 1.00 | - | 40 B | 1.00 | // // ------------------------------------------------------------------------------------ -// // BenchmarkDotNet v0.15.8, macOS Tahoe 26.2 (25C56) [Darwin 25.2.0] // Apple M3 Max, 1 CPU, 16 logical and 16 physical cores // .NET SDK 10.0.101 @@ -34,14 +33,14 @@ namespace Hexecs.Benchmarks.Actors; // // | Method | Count | Mean | Ratio | Gen0 | Gen1 | Allocated | Alloc Ratio | // |---------------------------- |------- |-------------:|------:|----------:|---------:|-----------:|------------:| -// | Hexecs_CreateAddDestroy | 1000 | 176.2 us | 1.00 | - | - | - | NA | -// | DefaultEcs_CreateAddDestroy | 1000 | 206.9 us | 1.17 | 3.6621 | - | 32000 B | NA | +// | Hexecs_CreateAddDestroy | 1000 | 176.0 us | 1.00 | - | - | - | NA | +// | DefaultEcs_CreateAddDestroy | 1000 | 207.3 us | 1.18 | 3.6621 | - | 32000 B | NA | // | | | | | | | | | -// | Hexecs_CreateAddDestroy | 100000 | 19,361.3 us | 1.00 | - | - | 40 B | 1.00 | -// | DefaultEcs_CreateAddDestroy | 100000 | 22,897.9 us | 1.18 | 375.0000 | 156.2500 | 3200040 B | 80,001.00 | +// | Hexecs_CreateAddDestroy | 100000 | 19,069.0 us | 1.00 | - | - | 40 B | 1.00 | +// | DefaultEcs_CreateAddDestroy | 100000 | 22,847.4 us | 1.20 | 375.0000 | 156.2500 | 3200040 B | 80,001.00 | // | | | | | | | | | -// | Hexecs_CreateAddDestroy | 500000 | 113,317.3 us | 1.00 | - | - | 40 B | 1.00 | -// | DefaultEcs_CreateAddDestroy | 500000 | 124,066.4 us | 1.09 | 1800.0000 | 800.0000 | 16000040 B | 400,001.00 | +// | Hexecs_CreateAddDestroy | 500000 | 113,318.9 us | 1.00 | - | - | 40 B | 1.00 | +// | DefaultEcs_CreateAddDestroy | 500000 | 121,507.3 us | 1.07 | 1800.0000 | 800.0000 | 16000040 B | 400,001.00 | [SimpleJob(RuntimeMoniker.Net10_0)] [Orderer(SummaryOrderPolicy.FastestToSlowest)] diff --git a/src/Hexecs.Benchmarks/Program.cs b/src/Hexecs.Benchmarks/Program.cs index 299c31e..fa338e8 100644 --- a/src/Hexecs.Benchmarks/Program.cs +++ b/src/Hexecs.Benchmarks/Program.cs @@ -1,5 +1,5 @@ using BenchmarkDotNet.Running; using Hexecs.Benchmarks.Actors; -//BenchmarkRunner.Run(); -BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); \ No newline at end of file +BenchmarkRunner.Run(); +//BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); \ No newline at end of file diff --git a/src/Hexecs/Actors/ActorContext.Components.cs b/src/Hexecs/Actors/ActorContext.Components.cs index 0c58200..f893ab8 100644 --- a/src/Hexecs/Actors/ActorContext.Components.cs +++ b/src/Hexecs/Actors/ActorContext.Components.cs @@ -3,6 +3,7 @@ namespace Hexecs.Actors; +[SuppressMessage("ReSharper", "InvertIf")] public sealed partial class ActorContext { private IActorComponentPool?[] _componentPools; @@ -65,8 +66,8 @@ public ref T CloneComponent(uint ownerId, uint cloneId) where T : struct, IAc public ComponentEnumerator Components(uint actorId) { ref var entry = ref GetEntryRef(actorId); - return Unsafe.IsNullRef(ref entry) - ? ComponentEnumerator.Empty + return Unsafe.IsNullRef(ref entry) + ? ComponentEnumerator.Empty : new ComponentEnumerator(actorId, _componentPools, entry.ToArray()); } @@ -190,12 +191,14 @@ public bool RemoveComponent(uint actorId) where T : struct, IActorComponent { var pool = GetComponentPool(); - if (pool == null || !pool.Remove(actorId)) return false; - - ref var entry = ref GetEntryRefExact(actorId); - entry.Remove(ActorComponentType.Id); + if (pool != null && pool.Remove(actorId)) + { + ref var entry = ref GetEntryRefExact(actorId); + entry.Remove(ActorComponentType.Id); + return true; + } - return true; + return false; } /// @@ -296,15 +299,17 @@ public bool UpdateComponent(uint actorId, in T component, bool createIfNotExi internal ActorComponentPool? GetComponentPool() where T : struct, IActorComponent { var id = ActorComponentType.Id; - var pools = _componentPools; - if (id >= pools.Length) return null; - var pool = pools[id]; + if (id < pools.Length) + { + var pool = pools[id]; + return pool == null + ? null + : Unsafe.As>(pool); + } - return pool == null - ? null - : Unsafe.As>(pool); + return null; } /// @@ -314,21 +319,40 @@ public bool UpdateComponent(uint actorId, in T component, bool createIfNotExi /// /// Пул компонентов указанного типа (существующий или вновь созданный). /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal ActorComponentPool GetOrCreateComponentPool() where T : struct, IActorComponent { var id = ActorComponentType.Id; - if (id < _componentPools.Length) + var pools = _componentPools; + + if ((uint)id < (uint)pools.Length) { - var existsPool = _componentPools[id]; - if (existsPool != null) return Unsafe.As>(existsPool); + var existsPool = pools[id]; + if (existsPool != null) + { + return Unsafe.As>(existsPool); + } } + return CreateComponentPoolSlow(id); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private ActorComponentPool CreateComponentPoolSlow(ushort id) where T : struct, IActorComponent + { #if NET9_0_OR_GREATER using (_componentPoolLock.EnterScope()) #else lock (_componentPoolLock) #endif { + // Повторная проверка под локом (Double-Check Locking) + if (id < _componentPools.Length) + { + var existsPool = _componentPools[id]; + if (existsPool != null) return Unsafe.As>(existsPool); + } + ArrayUtils.EnsureCapacity(ref _componentPools, id); ref var pool = ref _componentPools[id]; pool ??= new ActorComponentPool(this, GetOrCreateComponentConfiguration()); From 09782e3734ad0af6cb18923f043d6606c56a8bd1 Mon Sep 17 00:00:00 2001 From: Kirill Bazhaikin Date: Thu, 8 Jan 2026 01:08:52 +0300 Subject: [PATCH 07/11] relations refactor --- ... => ActorCheckComponentExistsBenchmark.cs} | 3 +- ...torCreateAddComponentsDestroyBenchmark.cs} | 3 +- .../ActorFilter2EnumerationBenchmark.cs | 1 + .../ActorFilter3EnumerationBenchmark.cs | 1 + .../Actors/ActorRelationBenchmark.cs | 110 +++++++++ ...UpdateSystemWithParallelWorkerBenchmark.cs | 1 + .../Hexecs.Benchmarks.csproj | 9 + .../Mocks/ActorComponents/Attack.cs | 6 + .../{ => ActorComponents}/AttackBuilder.cs | 2 +- .../Mocks/{ => ActorComponents}/Defence.cs | 2 +- .../{ => ActorComponents}/DefenceBuilder.cs | 2 +- .../Mocks/ActorComponents/Employee.cs | 6 + .../ActorComponents/EmployeeAgreement.cs | 6 + .../Mocks/ActorComponents/Employer.cs | 3 + .../Mocks/{ => ActorComponents}/Speed.cs | 2 +- .../Mocks/{Attack.cs => RelationMock.cs} | 2 +- src/Hexecs.Benchmarks/Program.cs | 2 +- .../Actors/ActorRelationShould.cs | 3 +- src/Hexecs.Tests/Collections/BucketShould.cs | 36 --- src/Hexecs/Actors/ActorContext.Relations.cs | 14 +- src/Hexecs/Actors/ActorRelation.cs | 3 +- .../Relations/ActorRelationComponent.cs | 36 ++- .../Relations/ActorRelationPool.Enumerator.cs | 34 ++- .../Actors/Relations/ActorRelationPool.cs | 222 +++++++++++++++--- src/Hexecs/Collections/Bucket.cs | 27 ++- 25 files changed, 402 insertions(+), 134 deletions(-) rename src/Hexecs.Benchmarks/Actors/{CheckComponentExistsBenchmark.cs => ActorCheckComponentExistsBenchmark.cs} (98%) rename src/Hexecs.Benchmarks/Actors/{CreateAddComponentsDestroyBenchmark.cs => ActorCreateAddComponentsDestroyBenchmark.cs} (98%) create mode 100644 src/Hexecs.Benchmarks/Actors/ActorRelationBenchmark.cs create mode 100644 src/Hexecs.Benchmarks/Mocks/ActorComponents/Attack.cs rename src/Hexecs.Benchmarks/Mocks/{ => ActorComponents}/AttackBuilder.cs (84%) rename src/Hexecs.Benchmarks/Mocks/{ => ActorComponents}/Defence.cs (56%) rename src/Hexecs.Benchmarks/Mocks/{ => ActorComponents}/DefenceBuilder.cs (84%) create mode 100644 src/Hexecs.Benchmarks/Mocks/ActorComponents/Employee.cs create mode 100644 src/Hexecs.Benchmarks/Mocks/ActorComponents/EmployeeAgreement.cs create mode 100644 src/Hexecs.Benchmarks/Mocks/ActorComponents/Employer.cs rename src/Hexecs.Benchmarks/Mocks/{ => ActorComponents}/Speed.cs (53%) rename src/Hexecs.Benchmarks/Mocks/{Attack.cs => RelationMock.cs} (61%) diff --git a/src/Hexecs.Benchmarks/Actors/CheckComponentExistsBenchmark.cs b/src/Hexecs.Benchmarks/Actors/ActorCheckComponentExistsBenchmark.cs similarity index 98% rename from src/Hexecs.Benchmarks/Actors/CheckComponentExistsBenchmark.cs rename to src/Hexecs.Benchmarks/Actors/ActorCheckComponentExistsBenchmark.cs index bd8ad5c..963e196 100644 --- a/src/Hexecs.Benchmarks/Actors/CheckComponentExistsBenchmark.cs +++ b/src/Hexecs.Benchmarks/Actors/ActorCheckComponentExistsBenchmark.cs @@ -1,5 +1,6 @@ using System.Runtime.CompilerServices; using Hexecs.Benchmarks.Mocks; +using Hexecs.Benchmarks.Mocks.ActorComponents; using Hexecs.Worlds; using World = Hexecs.Worlds.World; @@ -49,7 +50,7 @@ namespace Hexecs.Benchmarks.Actors; [JsonExporterAttribute.Full] [JsonExporterAttribute.FullCompressed] [BenchmarkCategory("Actors")] -public class CheckComponentExistsBenchmark +public class ActorCheckComponentExistsBenchmark { [Params(10_000, 100_000)] public int Count; diff --git a/src/Hexecs.Benchmarks/Actors/CreateAddComponentsDestroyBenchmark.cs b/src/Hexecs.Benchmarks/Actors/ActorCreateAddComponentsDestroyBenchmark.cs similarity index 98% rename from src/Hexecs.Benchmarks/Actors/CreateAddComponentsDestroyBenchmark.cs rename to src/Hexecs.Benchmarks/Actors/ActorCreateAddComponentsDestroyBenchmark.cs index 601c404..5e2e1ea 100644 --- a/src/Hexecs.Benchmarks/Actors/CreateAddComponentsDestroyBenchmark.cs +++ b/src/Hexecs.Benchmarks/Actors/ActorCreateAddComponentsDestroyBenchmark.cs @@ -1,4 +1,5 @@ using Hexecs.Benchmarks.Mocks; +using Hexecs.Benchmarks.Mocks.ActorComponents; using Hexecs.Worlds; namespace Hexecs.Benchmarks.Actors; @@ -49,7 +50,7 @@ namespace Hexecs.Benchmarks.Actors; [JsonExporterAttribute.Full] [JsonExporterAttribute.FullCompressed] [BenchmarkCategory("Actors")] -public class CreateAddComponentsDestroyBenchmark +public class ActorCreateAddComponentsDestroyBenchmark { [Params(1_000, 100_000, 500_000)] public int Count; diff --git a/src/Hexecs.Benchmarks/Actors/ActorFilter2EnumerationBenchmark.cs b/src/Hexecs.Benchmarks/Actors/ActorFilter2EnumerationBenchmark.cs index 0cd6a36..f9ed7f7 100644 --- a/src/Hexecs.Benchmarks/Actors/ActorFilter2EnumerationBenchmark.cs +++ b/src/Hexecs.Benchmarks/Actors/ActorFilter2EnumerationBenchmark.cs @@ -1,4 +1,5 @@ using Hexecs.Benchmarks.Mocks; +using Hexecs.Benchmarks.Mocks.ActorComponents; using Hexecs.Worlds; namespace Hexecs.Benchmarks.Actors; diff --git a/src/Hexecs.Benchmarks/Actors/ActorFilter3EnumerationBenchmark.cs b/src/Hexecs.Benchmarks/Actors/ActorFilter3EnumerationBenchmark.cs index 115677c..cd3c95c 100644 --- a/src/Hexecs.Benchmarks/Actors/ActorFilter3EnumerationBenchmark.cs +++ b/src/Hexecs.Benchmarks/Actors/ActorFilter3EnumerationBenchmark.cs @@ -1,4 +1,5 @@ using Hexecs.Benchmarks.Mocks; +using Hexecs.Benchmarks.Mocks.ActorComponents; using Hexecs.Worlds; using World = Hexecs.Worlds.World; diff --git a/src/Hexecs.Benchmarks/Actors/ActorRelationBenchmark.cs b/src/Hexecs.Benchmarks/Actors/ActorRelationBenchmark.cs new file mode 100644 index 0000000..85ea265 --- /dev/null +++ b/src/Hexecs.Benchmarks/Actors/ActorRelationBenchmark.cs @@ -0,0 +1,110 @@ +using System.Buffers; +using Hexecs.Benchmarks.Mocks.ActorComponents; +using Hexecs.Worlds; + +namespace Hexecs.Benchmarks.Actors; + +// BenchmarkDotNet v0.15.8, macOS Tahoe 26.2 (25C56) [Darwin 25.2.0] +// Apple M3 Max, 1 CPU, 16 logical and 16 physical cores +// .NET SDK 10.0.101 +// [Host] : .NET 10.0.1 (10.0.1, 10.0.125.57005), Arm64 RyuJIT armv8.0-a +// .NET 10.0 : .NET 10.0.1 (10.0.1, 10.0.125.57005), Arm64 RyuJIT armv8.0-a +// +// Job=.NET 10.0 Runtime=.NET 10.0 Error=2.80 ms +// StdDev=2.34 ms +// +// | Method | Count | Mean | Allocated | +// |------- |------ |---------:|----------:| +// | Do | 1000 | 254.5 ms | - | + +[SimpleJob(RuntimeMoniker.Net10_0)] +[Orderer(SummaryOrderPolicy.FastestToSlowest)] +[MeanColumn, MemoryDiagnoser] +[HideColumns("Job", "Error", "StdDev", "Median", "RatioSD")] +[JsonExporterAttribute.Full] +[JsonExporterAttribute.FullCompressed] +[BenchmarkCategory("Actors")] +public class ActorRelationBenchmark +{ + [Params(100, 1_000, 2_000)] public int Count; + + private ActorContext _actorContext = null!; + private ActorFilter _employeeFilter = null!; + private ActorFilter _employerFilter = null!; + private World _world = null!; + + [Benchmark] + public int Do() + { + // Часть 1: Наполнение (тут всё отлично) + using (var employeeEnumerator = _employeeFilter.GetEnumerator()) + { + foreach (var employer in _employerFilter) + { + for (var i = 0; i < Count; i++) + { + if (!employeeEnumerator.MoveNext()) break; + var employee = employeeEnumerator.Current; + employer.AddRelation(employee, new EmployeeAgreement { Salary = i }); + } + } + } + + var result = 0; + var buffer = ArrayPool.Shared.Rent(Count); + + // Часть 2: Удаление + foreach (var employer in _employerFilter) + { + var relations = employer.Relations(); + var i = 0; + + // Сначала копируем ID во временный буфер, чтобы не ломать итератор + foreach (var relation in relations) + { + buffer[i++] = relation.Id; + } + + // Теперь спокойно удаляем + for (var j = 0; j < i; j++) + { + if (_actorContext.RemoveRelation(employer.Id, buffer[j])) + { + result++; + } + } + } + + ArrayPool.Shared.Return(buffer); + return result; + } + + [GlobalCleanup] + public void Cleanup() + { + _world.Dispose(); + _world = null!; + } + + [GlobalSetup] + public void Setup() + { + _world = new WorldBuilder().Build(); + + _actorContext = _world.Actors; + _employeeFilter = _actorContext.Filter(); + _employerFilter = _actorContext.Filter(); + + for (var i = 0; i < Count; i++) + { + var employer = _actorContext.CreateActor(); + employer.Add(new Employer()); + + for (var y = 0; y < Count; y++) + { + var employee = _actorContext.CreateActor(); + employee.Add(new Employee()); + } + } + } +} \ No newline at end of file diff --git a/src/Hexecs.Benchmarks/Actors/UpdateSystemWithParallelWorkerBenchmark.cs b/src/Hexecs.Benchmarks/Actors/UpdateSystemWithParallelWorkerBenchmark.cs index 461e8b1..a7b6582 100644 --- a/src/Hexecs.Benchmarks/Actors/UpdateSystemWithParallelWorkerBenchmark.cs +++ b/src/Hexecs.Benchmarks/Actors/UpdateSystemWithParallelWorkerBenchmark.cs @@ -1,6 +1,7 @@ using DefaultEcs.System; using Hexecs.Actors.Systems; using Hexecs.Benchmarks.Mocks; +using Hexecs.Benchmarks.Mocks.ActorComponents; using Hexecs.Threading; using Hexecs.Worlds; diff --git a/src/Hexecs.Benchmarks/Hexecs.Benchmarks.csproj b/src/Hexecs.Benchmarks/Hexecs.Benchmarks.csproj index 2cf767c..f0824d2 100644 --- a/src/Hexecs.Benchmarks/Hexecs.Benchmarks.csproj +++ b/src/Hexecs.Benchmarks/Hexecs.Benchmarks.csproj @@ -15,4 +15,13 @@ + + + Attack.cs + + + Defence.cs + + + diff --git a/src/Hexecs.Benchmarks/Mocks/ActorComponents/Attack.cs b/src/Hexecs.Benchmarks/Mocks/ActorComponents/Attack.cs new file mode 100644 index 0000000..8ec282e --- /dev/null +++ b/src/Hexecs.Benchmarks/Mocks/ActorComponents/Attack.cs @@ -0,0 +1,6 @@ +namespace Hexecs.Benchmarks.Mocks.ActorComponents; + +public struct Attack : IActorComponent +{ + public int Value; +} \ No newline at end of file diff --git a/src/Hexecs.Benchmarks/Mocks/AttackBuilder.cs b/src/Hexecs.Benchmarks/Mocks/ActorComponents/AttackBuilder.cs similarity index 84% rename from src/Hexecs.Benchmarks/Mocks/AttackBuilder.cs rename to src/Hexecs.Benchmarks/Mocks/ActorComponents/AttackBuilder.cs index 2d4572a..d2d58c6 100644 --- a/src/Hexecs.Benchmarks/Mocks/AttackBuilder.cs +++ b/src/Hexecs.Benchmarks/Mocks/ActorComponents/AttackBuilder.cs @@ -1,7 +1,7 @@ using Hexecs.Assets; using Hexecs.Utils; -namespace Hexecs.Benchmarks.Mocks; +namespace Hexecs.Benchmarks.Mocks.ActorComponents; internal sealed class AttackBuilder : IActorBuilder { diff --git a/src/Hexecs.Benchmarks/Mocks/Defence.cs b/src/Hexecs.Benchmarks/Mocks/ActorComponents/Defence.cs similarity index 56% rename from src/Hexecs.Benchmarks/Mocks/Defence.cs rename to src/Hexecs.Benchmarks/Mocks/ActorComponents/Defence.cs index efb89c3..5834629 100644 --- a/src/Hexecs.Benchmarks/Mocks/Defence.cs +++ b/src/Hexecs.Benchmarks/Mocks/ActorComponents/Defence.cs @@ -1,4 +1,4 @@ -namespace Hexecs.Benchmarks.Mocks; +namespace Hexecs.Benchmarks.Mocks.ActorComponents; public struct Defence : IActorComponent { diff --git a/src/Hexecs.Benchmarks/Mocks/DefenceBuilder.cs b/src/Hexecs.Benchmarks/Mocks/ActorComponents/DefenceBuilder.cs similarity index 84% rename from src/Hexecs.Benchmarks/Mocks/DefenceBuilder.cs rename to src/Hexecs.Benchmarks/Mocks/ActorComponents/DefenceBuilder.cs index 881b669..3e63e2d 100644 --- a/src/Hexecs.Benchmarks/Mocks/DefenceBuilder.cs +++ b/src/Hexecs.Benchmarks/Mocks/ActorComponents/DefenceBuilder.cs @@ -1,7 +1,7 @@ using Hexecs.Assets; using Hexecs.Utils; -namespace Hexecs.Benchmarks.Mocks; +namespace Hexecs.Benchmarks.Mocks.ActorComponents; internal sealed class DefenceBuilder : IActorBuilder { diff --git a/src/Hexecs.Benchmarks/Mocks/ActorComponents/Employee.cs b/src/Hexecs.Benchmarks/Mocks/ActorComponents/Employee.cs new file mode 100644 index 0000000..44698de --- /dev/null +++ b/src/Hexecs.Benchmarks/Mocks/ActorComponents/Employee.cs @@ -0,0 +1,6 @@ +namespace Hexecs.Benchmarks.Mocks.ActorComponents; + +public struct Employee : IActorComponent +{ + +} \ No newline at end of file diff --git a/src/Hexecs.Benchmarks/Mocks/ActorComponents/EmployeeAgreement.cs b/src/Hexecs.Benchmarks/Mocks/ActorComponents/EmployeeAgreement.cs new file mode 100644 index 0000000..ccfa7f9 --- /dev/null +++ b/src/Hexecs.Benchmarks/Mocks/ActorComponents/EmployeeAgreement.cs @@ -0,0 +1,6 @@ +namespace Hexecs.Benchmarks.Mocks.ActorComponents; + +public struct EmployeeAgreement +{ + public int Salary; +} \ No newline at end of file diff --git a/src/Hexecs.Benchmarks/Mocks/ActorComponents/Employer.cs b/src/Hexecs.Benchmarks/Mocks/ActorComponents/Employer.cs new file mode 100644 index 0000000..da6849c --- /dev/null +++ b/src/Hexecs.Benchmarks/Mocks/ActorComponents/Employer.cs @@ -0,0 +1,3 @@ +namespace Hexecs.Benchmarks.Mocks.ActorComponents; + +public struct Employer : IActorComponent; \ No newline at end of file diff --git a/src/Hexecs.Benchmarks/Mocks/Speed.cs b/src/Hexecs.Benchmarks/Mocks/ActorComponents/Speed.cs similarity index 53% rename from src/Hexecs.Benchmarks/Mocks/Speed.cs rename to src/Hexecs.Benchmarks/Mocks/ActorComponents/Speed.cs index 3142bab..9fb5fa9 100644 --- a/src/Hexecs.Benchmarks/Mocks/Speed.cs +++ b/src/Hexecs.Benchmarks/Mocks/ActorComponents/Speed.cs @@ -1,4 +1,4 @@ -namespace Hexecs.Benchmarks.Mocks; +namespace Hexecs.Benchmarks.Mocks.ActorComponents; public struct Speed: IActorComponent { diff --git a/src/Hexecs.Benchmarks/Mocks/Attack.cs b/src/Hexecs.Benchmarks/Mocks/RelationMock.cs similarity index 61% rename from src/Hexecs.Benchmarks/Mocks/Attack.cs rename to src/Hexecs.Benchmarks/Mocks/RelationMock.cs index 358a8af..65454eb 100644 --- a/src/Hexecs.Benchmarks/Mocks/Attack.cs +++ b/src/Hexecs.Benchmarks/Mocks/RelationMock.cs @@ -1,6 +1,6 @@ namespace Hexecs.Benchmarks.Mocks; -public struct Attack : IActorComponent +public struct RelationMock { public int Value; } \ No newline at end of file diff --git a/src/Hexecs.Benchmarks/Program.cs b/src/Hexecs.Benchmarks/Program.cs index fa338e8..7aa34dc 100644 --- a/src/Hexecs.Benchmarks/Program.cs +++ b/src/Hexecs.Benchmarks/Program.cs @@ -1,5 +1,5 @@ using BenchmarkDotNet.Running; using Hexecs.Benchmarks.Actors; -BenchmarkRunner.Run(); +BenchmarkRunner.Run(); //BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); \ No newline at end of file diff --git a/src/Hexecs.Tests/Actors/ActorRelationShould.cs b/src/Hexecs.Tests/Actors/ActorRelationShould.cs index 171cfe8..30f3b4f 100644 --- a/src/Hexecs.Tests/Actors/ActorRelationShould.cs +++ b/src/Hexecs.Tests/Actors/ActorRelationShould.cs @@ -24,7 +24,8 @@ public void CreateRelation() .Should() .BeTrue(); - actor2.HasRelation(actor1) + actor2 + .HasRelation(actor1) .Should() .BeTrue(); } diff --git a/src/Hexecs.Tests/Collections/BucketShould.cs b/src/Hexecs.Tests/Collections/BucketShould.cs index 25fc5a8..dba5a37 100644 --- a/src/Hexecs.Tests/Collections/BucketShould.cs +++ b/src/Hexecs.Tests/Collections/BucketShould.cs @@ -370,42 +370,6 @@ public void Remove_ItemExists_ShouldRemoveItemAndReturnTrue() bucket.Has(2).Should().BeFalse(); } - [Fact(DisplayName = "Remove: должен удалять первый элемент")] - public void Remove_FirstItem_ShouldRemoveAndShift() - { - // Arrange - var bucket = new Bucket(3); - bucket.Add(10); - bucket.Add(20); - bucket.Add(30); - - // Act - var removed = bucket.Remove(10); - - // Assert - removed.Should().BeTrue(); - bucket.Length.Should().Be(2); - bucket.ToArray().Should().Equal(20, 30); - } - - [Fact(DisplayName = "Remove: должен удалять последний элемент")] - public void Remove_LastItem_ShouldRemove() - { - // Arrange - var bucket = new Bucket(3); - bucket.Add(10); - bucket.Add(20); - bucket.Add(30); - - // Act - var removed = bucket.Remove(30); - - // Assert - removed.Should().BeTrue(); - bucket.Length.Should().Be(2); - bucket.ToArray().Should().Equal(10, 20); - } - [Fact(DisplayName = "Remove: должен возвращать false, если элемент не существует")] public void Remove_ItemDoesNotExist_ShouldReturnFalse() { diff --git a/src/Hexecs/Actors/ActorContext.Relations.cs b/src/Hexecs/Actors/ActorContext.Relations.cs index cf18b26..eaa3945 100644 --- a/src/Hexecs/Actors/ActorContext.Relations.cs +++ b/src/Hexecs/Actors/ActorContext.Relations.cs @@ -120,11 +120,17 @@ public bool RemoveRelation(uint subject, uint relative, out T relation) where var relationId = ActorRelationType.Id; - ref var subjectRelations = ref GetComponent(subject); - subjectRelations.Remove(relationId); + if (pool.Count(subject) == 0) + { + ref var subjectRelations = ref GetComponent(subject); + subjectRelations.Remove(relationId); + } - ref var relativeRelations = ref GetComponent(relative); - relativeRelations.Remove(relationId); + if (pool.Count(relative) == 0) + { + ref var relativeRelations = ref GetComponent(relative); + relativeRelations.Remove(relationId); + } return true; } diff --git a/src/Hexecs/Actors/ActorRelation.cs b/src/Hexecs/Actors/ActorRelation.cs index 912d079..de01c35 100644 --- a/src/Hexecs/Actors/ActorRelation.cs +++ b/src/Hexecs/Actors/ActorRelation.cs @@ -32,7 +32,8 @@ public ref T1 Relation private readonly ref T1 _relation; - public ActorRelation(ActorContext context, uint id, ref T1 relation) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal ActorRelation(ActorContext context, uint id, ref T1 relation) { Id = id; Context = context; diff --git a/src/Hexecs/Actors/Relations/ActorRelationComponent.cs b/src/Hexecs/Actors/Relations/ActorRelationComponent.cs index 2d70c89..53c39dd 100644 --- a/src/Hexecs/Actors/Relations/ActorRelationComponent.cs +++ b/src/Hexecs/Actors/Relations/ActorRelationComponent.cs @@ -3,6 +3,7 @@ namespace Hexecs.Actors.Relations; +[method: MethodImpl(MethodImplOptions.AggressiveInlining)] internal struct ActorRelationComponent(int capacity) : IActorComponent, IDisposable { public static ActorRelationComponent Create(uint actorId) => new(4); @@ -31,15 +32,19 @@ public void Add(uint relationId) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly ReadOnlySpan AsReadOnlySpan() => _length == 0 + public readonly ReadOnlySpan AsReadOnlySpan() => _array == null ? ReadOnlySpan.Empty - : new ReadOnlySpan(_array, 0, _length); + : _array.AsSpan(0, _length); public void Dispose() { - if (_array is { Length: > 0 }) ArrayPool.Shared.Return(_array); + var arr = _array; + if (arr != null) + { + _array = null; // Защита от двойного Dispose + ArrayPool.Shared.Return(arr); + } - _array = []; _length = 0; } @@ -48,29 +53,22 @@ public readonly ArrayEnumerator GetEnumerator() => _array == null ? ArrayEnumerator.Empty : new ArrayEnumerator(_array, _length); - public readonly bool Has(uint relationId) - { - if (_length == 0) return false; - - foreach (var exists in AsReadOnlySpan()) - { - if (exists == relationId) return true; - } - - return false; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly bool Has(uint relationId) => _length != 0 && AsReadOnlySpan().Contains(relationId); public bool Remove(uint relationId) { - if (_length == 0) return false; - var span = AsReadOnlySpan(); for (var i = 0; i < span.Length; i++) { if (span[i] != relationId) continue; - ArrayUtils.Cut(_array!, i); - _length--; + // Swap-Back: $O(1)$ + var lastIndex = --_length; + if (i < lastIndex) + { + _array![i] = _array[lastIndex]; + } return true; } diff --git a/src/Hexecs/Actors/Relations/ActorRelationPool.Enumerator.cs b/src/Hexecs/Actors/Relations/ActorRelationPool.Enumerator.cs index f3fd882..a51886e 100644 --- a/src/Hexecs/Actors/Relations/ActorRelationPool.Enumerator.cs +++ b/src/Hexecs/Actors/Relations/ActorRelationPool.Enumerator.cs @@ -1,41 +1,42 @@ namespace Hexecs.Actors.Relations; -public struct ActorRelationEnumerator +public ref struct ActorRelationEnumerator where T : struct { public static ActorRelationEnumerator Empty { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => new(null!, 0); + get => new(null!, ReadOnlySpan.Empty); } public readonly ActorRelation Current { get { - var current = _enumerator.Current; - ref var reference = ref CollectionsMarshal.GetValueRefOrNullRef(_pool.Relations, current); - return new ActorRelation(_pool.Context, current.Second, ref reference); + var index = _indices[_currentIndex]; + var key = _pool.Keys[index]; + ref var reference = ref _pool.Values[index]; + return new ActorRelation(_pool.Context, key.First == _subject ? key.Second : key.First, ref reference); } } public readonly int Length { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _pool.Count(_subject); + get => _indices.Length; } - private Dictionary.RelationKey, T>.KeyCollection.Enumerator _enumerator; + private readonly ReadOnlySpan _indices; private readonly ActorRelationPool _pool; private readonly uint _subject; + private int _currentIndex; - internal ActorRelationEnumerator(ActorRelationPool? pool, uint subject) + internal ActorRelationEnumerator(ActorRelationPool pool, ReadOnlySpan indices, uint subject = 0) { - _pool = pool!; - _enumerator = pool == null - ? new Dictionary.RelationKey, T>.KeyCollection.Enumerator() - : pool.Relations.Keys.GetEnumerator(); + _pool = pool; + _indices = indices; _subject = subject; + _currentIndex = -1; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -43,17 +44,12 @@ internal ActorRelationEnumerator(ActorRelationPool? pool, uint subject) public void Dispose() { - _enumerator.Dispose(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool MoveNext() { - while (_enumerator.MoveNext()) - { - if (_enumerator.Current.Is(_subject)) return true; - } - - return false; + _currentIndex++; + return _currentIndex < _indices.Length; } } \ No newline at end of file diff --git a/src/Hexecs/Actors/Relations/ActorRelationPool.cs b/src/Hexecs/Actors/Relations/ActorRelationPool.cs index 418fb9f..34c4f00 100644 --- a/src/Hexecs/Actors/Relations/ActorRelationPool.cs +++ b/src/Hexecs/Actors/Relations/ActorRelationPool.cs @@ -1,4 +1,6 @@ -namespace Hexecs.Actors.Relations; +using Hexecs.Collections; + +namespace Hexecs.Actors.Relations; internal sealed partial class ActorRelationPool : IActorRelationPool where T : struct @@ -17,7 +19,18 @@ public Type Type get => typeof(T); } - internal readonly Dictionary Relations; + private T[] _values; + private RelationKey[] _keys; + private int _count; + private readonly Dictionary _indexMap; + + private Bucket[]?[] _sparsePages; + private const int PageBits = 10; // 1024 + private const int PageSize = 1 << PageBits; + private const int PageMask = PageSize - 1; + + internal ReadOnlySpan Keys => _keys.AsSpan(0, _count); + internal T[] Values => _values; public ActorRelationPool(ActorContext context, int capacity = 16) { @@ -25,82 +38,219 @@ public ActorRelationPool(ActorContext context, int capacity = 16) capacity = HashHelper.GetPrime(capacity); - Relations = new Dictionary(capacity); + _values = ArrayUtils.Create(capacity); + _keys = ArrayUtils.Create(capacity); + _indexMap = new Dictionary(capacity); + _sparsePages = []; + _count = 0; } public ref T Add(uint subject, uint relative, in T value) { var key = new RelationKey(subject, relative); - ref var relation = ref CollectionsMarshal.GetValueRefOrAddDefault(Relations, key, out var exists); - if (exists) ActorError.RelationAlreadyExists(subject, relative); + // Если такая связь уже есть (неважно, кто субъект, а кто цель), выбрасываем ошибку + if (_indexMap.ContainsKey(key)) ActorError.RelationAlreadyExists(subject, relative); + + if (_count >= _values.Length) + { + var newCapacity = _values.Length * 2; + ArrayUtils.Resize(ref _values, newCapacity); + ArrayUtils.Resize(ref _keys, newCapacity); + } + + var index = _count++; + _keys[index] = key; + _values[index] = value; + _indexMap[key] = index; + + // ВАЖНО: Добавляем индекс в списки ОБОИХ участников + AddToAdjacency(subject, index); + // Если это не связь сам с собой, добавляем и второму + if (subject != relative) + { + AddToAdjacency(relative, index); + } - relation = value; - return ref relation; + return ref _values[index]; } - public void Clear() + private void AddToAdjacency(uint actorId, int index) { - Relations.Clear(); + var pageIndex = (int)(actorId >> PageBits); + if (pageIndex >= _sparsePages.Length) + { + ArrayUtils.EnsureCapacity(ref _sparsePages, pageIndex + 1); + } + + ref var page = ref _sparsePages[pageIndex]; + page ??= new Bucket[PageSize]; + + ref var bucket = ref page[actorId & PageMask]; + bucket.Add(index); } - public int Count(uint subject) + private void RemoveFromAdjacency(uint actorId, int index) + { + var pageIndex = (int)(actorId >> PageBits); + var page = _sparsePages[pageIndex]; + ref var bucket = ref page![actorId & PageMask]; + + var span = bucket.AsSpan(); + for (var i = 0; i < span.Length; i++) + { + if (span[i] == index) + { + bucket.RemoveAtSwapBack(i); + return; + } + } + } + + private void ReplaceInAdjacency(uint actorId, int oldIndex, int newIndex) + { + var pageIndex = (int)(actorId >> PageBits); + var page = _sparsePages[pageIndex]; + ref var bucket = ref page![actorId & PageMask]; + var span = bucket.AsSpan(); + for (var i = 0; i < span.Length; i++) + { + if (span[i] == oldIndex) + { + span[i] = newIndex; + return; + } + } + } + + public void Clear() { - var result = 0; + _indexMap.Clear(); + ArrayUtils.Clear(_values, _count); + ArrayUtils.Clear(_keys, _count); - // ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator - foreach (var key in Relations.Keys) + foreach (var page in _sparsePages) { - if (key.First == subject) result++; + if (page == null) continue; + for (var i = 0; i < page.Length; i++) + { + page[i].Dispose(); + } } - return result; + _count = 0; + } + + public int Count(uint subject) + { + var pageIndex = (int)(subject >> PageBits); + if (pageIndex >= _sparsePages.Length) return 0; + var page = _sparsePages[pageIndex]; + if (page == null) return 0; + return page[subject & PageMask].Length; } public ref T Get(uint subject, uint relative) { var key = new RelationKey(subject, relative); - ref var relation = ref CollectionsMarshal.GetValueRefOrNullRef(Relations, key); - if (Unsafe.IsNullRef(ref relation)) ActorError.RelationNotFound(subject, relative); + if (_indexMap.TryGetValue(key, out var index)) + { + return ref _values[index]; + } - return ref relation; + ActorError.RelationNotFound(subject, relative); + return ref Unsafe.NullRef(); } - public ActorRelationEnumerator GetRelations(uint subject) => new(this, subject); + public ActorRelationEnumerator GetRelations(uint subject) + { + var pageIndex = (int)(subject >> PageBits); + if (pageIndex >= _sparsePages.Length) return ActorRelationEnumerator.Empty; + var page = _sparsePages[pageIndex]; + if (page == null) return ActorRelationEnumerator.Empty; + + return new ActorRelationEnumerator(this, page[subject & PageMask].AsReadOnlySpan(), subject); + } public bool Has(uint subject, uint relative) { var key = new RelationKey(subject, relative); - return Relations.ContainsKey(key); + return _indexMap.ContainsKey(key); } public bool Remove(uint subject) { - var arrayPool = ArrayPool.Shared; - var buffer = arrayPool.Rent(16); - var length = 0; - - foreach (var key in Relations.Keys) - { - if (!key.Is(subject)) continue; + var pageIndex = (int)(subject >> PageBits); + if (pageIndex >= _sparsePages.Length) return false; + var page = _sparsePages[pageIndex]; + if (page == null) return false; - ArrayUtils.Insert(ref buffer, arrayPool, length, key); - length++; - } + ref var bucket = ref page[subject & PageMask]; + if (bucket.Length == 0) return false; - foreach (var key in buffer.AsSpan(0, length)) + var removedAny = false; + // Итерируемся с конца бакета, так как при Remove связь удаляется из бакета + // (но Swap-Back в основном пуле все равно может перемешать индексы других связей в этом же бакете) + while (bucket.Length > 0) { - Relations.Remove(key); + // Берем всегда первый индекс из бакета + var index = bucket.AsReadOnlySpan()[0]; + var key = _keys[index]; + if (Remove(key.First, key.Second, out _)) + { + removedAny = true; + } + else + { + // Защита от бесконечного цикла, если что-то пошло не так + break; + } } - arrayPool.Return(buffer); - - return length > 0; + return removedAny; } public bool Remove(uint subject, uint relative, out T removed) { var key = new RelationKey(subject, relative); - return Relations.Remove(key, out removed); + if (!_indexMap.TryGetValue(key, out var index)) + { + removed = default; + return false; + } + + removed = _values[index]; + _indexMap.Remove(key); + + // Удаляем из списков смежности ОБОИХ участников + RemoveFromAdjacency(key.First, index); + if (key.First != key.Second) + { + RemoveFromAdjacency(key.Second, index); + } + + var lastIndex = _count - 1; + if (index != lastIndex) + { + var lastKey = _keys[lastIndex]; + var lastValue = _values[lastIndex]; + + _keys[index] = lastKey; + _values[index] = lastValue; + _indexMap[lastKey] = index; + + // Обновляем индексы в списках смежности для перемещенной связи у ОБОИХ участников + ReplaceInAdjacency(lastKey.First, lastIndex, index); + if (lastKey.First != lastKey.Second) + { + ReplaceInAdjacency(lastKey.Second, lastIndex, index); + } + } + + _keys[lastIndex] = default; + _values[lastIndex] = default; + _count--; + + return true; } [DebuggerDisplay("{First} to {Second}")] diff --git a/src/Hexecs/Collections/Bucket.cs b/src/Hexecs/Collections/Bucket.cs index 06cb234..f69f0f9 100644 --- a/src/Hexecs/Collections/Bucket.cs +++ b/src/Hexecs/Collections/Bucket.cs @@ -94,23 +94,30 @@ public readonly bool Has(T item, IEqualityComparer? equalityComparer = null) public bool Remove(T item, IEqualityComparer? equalityComparer = null) { - if (_length == 0) return false; + var index = IndexOf(item, equalityComparer); + if (index == -1) return false; - equalityComparer ??= EqualityComparer.Default; + RemoveAtSwapBack(index); + return true; + } - var span = AsReadOnlySpan(); - for (var i = 0; i < span.Length; i++) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RemoveAtSwapBack(int index) + { + var lastIndex = _length - 1; + if (index < lastIndex) { - if (!equalityComparer.Equals(span[i], item)) continue; + _array![index] = _array[lastIndex]; + } - ArrayUtils.Cut(_array!, i); - _length--; - return true; + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + { + _array![lastIndex] = default!; } - return false; + _length--; } - + [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly T[] ToArray() => AsReadOnlySpan().ToArray(); From acd5ab8887a88a91cda0a0081840c69ea3dec076 Mon Sep 17 00:00:00 2001 From: Kirill Bazhaikin Date: Wed, 14 Jan 2026 19:56:12 +0300 Subject: [PATCH 08/11] relations refactor --- .../Actors/ActorHierarchyBenchmark.cs | 83 +++++++++++++++++++ .../Actors/ActorRelationBenchmark.cs | 21 ++--- src/Hexecs.Benchmarks/Program.cs | 2 +- 3 files changed, 95 insertions(+), 11 deletions(-) create mode 100644 src/Hexecs.Benchmarks/Actors/ActorHierarchyBenchmark.cs diff --git a/src/Hexecs.Benchmarks/Actors/ActorHierarchyBenchmark.cs b/src/Hexecs.Benchmarks/Actors/ActorHierarchyBenchmark.cs new file mode 100644 index 0000000..3dded9a --- /dev/null +++ b/src/Hexecs.Benchmarks/Actors/ActorHierarchyBenchmark.cs @@ -0,0 +1,83 @@ +using Hexecs.Worlds; +using System.Buffers; + +namespace Hexecs.Benchmarks.Actors; + +[SimpleJob(RuntimeMoniker.Net10_0)] +[MeanColumn, MemoryDiagnoser] +[HideColumns("Job", "Error", "StdDev", "Median", "RatioSD")] +[BenchmarkCategory("Actors")] +public class ActorHierarchyBenchmark +{ + [Params(100, 1_000, 2_000)] public int Count; + + private ActorContext _actorContext = null!; + private Actor[] _parents = null!; + private Actor[] _children = null!; + private World _world = null!; + + [Benchmark] + public int Do() + { + // Часть 1: Построение иерархии + // Для каждого родителя добавляем Count детей (аналогично бенчмарку отношений) + var childIdx = 0; + for (var i = 0; i < _parents.Length; i++) + { + var parent = _parents[i]; + for (var j = 0; j < Count; j++) + { + parent.AddChild(_children[childIdx++]); + } + } + + var result = 0; + var buffer = ArrayPool.Shared.Rent(Count); + + // Часть 2: Итерация и удаление + for (var i = 0; i < _parents.Length; i++) + { + var parent = _parents[i]; + var children = parent.Children(); + var k = 0; + + // Сбор детей в буфер + foreach (var child in children) + { + buffer[k++] = child.Id; + result++; + } + + // Удаление детей + for (var j = 0; j < k; j++) + { + parent.RemoveChild(new Actor(_actorContext, buffer[j])); + } + } + + ArrayPool.Shared.Return(buffer); + return result; + } + + [GlobalSetup] + public void Setup() + { + _world = new WorldBuilder().Build(); + _actorContext = _world.Actors; + + _parents = new Actor[Count]; + _children = new Actor[Count * Count]; + + for (var i = 0; i < Count; i++) + { + _parents[i] = _actorContext.CreateActor(); + for (var j = 0; j < Count; j++) + { + _children[i * Count + j] = _actorContext.CreateActor(); + } + } + } + + [GlobalCleanup] + public void Cleanup() => _world.Dispose(); +} \ No newline at end of file diff --git a/src/Hexecs.Benchmarks/Actors/ActorRelationBenchmark.cs b/src/Hexecs.Benchmarks/Actors/ActorRelationBenchmark.cs index 85ea265..881eb71 100644 --- a/src/Hexecs.Benchmarks/Actors/ActorRelationBenchmark.cs +++ b/src/Hexecs.Benchmarks/Actors/ActorRelationBenchmark.cs @@ -5,17 +5,18 @@ namespace Hexecs.Benchmarks.Actors; // BenchmarkDotNet v0.15.8, macOS Tahoe 26.2 (25C56) [Darwin 25.2.0] -// Apple M3 Max, 1 CPU, 16 logical and 16 physical cores -// .NET SDK 10.0.101 -// [Host] : .NET 10.0.1 (10.0.1, 10.0.125.57005), Arm64 RyuJIT armv8.0-a -// .NET 10.0 : .NET 10.0.1 (10.0.1, 10.0.125.57005), Arm64 RyuJIT armv8.0-a +// Apple M3 Max, 1 CPU, 16 logical and 16 physical cores +// .NET SDK 10.0.101 +// [Host] : .NET 10.0.1 (10.0.1, 10.0.125.57005), Arm64 RyuJIT armv8.0-a +// .NET 10.0 : .NET 10.0.1 (10.0.1, 10.0.125.57005), Arm64 RyuJIT armv8.0-a +// +// Job=.NET 10.0 Runtime=.NET 10.0 // -// Job=.NET 10.0 Runtime=.NET 10.0 Error=2.80 ms -// StdDev=2.34 ms -// -// | Method | Count | Mean | Allocated | -// |------- |------ |---------:|----------:| -// | Do | 1000 | 254.5 ms | - | +// | Method | Count | Mean | Allocated | +// |------- |------ |---------------:|----------:| +// | Do | 100 | 798.9 us | - | +// | Do | 1000 | 261,046.6 us | - | +// | Do | 2000 | 2,049,070.5 us | - | [SimpleJob(RuntimeMoniker.Net10_0)] [Orderer(SummaryOrderPolicy.FastestToSlowest)] diff --git a/src/Hexecs.Benchmarks/Program.cs b/src/Hexecs.Benchmarks/Program.cs index 7aa34dc..9e90450 100644 --- a/src/Hexecs.Benchmarks/Program.cs +++ b/src/Hexecs.Benchmarks/Program.cs @@ -1,5 +1,5 @@ using BenchmarkDotNet.Running; using Hexecs.Benchmarks.Actors; -BenchmarkRunner.Run(); +BenchmarkRunner.Run(); //BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); \ No newline at end of file From 3082806dc34a6a7171b1a1b15b0a48bb0fad1d35 Mon Sep 17 00:00:00 2001 From: Kirill Bazhaykin Date: Thu, 15 Jan 2026 14:24:42 +0300 Subject: [PATCH 09/11] 0.4.3.3 add more benchmarks --- Directory.Packages.props | 53 ++++---- .../ActorCheckComponentExistsBenchmark.cs | 57 +++++++-- ...ctorCreateAddComponentsDestroyBenchmark.cs | 115 ++++++++++++++---- .../ActorFilter2EnumerationBenchmark.cs | 3 +- .../ActorFilter3EnumerationBenchmark.cs | 73 +++++++++-- .../Actors/ActorRelationBenchmark.cs | 98 +++++++++++++-- ...UpdateSystemWithParallelWorkerBenchmark.cs | 52 ++++++-- .../Collections/ThreadLocalStackBenchmark.cs | 1 - .../Hexecs.Benchmarks.csproj | 1 + .../Mocks/ActorComponents/Attack.cs | 4 +- .../Mocks/ActorComponents/Defence.cs | 4 +- .../Mocks/ActorComponents/Employee.cs | 4 +- .../ActorComponents/EmployeeAgreement.cs | 7 +- .../Mocks/ActorComponents/Employer.cs | 4 +- .../Mocks/ActorComponents/Speed.cs | 6 +- src/Hexecs.Benchmarks/Program.cs | 5 +- 16 files changed, 377 insertions(+), 110 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index e23cfad..23f099b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,31 +1,26 @@  - - true - true - - - - - - - - - - - - - - - - - - - - - - - - - - + + true + true + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Hexecs.Benchmarks/Actors/ActorCheckComponentExistsBenchmark.cs b/src/Hexecs.Benchmarks/Actors/ActorCheckComponentExistsBenchmark.cs index 963e196..64d3a92 100644 --- a/src/Hexecs.Benchmarks/Actors/ActorCheckComponentExistsBenchmark.cs +++ b/src/Hexecs.Benchmarks/Actors/ActorCheckComponentExistsBenchmark.cs @@ -1,5 +1,5 @@ using System.Runtime.CompilerServices; -using Hexecs.Benchmarks.Mocks; +using Friflo.Engine.ECS; using Hexecs.Benchmarks.Mocks.ActorComponents; using Hexecs.Worlds; using World = Hexecs.Worlds.World; @@ -8,18 +8,25 @@ namespace Hexecs.Benchmarks.Actors; // BenchmarkDotNet v0.15.8, Windows 11 (10.0.22621.4317/22H2/2022Update/SunValley2) // Intel Xeon CPU E5-2697 v3 2.60GHz, 2 CPU, 56 logical and 28 physical cores -// .NET SDK 10.0.100 -// [Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 -// .NET 10.0 : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 +// .NET SDK 10.0.100 +// [Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 +// .NET 10.0 : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 // // Job=.NET 10.0 Runtime=.NET 10.0 // -// | Method | Mean | Ratio | Allocated | Alloc Ratio | -// |----------------- |---------:|------:|----------:|------------:| -// | Hexecs_Is | 307.7 us | 0.90 | - | NA | -// | Hexecs_Has | 342.4 us | 1.00 | - | NA | -// | Hexecs_Reference | 380.7 us | 1.11 | - | NA | -// | DefaultEcs_Has | 713.7 us | 2.08 | - | NA | +// | Method | Count | Mean | Ratio | Allocated | Alloc Ratio | +// |----------------- |------- |----------:|------:|----------:|------------:| +// | Hexecs_Is | 10000 | 20.42 us | 0.96 | - | NA | +// | Hexecs_Has | 10000 | 21.19 us | 1.00 | - | NA | +// | Hexecs_Reference | 10000 | 24.30 us | 1.15 | - | NA | +// | FriFlo_Has | 10000 | 40.28 us | 1.90 | - | NA | +// | DefaultEcs_Has | 10000 | 73.24 us | 3.46 | - | NA | +// | | | | | | | +// | Hexecs_Is | 100000 | 204.98 us | 0.94 | - | NA | +// | Hexecs_Has | 100000 | 219.12 us | 1.00 | - | NA | +// | Hexecs_Reference | 100000 | 251.83 us | 1.15 | - | NA | +// | FriFlo_Has | 100000 | 409.48 us | 1.87 | - | NA | +// | DefaultEcs_Has | 100000 | 712.00 us | 3.25 | - | NA | // // ------------------------------------------------------------------------------------ // @@ -46,7 +53,7 @@ namespace Hexecs.Benchmarks.Actors; [SimpleJob(RuntimeMoniker.Net10_0)] [Orderer(SummaryOrderPolicy.FastestToSlowest)] [MeanColumn, MemoryDiagnoser] -[HideColumns("Job", "Error", "StdDev", "Median", "RatioSD", "Count")] +[HideColumns("Job", "Error", "StdDev", "Median", "RatioSD")] [JsonExporterAttribute.Full] [JsonExporterAttribute.FullCompressed] [BenchmarkCategory("Actors")] @@ -56,6 +63,8 @@ public class ActorCheckComponentExistsBenchmark private ActorContext _context = null!; private DefaultEcs.World _defaultWorld = null!; + private EntityStore _frifloWorld = null!; + private ArchetypeQuery _frifloAllEntitiesQuery = null!; private World _world = null!; [Benchmark(Baseline = true)] @@ -89,6 +98,24 @@ public int DefaultEcs_Has() return result; } + [Benchmark] + public int FriFlo_Has() + { + var result = 0; + + // ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator + foreach (var entity in _frifloAllEntitiesQuery.Entities) + { + if (entity.HasComponent()) + { + result++; + } + } + + return result; + } + + [Benchmark] public int Hexecs_Is() { @@ -130,6 +157,8 @@ public void Cleanup() _defaultWorld.Dispose(); _defaultWorld = null!; + _frifloWorld = null!; + _world.Dispose(); _world = null!; } @@ -138,6 +167,8 @@ public void Cleanup() public void Setup() { _defaultWorld = new DefaultEcs.World(); + _frifloWorld = new EntityStore(); + _frifloAllEntitiesQuery = _frifloWorld.Query(); _world = new WorldBuilder().Build(); _context = _world.Actors; @@ -151,10 +182,14 @@ public void Setup() defaultEntity.Set(); defaultEntity.Set(); + var frifloEntity = _frifloWorld.CreateEntity(new Attack(), new Defence()); + if (i % 10 != 0) continue; actor.Add(new Speed()); + defaultEntity.Set(); + frifloEntity.Add(new Speed()); } } } \ No newline at end of file diff --git a/src/Hexecs.Benchmarks/Actors/ActorCreateAddComponentsDestroyBenchmark.cs b/src/Hexecs.Benchmarks/Actors/ActorCreateAddComponentsDestroyBenchmark.cs index 5e2e1ea..38f1345 100644 --- a/src/Hexecs.Benchmarks/Actors/ActorCreateAddComponentsDestroyBenchmark.cs +++ b/src/Hexecs.Benchmarks/Actors/ActorCreateAddComponentsDestroyBenchmark.cs @@ -1,29 +1,35 @@ -using Hexecs.Benchmarks.Mocks; +using DefaultEcs; +using Friflo.Engine.ECS; using Hexecs.Benchmarks.Mocks.ActorComponents; using Hexecs.Worlds; +using World = Hexecs.Worlds.World; namespace Hexecs.Benchmarks.Actors; // BenchmarkDotNet v0.15.8, Windows 11 (10.0.22621.4317/22H2/2022Update/SunValley2) // Intel Xeon CPU E5-2697 v3 2.60GHz, 2 CPU, 56 logical and 28 physical cores -// .NET SDK 10.0.100 -// [Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 -// .NET 10.0 : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 +// .NET SDK 10.0.100 +// [Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 +// .NET 10.0 : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 // // Job=.NET 10.0 Runtime=.NET 10.0 // -// | Method | Count | Mean | Ratio | Gen0 | Allocated | Alloc Ratio | -// |---------------------------- |------- |-------------:|------:|---------:|-----------:|------------:| -// | DefaultEcs_CreateAddDestroy | 1000 | 411.8 us | 0.72 | 1.4648 | 32000 B | NA | -// | Hexecs_CreateAddDestroy | 1000 | 574.8 us | 1.00 | - | - | NA | -// | | | | | | | | -// | Hexecs_CreateAddDestroy | 100000 | 76,251.7 us | 1.00 | - | 40 B | 1.00 | -// | DefaultEcs_CreateAddDestroy | 100000 | 90,755.1 us | 1.19 | 166.6667 | 3200040 B | 80,001.00 | -// | | | | | | | | -// | DefaultEcs_CreateAddDestroy | 500000 | 470,864.8 us | 0.86 | - | 16000040 B | 400,001.00 | -// | Hexecs_CreateAddDestroy | 500000 | 548,102.0 us | 1.00 | - | 40 B | 1.00 | +// | Method | Count | Mean | Ratio | Gen0 | Allocated | Alloc Ratio | +// |---------------------------- |------- |-------------:|------:|-------:|-----------:|------------:| +// | FriFlo_CreateAddDestroy | 1000 | 153.5 us | 0.29 | - | - | NA | +// | DefaultEcs_CreateAddDestroy | 1000 | 401.1 us | 0.77 | 1.4648 | 32000 B | NA | +// | Hexecs_CreateAddDestroy | 1000 | 523.6 us | 1.00 | - | - | NA | +// | | | | | | | | +// | FriFlo_CreateAddDestroy | 100000 | 16,519.0 us | 0.25 | - | 40 B | 1.00 | +// | Hexecs_CreateAddDestroy | 100000 | 65,924.0 us | 1.00 | - | 40 B | 1.00 | +// | DefaultEcs_CreateAddDestroy | 100000 | 105,603.9 us | 1.60 | - | 3200040 B | 80,001.00 | +// | | | | | | | | +// | FriFlo_CreateAddDestroy | 500000 | 85,496.9 us | 0.18 | - | 40 B | 1.00 | +// | Hexecs_CreateAddDestroy | 500000 | 474,476.8 us | 1.00 | - | 40 B | 1.00 | +// | DefaultEcs_CreateAddDestroy | 500000 | 539,368.3 us | 1.14 | - | 16000040 B | 400,001.00 | // // ------------------------------------------------------------------------------------ +// // BenchmarkDotNet v0.15.8, macOS Tahoe 26.2 (25C56) [Darwin 25.2.0] // Apple M3 Max, 1 CPU, 16 logical and 16 physical cores // .NET SDK 10.0.101 @@ -54,10 +60,14 @@ public class ActorCreateAddComponentsDestroyBenchmark { [Params(1_000, 100_000, 500_000)] public int Count; - private List _defaultSets = null!; private List _defaultEntities = null!; + private List _defaultSets = null!; private DefaultEcs.World _defaultWorld = null!; + private List _frifloEntities = null!; + private List _frifloQueries = null!; + private EntityStore _frifloWorld = null!; + private List _hexecsActors = null!; private ActorContext _hexecsContext = null!; private List _hexecsFilters = null!; @@ -117,12 +127,41 @@ public int DefaultEcs_CreateAddDestroy() return _defaultSets.Sum(static x => x.Count); } + [Benchmark] + public int FriFlo_CreateAddDestroy() + { + _frifloEntities.Clear(); + + for (var i = 0; i < Count; i++) + { + var entity = _frifloWorld.CreateEntity(); + entity.AddComponent(new Attack { Value = i }); + entity.AddComponent(new Defence()); + entity.AddComponent(new Speed()); + + _frifloEntities.Add(entity); + } + + foreach (var entity in _frifloEntities) + { + entity.RemoveComponent(); + entity.RemoveComponent(); + entity.RemoveComponent(); + + entity.DeleteEntity(); + } + + return _frifloQueries.Sum(static x => x.Count); + } + [GlobalCleanup] public void Cleanup() { _defaultWorld.Dispose(); _defaultWorld = null!; + _frifloWorld = null!; + _hexecsWorld.Dispose(); _hexecsWorld = null!; } @@ -143,6 +182,19 @@ public void Setup() _defaultWorld.GetEntities().With().With().With().AsSet() ]; + _frifloEntities = new List(Count); + _frifloWorld = new EntityStore(); + _frifloQueries = + [ + _frifloWorld.Query(), + _frifloWorld.Query(), + _frifloWorld.Query(), + _frifloWorld.Query(), + _frifloWorld.Query(), + _frifloWorld.Query(), + _frifloWorld.Query() + ]; + _hexecsActors = new List(Count); _hexecsWorld = new WorldBuilder().Build(); _hexecsContext = _hexecsWorld.Actors; @@ -155,35 +207,46 @@ public void Setup() _hexecsContext.Filter(), _hexecsContext.Filter(), _hexecsContext.Filter() - // warmup ]; // warmup for (var i = 0; i < Count; i++) { + var defaultEntity = _defaultWorld.CreateEntity(); + defaultEntity.Set(); + defaultEntity.Set(); + defaultEntity.Set(); + + _defaultEntities.Add(defaultEntity); + + var frifloEntity = _frifloWorld.CreateEntity(new Attack(), new Defence(), new Speed()); + _frifloEntities.Add(frifloEntity); + var actor = _hexecsContext.CreateActor(); actor.Add(new Attack()); actor.Add(new Defence()); actor.Add(new Speed()); _hexecsActors.Add(actor); + } - var defaultEntity = _defaultWorld.CreateEntity(); - defaultEntity.Set(); - defaultEntity.Set(); - defaultEntity.Set(); - - _defaultEntities.Add(defaultEntity); + foreach (var entity in _defaultEntities) + { + entity.Dispose(); } - foreach (var actor in _hexecsActors) + foreach (var entity in _frifloEntities) { - actor.Destroy(); + entity.DeleteEntity(); } - foreach (var entity in _defaultEntities) + foreach (var actor in _hexecsActors) { - entity.Dispose(); + actor.Destroy(); } + + _defaultEntities.Clear(); + _frifloEntities.Clear(); + _hexecsActors.Clear(); } } \ No newline at end of file diff --git a/src/Hexecs.Benchmarks/Actors/ActorFilter2EnumerationBenchmark.cs b/src/Hexecs.Benchmarks/Actors/ActorFilter2EnumerationBenchmark.cs index f9ed7f7..faaec4a 100644 --- a/src/Hexecs.Benchmarks/Actors/ActorFilter2EnumerationBenchmark.cs +++ b/src/Hexecs.Benchmarks/Actors/ActorFilter2EnumerationBenchmark.cs @@ -1,5 +1,4 @@ -using Hexecs.Benchmarks.Mocks; -using Hexecs.Benchmarks.Mocks.ActorComponents; +using Hexecs.Benchmarks.Mocks.ActorComponents; using Hexecs.Worlds; namespace Hexecs.Benchmarks.Actors; diff --git a/src/Hexecs.Benchmarks/Actors/ActorFilter3EnumerationBenchmark.cs b/src/Hexecs.Benchmarks/Actors/ActorFilter3EnumerationBenchmark.cs index cd3c95c..a8903e4 100644 --- a/src/Hexecs.Benchmarks/Actors/ActorFilter3EnumerationBenchmark.cs +++ b/src/Hexecs.Benchmarks/Actors/ActorFilter3EnumerationBenchmark.cs @@ -1,4 +1,4 @@ -using Hexecs.Benchmarks.Mocks; +using Friflo.Engine.ECS; using Hexecs.Benchmarks.Mocks.ActorComponents; using Hexecs.Worlds; using World = Hexecs.Worlds.World; @@ -7,16 +7,23 @@ namespace Hexecs.Benchmarks.Actors; // BenchmarkDotNet v0.15.8, Windows 11 (10.0.22621.4317/22H2/2022Update/SunValley2) // Intel Xeon CPU E5-2697 v3 2.60GHz, 2 CPU, 56 logical and 28 physical cores -// .NET SDK 10.0.100 -// [Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 -// .NET 10.0 : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 +// .NET SDK 10.0.100 +// [Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 +// .NET 10.0 : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 // // Job=.NET 10.0 Runtime=.NET 10.0 // -// | Method | Mean | Ratio | Allocated | Alloc Ratio | -// |----------- |---------:|------:|----------:|------------:| -// | Hexecs | 69.55 us | 1.00 | - | NA | -// | DefaultEcs | 69.89 us | 1.00 | - | NA | +// | Method | Count | Mean | Ratio | Allocated | Alloc Ratio | +// |-------------- |------- |----------:|------:|----------:|------------:| +// | FriFlo_Chunks | 10000 | 16.28 us | 0.63 | - | NA | +// | Hexecs | 10000 | 25.99 us | 1.00 | - | NA | +// | FriFlo | 10000 | 26.76 us | 1.03 | 88 B | NA | +// | DefaultEcs | 10000 | 29.78 us | 1.15 | - | NA | +// | | | | | | | +// | FriFlo_Chunks | 100000 | 156.83 us | 0.58 | - | NA | +// | FriFlo | 100000 | 263.76 us | 0.98 | 88 B | NA | +// | Hexecs | 100000 | 268.25 us | 1.00 | - | NA | +// | DefaultEcs | 100000 | 289.78 us | 1.08 | - | NA | // // ------------------------------------------------------------------------------------ // @@ -53,6 +60,9 @@ public class ActorFilter3EnumerationBenchmark private DefaultEcs.World _defaultWorld = null!; private DefaultEcs.EntitySet _defaultEntitySet = null!; + private EntityStore _frifloWorld = null!; + private ArchetypeQuery _frifloQuery = null!; + [Benchmark(Baseline = true)] public int Hexecs() { @@ -85,6 +95,43 @@ public int DefaultEcs() return result; } + [Benchmark] + public int FriFlo() + { + var result = 0; + + _frifloQuery.ForEachEntity((ref attack, ref defence, ref speed, _) => + { + result += attack.Value + + defence.Value + + speed.Value; + }); + + return result; + } + + [Benchmark] + public int FriFlo_Chunks() + { + var result = 0; + + foreach (var queryChunk in _frifloQuery.Chunks) + { + var attacks = queryChunk.Chunk1; + var defences = queryChunk.Chunk2; + var speeds = queryChunk.Chunk3; + + for (var i = 0; i < queryChunk.Length; i++) + { + result += attacks[i].Value + + defences[i].Value + + speeds[i].Value; + } + } + + return result; + } + [GlobalCleanup] public void Cleanup() { @@ -99,23 +146,29 @@ public void Cleanup() public void Setup() { _defaultWorld = new DefaultEcs.World(); + _frifloWorld = new EntityStore(); _world = new WorldBuilder().Build(); _defaultEntitySet = _defaultWorld.GetEntities().With().With().With().AsSet(); _filter = _world.Actors.Filter(); + _frifloQuery = _frifloWorld.Query(); var context = _world.Actors; for (var i = 0; i < Count; i++) { + var attack = new Attack { Value = i }; + var actor = context.CreateActor(); - actor.Add(new Attack()); + actor.Add(in attack); actor.Add(new Defence()); actor.Add(new Speed()); var defaultEntity = _defaultWorld.CreateEntity(); - defaultEntity.Set(); + defaultEntity.Set(in attack); defaultEntity.Set(); defaultEntity.Set(); + + _frifloWorld.CreateEntity(attack, new Defence(), new Speed()); } } } \ No newline at end of file diff --git a/src/Hexecs.Benchmarks/Actors/ActorRelationBenchmark.cs b/src/Hexecs.Benchmarks/Actors/ActorRelationBenchmark.cs index 881eb71..933763b 100644 --- a/src/Hexecs.Benchmarks/Actors/ActorRelationBenchmark.cs +++ b/src/Hexecs.Benchmarks/Actors/ActorRelationBenchmark.cs @@ -1,9 +1,29 @@ using System.Buffers; +using Friflo.Engine.ECS; using Hexecs.Benchmarks.Mocks.ActorComponents; using Hexecs.Worlds; namespace Hexecs.Benchmarks.Actors; +// BenchmarkDotNet v0.15.8, Windows 11 (10.0.22621.4317/22H2/2022Update/SunValley2) +// Intel Xeon CPU E5-2697 v3 2.60GHz, 2 CPU, 56 logical and 28 physical cores +// .NET SDK 10.0.100 +// [Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 +// .NET 10.0 : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 +// +// Job=.NET 10.0 Runtime=.NET 10.0 +// +// | Method | Count | Mean | Ratio | Allocated | Alloc Ratio | +// |------- |------ |--------------:|------:|----------:|------------:| +// | Hexecs | 10 | 13.32 us | 1.00 | - | NA | +// | FriFlo | 10 | 15.66 us | 1.18 | - | NA | +// | | | | | | | +// | Hexecs | 100 | 1,929.54 us | 1.00 | - | NA | +// | FriFlo | 100 | 2,301.57 us | 1.19 | - | NA | +// | | | | | | | +// | Hexecs | 1000 | 604,757.61 us | 1.00 | - | NA | +// | FriFlo | 1000 | 807,112.05 us | 1.33 | - | NA | +// // BenchmarkDotNet v0.15.8, macOS Tahoe 26.2 (25C56) [Darwin 25.2.0] // Apple M3 Max, 1 CPU, 16 logical and 16 physical cores // .NET SDK 10.0.101 @@ -27,17 +47,23 @@ namespace Hexecs.Benchmarks.Actors; [BenchmarkCategory("Actors")] public class ActorRelationBenchmark { - [Params(100, 1_000, 2_000)] public int Count; + [Params(10, 100, 1_000)] public int Count; private ActorContext _actorContext = null!; + private Actor[] _actorBuffer = null!; private ActorFilter _employeeFilter = null!; private ActorFilter _employerFilter = null!; private World _world = null!; - [Benchmark] - public int Do() + private EntityStore _frifloWorld = null!; + private Entity[] _frifloBuffer = null!; + private ArchetypeQuery _frifloEmployees = null!; + private ArchetypeQuery _frifloEmployers = null!; + + [Benchmark(Baseline = true)] + public int Hexecs() { - // Часть 1: Наполнение (тут всё отлично) + // Часть 1: Наполнение using (var employeeEnumerator = _employeeFilter.GetEnumerator()) { foreach (var employer in _employerFilter) @@ -52,7 +78,7 @@ public int Do() } var result = 0; - var buffer = ArrayPool.Shared.Rent(Count); + var buffer = _actorBuffer; // Часть 2: Удаление foreach (var employer in _employerFilter) @@ -60,23 +86,64 @@ public int Do() var relations = employer.Relations(); var i = 0; - // Сначала копируем ID во временный буфер, чтобы не ломать итератор foreach (var relation in relations) { - buffer[i++] = relation.Id; + buffer[i++] = relation; } - // Теперь спокойно удаляем for (var j = 0; j < i; j++) { - if (_actorContext.RemoveRelation(employer.Id, buffer[j])) + if (employer.RemoveRelation(buffer[j])) + { + result++; + } + } + } + + return result; + } + + [Benchmark] + public int FriFlo() + { + // Часть 1: Наполнение + using (var employeeEnumerator = _frifloEmployees.Entities.GetEnumerator()) + { + foreach (var employer in _frifloEmployers.Entities) + { + for (var i = 0; i < Count; i++) + { + if (!employeeEnumerator.MoveNext()) break; + var employee = employeeEnumerator.Current; + employer.AddRelation(new EmployeeAgreement { Salary = i, Target = employee }); + } + } + } + + var result = 0; + var buffer = _frifloBuffer; + + // Часть 2: Удаление + foreach (var employer in _frifloEmployers.Entities) + { + // Получаем все связи данного типа у сущности + var relations = employer.GetRelations(); + var i = 0; + + foreach (var relation in relations) + { + buffer[i++] = relation.Target; + } + + for (var j = 0; j < i; j++) + { + if (employer.RemoveRelation(buffer[j])) { result++; } } } - ArrayPool.Shared.Return(buffer); return result; } @@ -91,20 +158,29 @@ public void Cleanup() public void Setup() { _world = new WorldBuilder().Build(); - _actorContext = _world.Actors; + _actorBuffer = new Actor[Count]; _employeeFilter = _actorContext.Filter(); _employerFilter = _actorContext.Filter(); + _frifloWorld = new EntityStore(); + _frifloBuffer = new Entity[Count]; + _frifloEmployees = _frifloWorld.Query(); + _frifloEmployers = _frifloWorld.Query(); + for (var i = 0; i < Count; i++) { var employer = _actorContext.CreateActor(); employer.Add(new Employer()); + _frifloWorld.CreateEntity(new Employer()); + for (var y = 0; y < Count; y++) { var employee = _actorContext.CreateActor(); employee.Add(new Employee()); + + _frifloWorld.CreateEntity(new Employee()); } } } diff --git a/src/Hexecs.Benchmarks/Actors/UpdateSystemWithParallelWorkerBenchmark.cs b/src/Hexecs.Benchmarks/Actors/UpdateSystemWithParallelWorkerBenchmark.cs index a7b6582..073fa81 100644 --- a/src/Hexecs.Benchmarks/Actors/UpdateSystemWithParallelWorkerBenchmark.cs +++ b/src/Hexecs.Benchmarks/Actors/UpdateSystemWithParallelWorkerBenchmark.cs @@ -1,24 +1,29 @@ using DefaultEcs.System; +using Friflo.Engine.ECS; using Hexecs.Actors.Systems; -using Hexecs.Benchmarks.Mocks; using Hexecs.Benchmarks.Mocks.ActorComponents; using Hexecs.Threading; using Hexecs.Worlds; namespace Hexecs.Benchmarks.Actors; -// BenchmarkDotNet v0.15.8, macOS Tahoe 26.2 (25C56) [Darwin 25.2.0] -// Apple M3 Max, 1 CPU, 16 logical and 16 physical cores -// .NET SDK 10.0.101 -// [Host] : .NET 10.0.1 (10.0.1, 10.0.125.57005), Arm64 RyuJIT armv8.0-a -// .NET 10.0 : .NET 10.0.1 (10.0.1, 10.0.125.57005), Arm64 RyuJIT armv8.0-a +// BenchmarkDotNet v0.15.8, Windows 11 (10.0.22621.4317/22H2/2022Update/SunValley2) +// Intel Xeon CPU E5-2697 v3 2.60GHz, 2 CPU, 56 logical and 28 physical cores +// .NET SDK 10.0.100 +// [Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 +// .NET 10.0 : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 // // Job=.NET 10.0 Runtime=.NET 10.0 // -// | Method | Mean | Ratio | Allocated | Alloc Ratio | -// |-------------------- |-----------:|------:|----------:|------------:| -// | Hexecs_Parallel | 769.8 us | 1.00 | - | NA | -// | DefaultEcs_Parallel | 1,576.7 us | 2.05 | - | NA | +// | Method | Count | Mean | Ratio | Allocated | Alloc Ratio | +// |-------------------- |-------- |------------:|------:|----------:|------------:| +// | FriFlo_Parallel | 100000 | 94.42 us | 0.77 | - | NA | +// | Hexecs_Parallel | 100000 | 122.17 us | 1.00 | - | NA | +// | DefaultEcs_Parallel | 100000 | 221.83 us | 1.82 | - | NA | +// | | | | | | | +// | FriFlo_Parallel | 1000000 | 842.69 us | 0.91 | - | NA | +// | Hexecs_Parallel | 1000000 | 931.08 us | 1.00 | - | NA | +// | DefaultEcs_Parallel | 1000000 | 2,370.75 us | 2.55 | - | NA | // // ------------------------------------------------------------------------------------ // @@ -55,6 +60,10 @@ public class UpdateSystemWithParallelWorkerBenchmark private World _hexecsWorld = null!; private HexecsUpdateParallelSystem _hexecsSystem = null!; + private EntityStore _frifloWorld = null!; + private ArchetypeQuery _frifloQuery = null!; + private QueryJob _frifloJob = null!; + private WorldTime _state; [Benchmark(Baseline = true)] @@ -71,12 +80,21 @@ public int DefaultEcs_Parallel() return _defaultSystem.Set.Count; } + [Benchmark] + public int FriFlo_Parallel() + { + _frifloJob.RunParallel(); + return _frifloQuery.Count; + } + [GlobalCleanup] public void Cleanup() { _defaultSystem.Dispose(); _defaultWorld.Dispose(); _hexecsWorld.Dispose(); + + _frifloJob.JobRunner.Dispose(); } [GlobalSetup] @@ -93,6 +111,18 @@ public void Setup() .Build(); _hexecsSystem = _hexecsWorld.Actors.GetUpdateSystem(); + _frifloWorld = new EntityStore { JobRunner = new ParallelJobRunner(4) }; + _frifloQuery = _frifloWorld.Query(); + _frifloJob = _frifloQuery.ForEach((a, d, s, ch) => + { + for (var i = 0; i < ch.Length; i++) + { + a[i].Value++; + d[i].Value++; + s[i].Value++; + } + }); + _state = new WorldTime(); var context = _hexecsWorld.Actors; @@ -107,6 +137,8 @@ public void Setup() defaultEntity.Set(); defaultEntity.Set(); defaultEntity.Set(); + + _frifloWorld.CreateEntity(new Attack(), new Defence(), new Speed()); } } diff --git a/src/Hexecs.Benchmarks/Collections/ThreadLocalStackBenchmark.cs b/src/Hexecs.Benchmarks/Collections/ThreadLocalStackBenchmark.cs index 761efee..3458161 100644 --- a/src/Hexecs.Benchmarks/Collections/ThreadLocalStackBenchmark.cs +++ b/src/Hexecs.Benchmarks/Collections/ThreadLocalStackBenchmark.cs @@ -1,5 +1,4 @@ using System.Collections.Concurrent; -using System.ComponentModel; using Hexecs.Collections; namespace Hexecs.Benchmarks.Collections; diff --git a/src/Hexecs.Benchmarks/Hexecs.Benchmarks.csproj b/src/Hexecs.Benchmarks/Hexecs.Benchmarks.csproj index f0824d2..40fbedc 100644 --- a/src/Hexecs.Benchmarks/Hexecs.Benchmarks.csproj +++ b/src/Hexecs.Benchmarks/Hexecs.Benchmarks.csproj @@ -13,6 +13,7 @@ + diff --git a/src/Hexecs.Benchmarks/Mocks/ActorComponents/Attack.cs b/src/Hexecs.Benchmarks/Mocks/ActorComponents/Attack.cs index 8ec282e..8479b55 100644 --- a/src/Hexecs.Benchmarks/Mocks/ActorComponents/Attack.cs +++ b/src/Hexecs.Benchmarks/Mocks/ActorComponents/Attack.cs @@ -1,6 +1,8 @@ +using Friflo.Engine.ECS; + namespace Hexecs.Benchmarks.Mocks.ActorComponents; -public struct Attack : IActorComponent +public struct Attack : IActorComponent, IComponent { public int Value; } \ No newline at end of file diff --git a/src/Hexecs.Benchmarks/Mocks/ActorComponents/Defence.cs b/src/Hexecs.Benchmarks/Mocks/ActorComponents/Defence.cs index 5834629..62c521a 100644 --- a/src/Hexecs.Benchmarks/Mocks/ActorComponents/Defence.cs +++ b/src/Hexecs.Benchmarks/Mocks/ActorComponents/Defence.cs @@ -1,6 +1,8 @@ +using Friflo.Engine.ECS; + namespace Hexecs.Benchmarks.Mocks.ActorComponents; -public struct Defence : IActorComponent +public struct Defence : IActorComponent, IComponent { public int Value; } \ No newline at end of file diff --git a/src/Hexecs.Benchmarks/Mocks/ActorComponents/Employee.cs b/src/Hexecs.Benchmarks/Mocks/ActorComponents/Employee.cs index 44698de..a61b0cb 100644 --- a/src/Hexecs.Benchmarks/Mocks/ActorComponents/Employee.cs +++ b/src/Hexecs.Benchmarks/Mocks/ActorComponents/Employee.cs @@ -1,6 +1,8 @@ +using Friflo.Engine.ECS; + namespace Hexecs.Benchmarks.Mocks.ActorComponents; -public struct Employee : IActorComponent +public struct Employee : IActorComponent, IComponent { } \ No newline at end of file diff --git a/src/Hexecs.Benchmarks/Mocks/ActorComponents/EmployeeAgreement.cs b/src/Hexecs.Benchmarks/Mocks/ActorComponents/EmployeeAgreement.cs index ccfa7f9..6f153a8 100644 --- a/src/Hexecs.Benchmarks/Mocks/ActorComponents/EmployeeAgreement.cs +++ b/src/Hexecs.Benchmarks/Mocks/ActorComponents/EmployeeAgreement.cs @@ -1,6 +1,11 @@ +using Friflo.Engine.ECS; + namespace Hexecs.Benchmarks.Mocks.ActorComponents; -public struct EmployeeAgreement +public struct EmployeeAgreement : ILinkRelation { + public Entity Target; public int Salary; + + public Entity GetRelationKey() => Target; } \ No newline at end of file diff --git a/src/Hexecs.Benchmarks/Mocks/ActorComponents/Employer.cs b/src/Hexecs.Benchmarks/Mocks/ActorComponents/Employer.cs index da6849c..fe32ae2 100644 --- a/src/Hexecs.Benchmarks/Mocks/ActorComponents/Employer.cs +++ b/src/Hexecs.Benchmarks/Mocks/ActorComponents/Employer.cs @@ -1,3 +1,5 @@ +using Friflo.Engine.ECS; + namespace Hexecs.Benchmarks.Mocks.ActorComponents; -public struct Employer : IActorComponent; \ No newline at end of file +public struct Employer : IActorComponent, IComponent; \ No newline at end of file diff --git a/src/Hexecs.Benchmarks/Mocks/ActorComponents/Speed.cs b/src/Hexecs.Benchmarks/Mocks/ActorComponents/Speed.cs index 9fb5fa9..bb07917 100644 --- a/src/Hexecs.Benchmarks/Mocks/ActorComponents/Speed.cs +++ b/src/Hexecs.Benchmarks/Mocks/ActorComponents/Speed.cs @@ -1,6 +1,8 @@ -namespace Hexecs.Benchmarks.Mocks.ActorComponents; +using Friflo.Engine.ECS; -public struct Speed: IActorComponent +namespace Hexecs.Benchmarks.Mocks.ActorComponents; + +public struct Speed: IActorComponent, IComponent { public int Value; } \ No newline at end of file diff --git a/src/Hexecs.Benchmarks/Program.cs b/src/Hexecs.Benchmarks/Program.cs index 9e90450..3c07ae5 100644 --- a/src/Hexecs.Benchmarks/Program.cs +++ b/src/Hexecs.Benchmarks/Program.cs @@ -1,5 +1,4 @@ using BenchmarkDotNet.Running; -using Hexecs.Benchmarks.Actors; -BenchmarkRunner.Run(); -//BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); \ No newline at end of file +//BenchmarkRunner.Run(); +BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); \ No newline at end of file From 1c30db32a0d41985c11fe730ff852284c8bf0e07 Mon Sep 17 00:00:00 2001 From: Kirill Bazhaykin Date: Thu, 15 Jan 2026 17:30:40 +0300 Subject: [PATCH 10/11] cleanup --- .../Commands/Generate/GenerateTerrainHandler.cs | 1 - .../Actors/ActorCheckComponentExistsBenchmark.cs | 10 +++++----- .../Actors/ActorHierarchyBenchmark.cs | 3 ++- .../Mocks/ActorComponents/Employee.cs | 5 +---- src/Hexecs.Benchmarks/Mocks/ActorComponents/Speed.cs | 2 +- 5 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/Hexecs.Benchmarks.City/Terrains/Commands/Generate/GenerateTerrainHandler.cs b/src/Hexecs.Benchmarks.City/Terrains/Commands/Generate/GenerateTerrainHandler.cs index 34a4803..4b605c7 100644 --- a/src/Hexecs.Benchmarks.City/Terrains/Commands/Generate/GenerateTerrainHandler.cs +++ b/src/Hexecs.Benchmarks.City/Terrains/Commands/Generate/GenerateTerrainHandler.cs @@ -42,7 +42,6 @@ public override Result Handle(in GenerateTerrainCommand terrainCommand) { Context.BuildActor(urbanConcrete, args); } - else // just ground { Context.BuildActor(ground, args); diff --git a/src/Hexecs.Benchmarks/Actors/ActorCheckComponentExistsBenchmark.cs b/src/Hexecs.Benchmarks/Actors/ActorCheckComponentExistsBenchmark.cs index 64d3a92..8896d57 100644 --- a/src/Hexecs.Benchmarks/Actors/ActorCheckComponentExistsBenchmark.cs +++ b/src/Hexecs.Benchmarks/Actors/ActorCheckComponentExistsBenchmark.cs @@ -114,8 +114,8 @@ public int FriFlo_Has() return result; } - - + + [Benchmark] public int Hexecs_Is() { @@ -158,7 +158,7 @@ public void Cleanup() _defaultWorld = null!; _frifloWorld = null!; - + _world.Dispose(); _world = null!; } @@ -183,11 +183,11 @@ public void Setup() defaultEntity.Set(); var frifloEntity = _frifloWorld.CreateEntity(new Attack(), new Defence()); - + if (i % 10 != 0) continue; actor.Add(new Speed()); - + defaultEntity.Set(); frifloEntity.Add(new Speed()); } diff --git a/src/Hexecs.Benchmarks/Actors/ActorHierarchyBenchmark.cs b/src/Hexecs.Benchmarks/Actors/ActorHierarchyBenchmark.cs index 3dded9a..24e9a03 100644 --- a/src/Hexecs.Benchmarks/Actors/ActorHierarchyBenchmark.cs +++ b/src/Hexecs.Benchmarks/Actors/ActorHierarchyBenchmark.cs @@ -73,7 +73,8 @@ public void Setup() _parents[i] = _actorContext.CreateActor(); for (var j = 0; j < Count; j++) { - _children[i * Count + j] = _actorContext.CreateActor(); + var index = i * Count + j; + _children[index] = _actorContext.CreateActor(); } } } diff --git a/src/Hexecs.Benchmarks/Mocks/ActorComponents/Employee.cs b/src/Hexecs.Benchmarks/Mocks/ActorComponents/Employee.cs index a61b0cb..55e9ce9 100644 --- a/src/Hexecs.Benchmarks/Mocks/ActorComponents/Employee.cs +++ b/src/Hexecs.Benchmarks/Mocks/ActorComponents/Employee.cs @@ -2,7 +2,4 @@ namespace Hexecs.Benchmarks.Mocks.ActorComponents; -public struct Employee : IActorComponent, IComponent -{ - -} \ No newline at end of file +public struct Employee : IActorComponent, IComponent; \ No newline at end of file diff --git a/src/Hexecs.Benchmarks/Mocks/ActorComponents/Speed.cs b/src/Hexecs.Benchmarks/Mocks/ActorComponents/Speed.cs index bb07917..7483cdf 100644 --- a/src/Hexecs.Benchmarks/Mocks/ActorComponents/Speed.cs +++ b/src/Hexecs.Benchmarks/Mocks/ActorComponents/Speed.cs @@ -2,7 +2,7 @@ namespace Hexecs.Benchmarks.Mocks.ActorComponents; -public struct Speed: IActorComponent, IComponent +public struct Speed : IActorComponent, IComponent { public int Value; } \ No newline at end of file From 7e095256b6bfe01f1c28ec520854bf9f6ab51cae Mon Sep 17 00:00:00 2001 From: Kirill Bazhaykin Date: Thu, 15 Jan 2026 17:32:16 +0300 Subject: [PATCH 11/11] cleanup --- .../Actors/ActorCheckComponentExistsBenchmark.cs | 1 - src/Hexecs.Benchmarks/Actors/ActorHierarchyBenchmark.cs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Hexecs.Benchmarks/Actors/ActorCheckComponentExistsBenchmark.cs b/src/Hexecs.Benchmarks/Actors/ActorCheckComponentExistsBenchmark.cs index 8896d57..4670121 100644 --- a/src/Hexecs.Benchmarks/Actors/ActorCheckComponentExistsBenchmark.cs +++ b/src/Hexecs.Benchmarks/Actors/ActorCheckComponentExistsBenchmark.cs @@ -115,7 +115,6 @@ public int FriFlo_Has() return result; } - [Benchmark] public int Hexecs_Is() { diff --git a/src/Hexecs.Benchmarks/Actors/ActorHierarchyBenchmark.cs b/src/Hexecs.Benchmarks/Actors/ActorHierarchyBenchmark.cs index 24e9a03..98e5b5f 100644 --- a/src/Hexecs.Benchmarks/Actors/ActorHierarchyBenchmark.cs +++ b/src/Hexecs.Benchmarks/Actors/ActorHierarchyBenchmark.cs @@ -73,7 +73,7 @@ public void Setup() _parents[i] = _actorContext.CreateActor(); for (var j = 0; j < Count; j++) { - var index = i * Count + j; + var index = (i * Count) + j; _children[index] = _actorContext.CreateActor(); } }