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

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

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

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

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

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

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

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

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

  • 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)-вариант показывает очень хорошие результаты для вызовов, использующих Reflection. Хорошая замена для всех перечисленных выше вариантов.

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