Тестирование LeoECS Lite, Morpeh, DragonECS и LeoECS Proto

Тут вышла занимательная статья по сравнению двух фреймворков. Предлагаю расширить ее до четырех: LeoECS Lite, Morpeh, DragonECS и LeoECS Proto.

Окружение

Версии

Мерять будем как и в оригинальной статье - через Graphy, ведь профессионалы меряют все кадрами в секунду, а не миллисекундами, не будем от них отставать! Заодно и потребление памяти померяем, чтобы мне стало стыдно за жирные SparseSet-ы и зря потраченную память в LeoECS Lite и LeoECS Proto.

Замеры сделаны в сборках под MacOS, собранных с помощью IL2CPP (Release) и запущенных на следующем железе:

Тесты

Тесты как и в оригинальной статье будут состоять из 3 частей:

  • Простое итерирование с обновлением полей компонентов
  • Итерирование с миграцией сущностей при удалении/добавлении 1 компонента
  • Итерирование с миграцией сущностей при удалении/добавлении 3 компонентов.

Во всех тестах используется одно количество сущностей : 500_000.

Итерирование

Все системы помечены IL2CPP-атрибутами (убраны из примеров кода для краткости):

1
2
3
[Il2CppSetOption (Option.NullChecks, false)]
[Il2CppSetOption (Option.ArrayBoundsChecks, false)]
[Il2CppSetOption (Option.DivideByZeroChecks, false)]

Компоненты:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Consts {
public const int ENTITIES_COUNT = 500000;
}
struct TestComponent0 : IComponent, IEcsComponent {
public int Test;
}
struct TestComponent1 : IComponent, IEcsComponent {
public int Test;
}
struct TestComponent2 : IComponent, IEcsComponent {
public int Test;
}
struct TestComponent3 : IComponent, IEcsComponent {
public int Test;
}

LeoECS Lite

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class LiteIterateSystem : IEcsInitSystem, IEcsRunSystem {
EcsFilter _filter0;
EcsFilter _filter1;
EcsFilter _filter2;
EcsFilter _filter3;
EcsPool<TestComponent0> _pool0;
EcsPool<TestComponent1> _pool1;
EcsPool<TestComponent2> _pool2;
EcsPool<TestComponent3> _pool3;
public void Init (IEcsSystems systems) {
var world = systems.GetWorld ();
_filter0 = world.Filter<TestComponent0> ().End ();
_filter1 = world.Filter<TestComponent0> ().Inc<TestComponent1> ().End ();
_filter2 = world.Filter<TestComponent0> ().Inc<TestComponent1> ().Inc<TestComponent2> ().End ();
_filter3 = world.Filter<TestComponent0> ().Inc<TestComponent1> ().Inc<TestComponent2> ().Inc<TestComponent3> ().End ();
_pool0 = world.GetPool<TestComponent0> ();
_pool1 = world.GetPool<TestComponent1> ();
_pool2 = world.GetPool<TestComponent2> ();
_pool3 = world.GetPool<TestComponent3> ();
for (var i = 0; i < Consts.ENTITIES_COUNT; i++) {
var e = world.NewEntity ();
_pool0.Add (e);
_pool1.Add (e);
_pool2.Add (e);
_pool3.Add (e);
}
}

public void Run (IEcsSystems systems) {
foreach (var ent in _filter3) {
_pool0.Get (ent).Test++;
_pool1.Get (ent).Test++;
_pool2.Get (ent).Test++;
_pool3.Get (ent).Test++;
}
}
}

Morpeh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
[CreateAssetMenu (menuName = "ECS/Systems/" + nameof (MorpehIterateSystem))]
public sealed class MorpehIterateSystem : UpdateSystem {
Stash<TestComponent0> _stash0;
Stash<TestComponent1> _stash1;
Stash<TestComponent2> _stash2;
Stash<TestComponent3> _stash3;
Filter _filter0;
Filter _filter1;
Filter _filter2;
Filter _filter3;

public override void OnAwake () {
_filter0 = World.Filter.With<TestComponent0> ().Build ();
_filter1 = World.Filter.With<TestComponent0> ().With<TestComponent1> ().Build ();
_filter2 = World.Filter.With<TestComponent0> ().With<TestComponent1> ().With<TestComponent2> ().Build ();
_filter3 = World.Filter.With<TestComponent0> ().With<TestComponent1> ().With<TestComponent2> ().With<TestComponent3> ().Build ();
_stash0 = World.GetStash<TestComponent0> ();
_stash1 = World.GetStash<TestComponent1> ();
_stash2 = World.GetStash<TestComponent2> ();
_stash3 = World.GetStash<TestComponent3> ();
for (var i = 0; i < Consts.ENTITIES_COUNT; i++) {
var e = World.CreateEntity ();
_stash0.Add (e);
_stash1.Add (e);
_stash2.Add (e);
_stash3.Add (e);
}
}

public override void OnUpdate (float deltaTime) {
foreach (var ent in _filter3) {
_stash0.Get (ent).Test++;
_stash1.Get (ent).Test++;
_stash2.Get (ent).Test++;
_stash3.Get (ent).Test++;
}
}
}

DragonECS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class Aspect0 : EcsAspect {
public EcsPool<TestComponent0> C0 = Inc;
public EcsPool<TestComponent1> C1 = Opt;
public EcsPool<TestComponent2> C2 = Opt;
public EcsPool<TestComponent3> C3 = Opt;
}

class Aspect1 : EcsAspect {
public EcsPool<TestComponent0> C0 = Inc;
public EcsPool<TestComponent1> C1 = Inc;
}

class Aspect2 : EcsAspect {
public EcsPool<TestComponent0> C0 = Inc;
public EcsPool<TestComponent1> C1 = Inc;
public EcsPool<TestComponent2> C2 = Inc;
}

class Aspect3 : EcsAspect {
public EcsPool<TestComponent0> C0 = Inc;
public EcsPool<TestComponent1> C1 = Inc;
public EcsPool<TestComponent2> C2 = Inc;
public EcsPool<TestComponent3> C3 = Inc;
}

public class DragonIterateSystem : IEcsInject<EcsDefaultWorld>, IEcsInit, IEcsRun {
EcsDefaultWorld _world;

public void Init () {
var a3 = _world.GetAspect<Aspect3> ();
for (var i = 0; i < Consts.ENTITIES_COUNT; i++) {
var e = _world.NewEntity ();
a3.C0.Add (e);
a3.C1.Add (e);
a3.C2.Add (e);
a3.C3.Add (e);
}
}

public void Inject (EcsDefaultWorld obj) {
_world = obj;
}

public void Run () {
foreach (var ent in _world.Where (out Aspect3 a3)) {
a3.C0.Get (ent).Test++;
a3.C1.Get (ent).Test++;
a3.C2.Get (ent).Test++;
a3.C3.Get (ent).Test++;
}
}
}

LeoECS Proto

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class TestAspect : ProtoAspectInject {
public ProtoPool<TestComponent0> Pool0;
public ProtoPool<TestComponent1> Pool1;
public ProtoPool<TestComponent2> Pool2;
public ProtoPool<TestComponent3> Pool3;

public ProtoIt It0 = new (It.Inc<TestComponent0> ());
public ProtoIt It1 = new (It.Inc<TestComponent0, TestComponent1> ());
public ProtoIt It2 = new (It.Inc<TestComponent0, TestComponent1, TestComponent2> ());
public ProtoIt It3 = new (It.Inc<TestComponent0, TestComponent1, TestComponent2, TestComponent3> ());
}

public class ProtoIterateSystem : IProtoInitSystem, IProtoRunSystem {
[DI] TestAspect _aspect;

public void Init (IProtoSystems systems) {
for (var i = 0; i < Consts.ENTITIES_COUNT; i++) {
var e = _aspect.World ().NewEntity ();
_aspect.Pool0.Add (e);
_aspect.Pool1.Add (e);
_aspect.Pool2.Add (e);
_aspect.Pool3.Add (e);
}
}

public void Run () {
foreach (var ent in _aspect.It3) {
_aspect.Pool0.Get (ent).Test++;
_aspect.Pool1.Get (ent).Test++;
_aspect.Pool2.Get (ent).Test++;
_aspect.Pool3.Get (ent).Test++;
}
}
}

Результаты

LeoECS Lite 2023.11.22 (release)

Morpeh 2023.1.0 (release)

Morpeh 2024.1.0-rc46 (dev)

DragonECS: 0.8.31 (dev)

LeoECS Proto 2024.4.25 (boosty release)

LeoECS Proto 2024.4.25 (boosty release)

Итерации с кеширующими итераторами (при этом зависимые компоненты не могут удаляться или добавляются)

Выводы

  • Видна значительная деградация релизной версии Morpeh 2023.1.0 по сравнению с 2022 годом (тогда скорость была примерно одинакова с LeoECS Lite), в Morpeh 2024.1.0-rc46 скорость более-менее возвращена к той, что была ранее. Налицо отсутствие системного подхода к контролю качества, иначе это было бы исправлено сразу, а не через год.
  • Потребление оперативной памяти: Morpeh 2023.1.0 потребляет 300Мб - это в 6.5 раз больше, чем потребляет LeoECS Lite и в 10 раз больше, чем LeoECS Proto! И это по сути только инфраструктурные издержки, самих данных компонентов там практически нет. В Morpeh 2024.1.0-rc46 потребление памяти снизили в 2 раза, но на общем фоне это мало что поменяло - 160Мб это очень много по сравнению с потенциальными 30Мб. А потом эти люди учат меня, что SparseSet-ы размером с мир много занимают и так делать нельзя, а Morpeh является “production-ready” и там все сделано правильно.
  • Новичок DragonECS показал себя неплохо, он еще не в релизе и есть куда развиваться.

Миграции одного компонента

Код инициализации систем идентичный коду из предыдущего теста, будет приведена только отличающаяся часть кода.

LeoECS Lite

1
2
3
4
5
6
7
8
9
public void Run (IEcsSystems systems) {
foreach (var ent in _filter3) {
_pool3.Del (ent);
}

foreach (var ent in _filter0) {
_pool3.Add (ent);
}
}

Morpeh

1
2
3
4
5
6
7
8
9
public override void OnUpdate (float deltaTime) {
foreach (var ent in _filter3) {
_stash3.Remove (ent);
}
World.Commit ();
foreach (var ent in _filter0) {
_stash3.Add (ent);
}
}

DragonECS

1
2
3
4
5
6
7
8
9
public void Run () {
foreach (var ent in _world.Where (out Aspect3 a3)) {
a3.C3.Del (ent);
}

foreach (var ent in _world.Where (out Aspect0 a0)) {
a0.C3.Add (ent);
}
}

LeoECS Proto

1
2
3
4
5
6
7
8
9
public void Run () {
foreach (var ent in _aspect.It3) {
_aspect.Pool3.Del (ent);
}

foreach (var ent in _aspect.It0) {
_aspect.Pool3.Add (ent);
}
}

Результаты

LeoECS Lite 2023.11.22 (release)

Morpeh 2023.1.0 (release)

Morpeh 2024.1.0-rc46 (dev)

DragonECS: 0.8.31 (dev)

LeoECS Proto 2024.4.25 (boosty release)

Выводы

  • LeoECS Lite ожидаемо просаживается по скорости из-за необходимости постоянно обновлять несколько фильтров. Странно, что Morpeh 2024.1.0-rc46 не показывает фантастических результатов, как обещали разработчики.
  • LeoECS Proto ожидаемо игнорирует количество обновляемых фильтров, просаживаясь только на самом добавлении/удалении компонентов.
  • Новичок DragonECS прекрасно показал себя на миграции, выдав результат в 2 раза лучше, чем Morpeh 2024.1.0-rc46! Когда один разработчик лучше, чем целая команда.

Миграции трех компонентов

LeoECS Lite

1
2
3
4
5
6
7
8
9
10
11
12
13
public void Run (IEcsSystems systems) {
foreach (var ent in _filter3) {
_pool1.Del (ent);
_pool2.Del (ent);
_pool3.Del (ent);
}

foreach (var ent in _filter0) {
_pool1.Add (ent);
_pool2.Add (ent);
_pool3.Add (ent);
}
}

Morpeh

1
2
3
4
5
6
7
8
9
10
11
12
13
public override void OnUpdate (float deltaTime) {
foreach (var ent in _filter3) {
_stash1.Remove (ent);
_stash2.Remove (ent);
_stash3.Remove (ent);
}
World.Commit ();
foreach (var ent in _filter0) {
_stash1.Add (ent);
_stash2.Add (ent);
_stash3.Add (ent);
}
}

DragonECS

1
2
3
4
5
6
7
8
9
10
11
12
13
public void Run () {
foreach (var ent in _world.Where (out Aspect3 a3)) {
a3.C1.Del (ent);
a3.C2.Del (ent);
a3.C3.Del (ent);
}

foreach (var ent in _world.Where (out Aspect0 a0)) {
a0.C1.Add (ent);
a0.C2.Add (ent);
a0.C3.Add (ent);
}
}

LeoECS Proto

1
2
3
4
5
6
7
8
9
10
11
12
13
public void Run () {
foreach (var ent in _aspect.It3) {
_aspect.Pool1.Del (ent);
_aspect.Pool2.Del (ent);
_aspect.Pool3.Del (ent);
}

foreach (var ent in _aspect.It0) {
_aspect.Pool1.Add (ent);
_aspect.Pool2.Add (ent);
_aspect.Pool3.Add (ent);
}
}

Результаты

LeoECS Lite 2023.11.22 (release)

Morpeh 2023.1.0 (release)

Morpeh 2024.1.0-rc46 (dev)

DragonECS: 0.8.31 (dev)

LeoECS Proto 2024.4.25 (boosty release)

Выводы

  • LeoECS Lite практически мертвый на таком количестве сущностей при постоянном обновлении всех фильтров.
  • Morpeh 2024.1.0-rc46 быстрее, чем Morpeh 2023.1.0, но незначительно.
  • Новичок DragonECS показывает скорость в 2 раза большую (при потреблении памяти в 2 раза меньше), чем у “лучшего, самого быстрого, production-ready” Morpeh-а и в этот раз. Рекомендую сходить и поставить звезду этому репозиторию, автор DragonECS это заслужил!

LeoECS Lite с 22.05.2024 переводится из активной разработки в режим поддержки - будут исправляться только баги, нового функционала не планируется. На текущий момент известных багов нет, фреймворк стабильный и выполняет свои задачи.

Актуальные версии пакетов доступны в закрытом discord-сервере для vk/boosty-подписчиков.
Оформить подписку можно здесь: