Yr12 Journal 23

In Term 3 Week 10, I tested my project again and I was also optimizing my C# open source repo.

Progress

Testing

As usual, I tested my project, it is definitely accurate again this time:

Therefore, this model is elegant and accurate, it makes reliable predictions.

My C# Project

I realised I had a function that ends up with a huge GC allocation and time consumption, so I decided to optimize it.

The code aims to find all the specific Unity Component Type components that has been instantiated in all loaded scenes, including the hidden ones.

Problem code:

1
2
3
return ClassBindMgr.LoadedScenes.SelectMany(scene => scene.GetRootGameObjects())
.SelectMany(g => g.GetComponentsInChildren<T>(true))
.ToList();

In a test, when calling the problem code to retrieve 20 of the specific type in a provided scene, it will allocate 3KB GC and takes 3ms to process

Why is it a problem?

Lot of LINQs, that creates redundant collections that leads to GC allocation, and the Unity’s API GetComponentsInChildren<T>(bool includeHidden) will create a list as the result internally, therefore it will create a lot of Lists… As it is the anonymous lambda delegate inside SelectMany method, so it will create total number of root gameObjects across all scenes amount of Lists which is definitely unnecessary.

My Attempt

First, we need to reduce the LINQ, although I know LINQ is really convenient…

We need a list to store all gameObjects across all scenes, and we need a temporary list to store the result from GetComponentsInChildren method rather than let unity create one:

1
2
List<GameObject> all = null;
List<GameObject> temp = null;

We need to iterate all scenes and retrieve root gameObjects,

here I used ObjectPool to reuse previous lists, and I used the Unity’s API GetComponentsInChildren<T>(bool includeHidden, List<T> result), unity will clear the result list so I don’t need to clear the temp list (unlike the ‘all’ list, I have to clear it)

after retrieving all gameObjects, we can return the temp List to the ObjectPool

if there is no gameObject, we can return an empty list as result, because there will not be any components we want

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
foreach (var scene in ClassBindMgr.LoadedScenes)
{
if (all == null)
{
all = ObjectPool<List<GameObject>>.Peak() != null ? ObjectPool<List<GameObject>>.Request() : new List<GameObject>(scene.rootCount);
all.Clear();
}
if (temp == null)
{
temp = ObjectPool<List<GameObject>>.Peak() != null ? ObjectPool<List<GameObject>>.Request() : new List<GameObject>(scene.rootCount);
}
scene.GetRootGameObjects(temp);
all.AddRange(temp);
}
temp.Clear();
ObjectPool<List<GameObject>>.Return(temp);
if (all == null)
{
return new List<T>();
}

Now we need to retrieve components from gameObjects,

same concept here, we can use ObjectPool to reduce allocation, and we just calls the Unity API that stores the result into the provided List to reduce allocation, then we add every components to the result list.

at the end we can return the ‘All’ and ‘tempT’ to the pool to reuse it

1
2
3
4
5
6
7
8
9
10
11
12
13
14
List<T> lst = ObjectPool<List<T>>.Peak() != null ? ObjectPool<List<T>>.Request() : new List<T>(all.Count);
lst.Clear();
List<T> tempT = ObjectPool<List<T>>.Peak() != null ? ObjectPool<List<T>>.Request() : new List<T>(all.Count);
tempT.Clear();
foreach (var gameObject in all)
{
gameObject.GetComponentsInChildren(true, tempT);
lst.AddRange(tempT);
tempT.Clear();
}
all.Clear();
ObjectPool<List<GameObject>>.Return(all);
ObjectPool<List<T>>.Return(tempT);
return lst;

After this optimization, we no longer have allocation from LINQ, and we almost have no allocation for GetComponentsInChildren.

The complete code:

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
public static List<T> FindObjectsOfTypeAll<T>()
{
if (!Application.isPlaying)
{
return SceneManager.GetActiveScene().GetRootGameObjects()
.SelectMany(g => g.GetComponentsInChildren<T>(true))
.ToList();
}
#if INIT_JE
List<GameObject> all = null;
List<GameObject> temp = null;
foreach (var scene in ClassBindMgr.LoadedScenes)
{
if (all == null)
{
all = ObjectPool<List<GameObject>>.Peak() != null ? ObjectPool<List<GameObject>>.Request() : new List<GameObject>(scene.rootCount);
all.Clear();
}
if (temp == null)
{
temp = ObjectPool<List<GameObject>>.Peak() != null ? ObjectPool<List<GameObject>>.Request() : new List<GameObject>(scene.rootCount);
}
scene.GetRootGameObjects(temp);
all.AddRange(temp);
}
ObjectPool<List<GameObject>>.Return(temp);
if (all == null)
{
return new List<T>();
}
List<T> lst = ObjectPool<List<T>>.Peak() != null ? ObjectPool<List<T>>.Request() : new List<T>(all.Count);
lst.Clear();
List<T> tempT = ObjectPool<List<T>>.Peak() != null ? ObjectPool<List<T>>.Request() : new List<T>(all.Count);
tempT.Clear();
foreach (var gameObject in all)
{
gameObject.GetComponentsInChildren(true, tempT);
lst.AddRange(tempT);
tempT.Clear();
}
ObjectPool<List<T>>.Return(tempT);
return lst;
#endif
return null;
}

With the same test, now it only has 300bytes GC and cost 1ms to process. Great Success.

Challenges

I am honestly not sure if I need a journal for this week or not, but I still decided to write it in midnight which is frustrating me.

Reflection

This week I did no school work because I already finished my school IT project and we did not have much classes, but I was still learning stuffs in IT area. Therefore, overall I think I did well this week, and I used my class time wisely to think about these codes in my head. For this week, I also wrote some JavaScript for my ANU project that should due Thursday and got extended to next Friday, I actually finished it before the original deadline which is good. I think overwell I did well this week.

Timeline

I followed up my schedule, as I successfully tested my project and it still works. I will start making my documentation and presentation from the start of next term just as what I planned.