Post

Jak Szybki Jest Get Component?

Jak Szybki Jest Get Component?

Wstęp

W czasie nauki silnika Unity natkniemy się na informację, aby mądrze korzystać z metody GetComponent, gdyż jest ona dosyć wolnym kawałkiem kodu. Z tego powodu powinniśmy również unikać umieszczania jej w metodzie Update. W tym artykule chciałbym skupić się na tym temacie oraz sprawdzić jak najrzetelniej jak to możliwe jak szybka jest egzekucja tej metody.

Co robi GetComponent?

Metoda GetComponent pozwala nam na pobranie referencji do pierwszego komponentu spełniającego podane warunki, znajdującego się na konkretnym obiekcie. Możemy również wywołać tą metode bezpośrednio na komponencie, który skryptujemy, ponieważ jest ona dostępna w klasie Component, która jest bazą dla każdego innego komponentu.

Metoda ta jest dostępna w dwóćh wariantach.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private GameObject test;

public void Foo1()
{
  test.GetComponent<Type>();
  
  test.GetComponent("Type");
}

public void Foo2()
{
  this.GetComponent<Type>();
  
  this.GetComponent("Type");
}

Założenia

W celu wykonania testu potrzebujemy kilka założeń, aby wykonać go jak najrzetelniej jak to możliwe.

  • Do zmierzenia czasu egzekucji metody użyjemy System.Diagnostics.Stopwatch
  • Kolejną kwestią jest wyświetlenie rezultatu naszych pomiarów. Chcemy za wszelką cene uniknąć używania metody Debug.Log, gdyż jest ona bardzo wolna i może mieć wpływ na końcowy rezultat. Zamiast tego wyświetlimy rezultat pomiarów bezpośrednio na ekranie przy użyciu GUI.Label.
  • W celu pokazania bardziej kompleksowych informacji musimy połączyć ze sobą kilka stringów. Niestety, taka operacja doprowadzi do użycia kilku alokacji. Aby tego uniknąć, użyjemy klasy System.Text.StringBuilder. Jej implementacja przechowuje znaki, które chcemy wyświetlić w specjalnym bufferze. Dzięki temu nie zaalokujemy nowej pamięci tak długo jak nie przekroczymy pojemności Buildera. W rezultacie, możemy reużywać tę samą zmienną w kolejnych testach bez triggerowania Garbage Collectora, jako, że rozmiar bufora będzie taki sam w kolejnych iteracjach
  • Ostatnia rzecz to narzut z samego edytora. W celu pozbycia się go uruchomimy test na zbuildowanej wersji aplikacji.

Przetestujemy tę metodę w następujący sposób

  1. Wywołując jedną metodę GetComponent w obu wariantach. Zrobimy to, aby pokazać, że ten czas będzie ultrakrótki, udowadniając, że jedno wywołanie (lub kilka) nie ma negatywnego wpływu na wydajność.
  2. Wywołując metodę GetComponent 200000 razy w obu wariantach. Test ten powinien wykazać, w jakim stopniu łączna liczba wywołań tych metod w danej klatce wpływa na wydajność. Jest to powszechna sytuacja, szczególnie w przypadku dużych projektów.
  3. Powtarzamy krok drugi, gdy pożądany komponent znajduje się na dole hierarchii komponentów. Biorąc pod uwagę, że może to wpłynąć na wydajność metody (ponieważ zwraca ona pierwszy znaleziony komponent), chcemy sprawdzić, jak duża będzie różnica.
  4. Powtarzamy drugi krok, gdy wybierzemy interfejs jako komponent, który chcemy uzyskać (sam komponent będzie również na ostatnim miejscu w hierarchii komponentów)

Przygotowanie do testów

Do celów testowych przygotowałem obiekt składający się z 52 komponentów.

Components config**

W pierwszych dwóch testach pobierzemy komponent Transform, który znajduje się na samej górze hierarchii. W pozostałych testach wykorzystamy komponent, który znajduje się na 52. miejscu w hierarchii, czyli na samym dole. Następnie porównamy uzyskane czasy.

Ostatnim krokiem jest przygotowanie kompilacji. Aby zminimalizować obciążenie graficzne, gra zostanie uruchomiona w trybie okienkowym z najbardziej wydajnymi ustawieniami grafiki. Okno będzie miało rozdzielczość 640×480.

Test przeprowadzono w następującym środowisku:

  • Intel i9-12900K 3.20 GHz
  • Windows 11 Pro 22H2
  • Unity 2022.3.0, Intel 64-bit
  • 640×480, Wysoka wydajność, Tryb okientkowy

Wyniki

Przypadek 1: Jedno wywołanie; komponent na szczycie hierarchii

MetodaCzas egzekucji
Type0.0009 ms
String0.0011 ms

Przypadek 2: 200000 wywołań; komponent na szczycie hierarchii

MetodaCzas egzekucji
Type11 ms
String19 ms

Przypadek 3: 200000 wywołań; komponent 52 w hierarchii

MetodaCzas egzekucji
Type145 ms
String158 ms

Przypadek 4: 200000 wywołań; komponent 52 w hierarchii; wywołanie interfejsu, a nie klasy komponentu

MetodaCzas egzekucji
Type144 ms
String1360 ms

Podsumowanie

Jak widać w przypadku jednego (lub kilku) wywołań metody GetComponent, nawet jeśli zrobimy to wewnątrz metody Update, czas egzekucji jest całkowicie pomijalny. Dopiero gdy liczba wywołań zaczyna drastycznie wzrastać, zaczyna się robić interesująco. Ponieważ metoda przeszukuje całą hierarchię, to miejsce komponentu w tej hierarchii ma ogromny wpływ na końcowy wynik. Wybrany przeze mnie przykład jest dość ekstremalny, ponieważ mieliśmy do czynienia z aż 52 komponentami, ale możemy łatwo zauważyć, że zależność miejsca w hierarchii komponentów od czasu wykonania z pewnością istnieje i warto mieć to na uwadze.

Wersja używająca stringa jako argumentu przegrywa w każdym porównaniu. W najlepszym przypadku różnica wynosi 8 i 13 milisekund. Biorąc pod uwagę, że gra działająca w 60 klatkach ma tylko 16,67 milisekund na wygenerowanie całej klatki, ta różnica jest znaczna i zdecydowanie powinniśmy zawsze wybierać wersję z twardym typem, kiedy tylko możemy.

Jednak najciekawiej robi się, gdy chcemy pobrać referencję za pomocą interfejsu, a nie typu komponentu. Wtedy różnica jest ogromna, więc możemy z pewnością powiedzieć, że w tym przypadku użycie twardego typowania jest jedyną rozsądną opcją.

Zachęcam do komentowania, jeśli macie coś do dodania odnośnie poruszonych tutaj kwestii.

Kod Testu

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
using System.Diagnostics;
using System.Text;
using UnityEngine;

public class GetComponentSpeedTest : MonoBehaviour
{
    [SerializeField]
    private GameObject testObj;
    private Stopwatch sw;
    private StringBuilder sb;
    private Rect rect;
    
    private void Start()
    {
        Application.targetFrameRate = 60;
        sw = new Stopwatch();
        sb = new StringBuilder();
        rect = new Rect(0, 0, Screen.width, Screen.height);
    }

    private void OnGUI()
    {
        sb.Length = 0;
        sw.Reset();
        
        //case 1
        sw.Start();
        testObj.GetComponent<Transform>();
        sw.Stop();
        sb.Append("One call(Top)\n");
        sb.Append("Type Version(ms): ");
        sb.Append(sw.ElapsedMilliseconds);
        sb.Append("\nType Version(ticks): ");
        sb.Append(sw.ElapsedTicks);
        sw.Reset();
        
        sw.Start();
        testObj.GetComponent("Transform");
        sw.Stop();
        sb.Append("\nOne call(Top)\n");
        sb.Append("String Version(ms): ");
        sb.Append(sw.ElapsedMilliseconds);
        sb.Append("\nString Version(ticks): ");
        sb.Append(sw.ElapsedTicks);
        sw.Reset();
        sb.Append('\n');
        sb.Append('\n');
        
        //case2
        sw.Start();
        for (int i = 0; i < 200000; ++i)
        {
            testObj.GetComponent<Transform>();
        }
        sw.Stop();
        sb.Append("Multiple calls(Top)\n");
        sb.Append("Type Version(ms): ");
        sb.Append(sw.ElapsedMilliseconds);
        sb.Append("\nType Version(ticks): ");
        sb.Append(sw.ElapsedTicks);
        sw.Reset();
        
        sw.Start();
        for (int i = 0; i < 200000; ++i)
        {
            testObj.GetComponent("Transform");
        }
        sw.Stop();
        sb.Append("\nMultiple calls(Top)\n");
        sb.Append("String Version(ms): ");
        sb.Append(sw.ElapsedMilliseconds);
        sb.Append("\nString Version(ticks): ");
        sb.Append(sw.ElapsedTicks);
        sw.Reset();
        sb.Append('\n');
        sb.Append('\n');
        
        //case3
        sw.Start();
        for (int i = 0; i < 200000; ++i)
        {
            testObj.GetComponent<TargetClass>();
        }
        sw.Stop();
        sb.Append("Multiple calls(Bottom)\n");
        sb.Append("Type Version(ms): ");
        sb.Append(sw.ElapsedMilliseconds);
        sb.Append("\nType Version(ticks): ");
        sb.Append(sw.ElapsedTicks);
        sw.Reset();
        
        sw.Start();
        for (int i = 0; i < 200000; ++i)
        {
            testObj.GetComponent("TargetClass");
        }
        sw.Stop();
        sb.Append("\nMultiple calls(Bottom)\n");
        sb.Append("String Version(ms): ");
        sb.Append(sw.ElapsedMilliseconds);
        sb.Append("\nString Version(ticks): ");
        sb.Append(sw.ElapsedTicks);
        sw.Reset();
        sb.Append('\n');
        sb.Append('\n');
        
        //case4
        sw.Start();
        for (int i = 0; i < 200000; ++i)
        {
            testObj.GetComponent<ITest>();
        }
        sw.Stop();
        sb.Append("Multiple calls(Bottom)\n");
        sb.Append("Type Version(ms): ");
        sb.Append(sw.ElapsedMilliseconds);
        sb.Append("\nType Version(ticks): ");
        sb.Append(sw.ElapsedTicks);
        sw.Reset();
        
        sw.Start();
        for (int i = 0; i < 200000; ++i)
        {
            testObj.GetComponent($"ITest");
        }
        sw.Stop();
        sb.Append("\nMultiple calls(Bottom)\n");
        sb.Append("String Version(ms): ");
        sb.Append(sw.ElapsedMilliseconds);
        sb.Append("\nString Version(ticks): ");
        sb.Append(sw.ElapsedTicks);
        sw.Reset();
        sb.Append('\n');
        sb.Append('\n');
        
        GUI.Label(rect, sb.ToString());
    }
}

Inspiracją do napisania tego artykułu był poniższy wpis: https://www.jacksondunstan.com/articles/2934

Opublikowano przez

Konrad Słoń

Programming Wizard

Artykuł został udostępniony przez autora na licencji CC BY 4.0 .

Comments powered by Disqus.

Popularne Tagi