Тесты производительности создания новых экземпляров классов

Для ECS фреймворка я попытался найти самый быстрый способ создания новых экземпляров любого класса. Стандартный вариант через Activator.CreateInstance работает как надо, но почему бы не попробовать другие способы и не выбрать самый быстрый вариант.

Тестовое окружение

Для тестов прогоним 10000 итераций вызовов события, повторять этот процесс 30 раз и затем возьмем средний результат. В каждом тесте будем измерять время создания нового экземпляра и присваивание его переменной. О дополнительных особенностях тестирования кода для Unity можно почитать в “Тесты производительности Event, ActionList, Observer, InterfaceObserver“, те же правила будет использоваться и в новых тестах. Т.к мы активно аллоцируем память в каждом тесте, мы должны вызывать сборку мусора перед следующим тестом.

Варианты для проверки:

  • Activator.CreateInstance (Type)
    Создает экземпляр и делает принудительный каст к целевому типу.

  • Activator.CreateInstance()
    Создает экземпляр в Generic-стиле без принудительного приведения к целевому типу.

  • new T () where T: new()
    Создает экземпляр в Generic-стиле с использованием ограничения new() для возможности вызова конструктора по умолчанию.

  • ConstructorInfo.Invoke
    Создает экземпляр путем прямого вызова конструктора через рефлексию и дальнейшего приведения к целевому типу.

  • CustomActivator
    Создает экземпляр путем вызова lambda-метода, внутри которого вызывается создание целевого типа.

Тестовый код

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
class InstanceCreationPerformance : MonoBehaviour {
IEnumerator Start () {
yield return new WaitForSeconds (3f);

const int TESTS = 30;
const int T = 100000;
int result;
Test1 obj;
var sw = new System.Diagnostics.Stopwatch ();

result = 0;
for (var test = 0; test < TESTS; test++) {
GC.Collect ();
GC.WaitForPendingFinalizers ();
sw.Reset ();
sw.Start ();
for (var i = 0; i < T; i++) {
obj = (Test1) CreateInstance1 (typeof (Test1));
}
sw.Stop ();
obj = null;
result += sw.Elapsed.Milliseconds;
}
Debug.LogFormat ("Activator.CreateInstance(Type): {0}", result / (float) TESTS);

result = 0;
for (var test = 0; test < TESTS; test++) {
GC.Collect ();
GC.WaitForPendingFinalizers ();
sw.Reset ();
sw.Start ();
for (var i = 0; i < T; i++) {
obj = CreateInstance2<Test1> ();
}
sw.Stop ();
obj = null;
result += sw.Elapsed.Milliseconds;
}
Debug.LogFormat ("Activator.CreateInstance<T> (): {0}", result / (float) TESTS);

result = 0;
for (var test = 0; test < TESTS; test++) {
GC.Collect ();
GC.WaitForPendingFinalizers ();
sw.Reset ();
sw.Start ();
for (var i = 0; i < T; i++) {
obj = CreateInstance3<Test1> ();
}
sw.Stop ();
obj = null;
result += sw.Elapsed.Milliseconds;
}
Debug.LogFormat ("new T(): {0}", result / (float) TESTS);

result = 0;
for (var test = 0; test < TESTS; test++) {
GC.Collect ();
GC.WaitForPendingFinalizers ();
sw.Reset ();
sw.Start ();
for (var i = 0; i < T; i++) {
obj = CreateInstance4<Test1> ();
}
sw.Stop ();
result += sw.Elapsed.Milliseconds;
}
Debug.LogFormat ("Activator.CreateInstance(typeof(T)): {0}", result / (float) TESTS);

result = 0;
var ctor = typeof (Test1).GetConstructor (new Type[0]);
for (var test = 0; test < TESTS; test++) {
GC.Collect ();
GC.WaitForPendingFinalizers ();
sw.Reset ();
sw.Start ();
for (var i = 0; i < T; i++) {
obj = CreateInstance5<Test1> (ctor);
}
sw.Stop ();
result += sw.Elapsed.Milliseconds;
}
Debug.LogFormat ("Invoke(ConstructorInfo): {0}", result / (float) TESTS);

result = 0;
var activator = CustomActivator<Test1>.Instance;
activator.Bind (() => new Test1 ());
for (var test = 0; test < TESTS; test++) {
GC.Collect ();
GC.WaitForPendingFinalizers ();
sw.Reset ();
sw.Start ();
for (var i = 0; i < T; i++) {
obj = activator.CreateInstance ();
}
sw.Stop ();
result += sw.Elapsed.Milliseconds;
}
Debug.LogFormat ("CustomActivator: {0}", result / (float) TESTS);
}

class Test1 { }

object CreateInstance1 (Type type) {
return Activator.CreateInstance (type);
}

T CreateInstance2<T> () where T : class {
return Activator.CreateInstance<T> ();
}

T CreateInstance3<T> () where T : class, new () {
return new T ();
}

T CreateInstance4<T> () where T : class {
return (T) Activator.CreateInstance (typeof (T));
}

static object[] _noParams = new object[0];

T CreateInstance5<T> (System.Reflection.ConstructorInfo ctor) where T : class {
return (T) ctor.Invoke (_noParams);
}

class CustomActivator<T> where T : class, new () {
public static readonly CustomActivator<T> Instance = new CustomActivator<T> ();
Func<T> _factory;

public void Bind (Func<T> factory) {
_factory = factory;
}

public T CreateInstance () {
if (_factory != null) {
return _factory ();
} else {
return new T ();
}
}
}
}

Результаты теста

1
2
3
4
5
6
7
8
// unity 2017.3.0f3, fw3.5 profile, 100k iterations, average of 30 tests.
// standalone macos 10.13.2, mono.
Activator.CreateInstance(Type): 61.93333
Activator.CreateInstance<T> (): 65.23333
new T(): 65.2
Activator.CreateInstance(typeof(T)): 63.26667
Invoke(ConstructorInfo): 44.03333
CustomActivator: 3

Оба варианта Activator.CreateInstance очень близки по скорости. Generic-вариант немного медленнее, но не стоит забывать, что это синтетический тест на 100000 итераций без дополнительной обработки.

New T()-вариант равен по скорости Activator.CreateInstance<T>, т.к основывается на нем.

Activator.CreateInstance(typeof(T)) дает комбинацию скорости Activator.CreateInstance и защиту типов от Generic-варианта. Хорошая замена вместо использования Activator.CreateInstance<T> () и new T().

Invoke(ConstructorInfo)-вариант показывает очень хорошие результаты для вызовов, использующих рефлексию. Хорошая замена для всех перечисленных выше вариантов.

Чистая победа - у прямого создания экземпляра нужного типа через lambda-метод, 20-кратное ускорение относительно Activator.CreateInstance! Да, не стоит забывать, что это синтетический тест, в случае ECS фреймворка этот подход дал 2-кратное ускорение при создании новых экземпляров компонентов.

Оформить подписку можно здесь: