• В .net существует 3 поколения: 0, 1 и 2.
  • Поколение - это некое хранилище, которое содержит в себе объекты.
  • При сборке поколения 0, выжившие объекты переходят в поколение 1.
  • В поколении 2 хранятся долгоживущие объекты. Однако, если мысоздаем крупный объект, то он сразу попадает в поколение 2.
  • Иногда покление крупных объектов называют поколением 3, но фактически сборка происходит при сборке поколения 2.
  • Если размер объекта больше или равен 85 000 байтов, он считается крупным объектом.
  • Когда мы собираем старшее поколение, младшие поколения также собираются.
  • Объекты, пережившие сборку мусора последнего поколения, по-прежнему будут относиться к этому поколению.
  • Если объект меньше 85 000 байтов, он будет помещен в сегмент SOH (куча малых объектов). В противном случае он помещается в сегмент LOH (куча больших объектов).
  • SOH сжимается и дефрагментируется, чтобы данные хранились с минимальным оверхедом по памяти.
  • LOH по-умолчанию не сжимается, что может приводить к большому потреблению reserved памяти. То есть если большой объект будет удален между существующими объектами, то место не будет освобождено и может быть занято в дальнейшем.
  • Для LOH существует возможность сжатия через свойство GCSettings.LargeObjectHeapCompactionMode, таким образом при сборке LOH будет сжиматься и куча больших объектов.
Read More  

Есть такая штука, которая позволяет вызывать метод, когда мы не знаем сколько параметров хотим туда передать:

void Method(params object[] arr)

Но есть некоторые особенности, которые нужно понимать:

1. По-умолчанию такой вызов создает новый массив (т.е. триггерит GC, что плохо):

void Method1(params object[] arr)
void Method2(object[] arr)

var arr = new int[10];
Method1(arr); // будет создан массив object[1] и к первому элементу присвоен массив arr
Method2(arr); // будет ошибка компиляции, т.к. object[] не соотвествует типу int[]

2. Если мы передадим точный тип массива, то это не будет создавать новый массив:

void Method1(params int[] arr)
void Method2(int[] arr)

var arr = new int[10];
Method1(arr);
Method2(arr);
// Эти 2 вызова идентичны

3. Любой вызов будет создавать новый массив:

void Method(params int[] arr)

Method(1, 2, 3)
Method(1)

Вывод: избегайте методов с params, особенно если это хот часть.

Read More  

Мы часто пишем подобные методы:

List<int> GetItems() {
   var items = new List<int>();
   ...
   return items; 
} 

В этом методе мы просто собираем элементы и возвращаем.

При этом создаем список, создание которого мы не можем запретить извне. Для этого лучше писать таким образом:

void GetItems(List<int> items) {
   ... 
} 

Таким образом контроль над списком может быть таким:

var list = GetFromPool();
GetItems(list);
...
ReturnToPool(list);
Read More  

Когда мы начинали делать проект, в юнити не было возможности отключить Garbage Collector. А нам было нужно)

У нас геймплей был динамичный и любые "провисания" из-за gc плохо влияли на ощущения от игры. Поэтому мы решили его отключить. Раньше для этого нужно было делать хаки, а сейчас уже есть возможность это сделать нормально: https://docs.unity3d.com/Manual/performance-disabling-garbage-collection.html

В нашем кейсе в геймплее мы очень бережно относились (да и относимся) к аллокациям, всё на пулах, поэтому перед началом боя мы выключаем GC, а после боя - включаем и собираем мусор.

Такое решение на самом деле в последствии нам помогло на различных платформах в свое время: на ps4 и switch GC.Collect мог занимать до пары секунд. Надеюсь, что сейчас уже нет таких проблем, но 10 лет назад - были 🙂

Read More  

Если у вас в игре часто используются числа для вывода в UI, то вы, наверное, замечали в профайлере (есть такой, да) аллокации, от которых вы не можете избавиться:

text = health.ToString();

А ведь есть еще и всякие

text = $"{value}/{maxValue}";

На самом деле есть довольно простой способ избежать аллокаций в данном кейсе, когда мы знаем конечное значение для вывода:

если health = 0..100, то можно завести массив и брать значение оттуда:

text = arr[health];

или

text = arr[maxValue][value];

Да, мы сделаем некое подобие https://t.me/unsafecsharp/11, но мы не используем объединение строк, т.к. у нас они уже созданы.

Read More  

Самый простой пример аллокаций - это замыкание. Тут остановимся подробнее. Где могут скрываться аллокации, например, в такой конструкции: list.Where(x => x > 10).ToArray() Очевидно, что при вызове ToArray будет создан массив. Больше никаких аллокаций в данном примере не будет. Давайте рассмотрим второй пример: 

var a = 10; 
list.Where(x => x > a).ToArray() 

Тут к предыдущей аллокации добавляется еще замыкание с переменной "a". Проблема в том, что с этим мы ничего поделать не можем. Но давайте представим, что мы пишем свой замечательный Linq. Каким образом можно избежать аллокации? Нужно сделать просто передачу параметра:

public static void Where<TClosure, T>(this List<T> list, TClosure closure, System.Func<T, TClosure, bool> where) {
     if (where.Invoke(list[i], closure) == false) ... 
}  

Т.е. чтобы избежать аллокаций в замыканиях, нужно передавать все используемые параметры.

Аллокации при боксинге. Боксинг (boxing) - это фактически создание ValueType в куче. Это довольно затратный процесс сам по себе, но самое главное - это тот факт, что когда-нибудь GC об этом вам напомнит. При обратном процессе (unboxing) этого не происходит.

Вообще аллокации в куче плохи тем, что они не очень cache-friedly. Но иногда нам нужно сделать аллокации. Представим ситуацию, когда мы аллоцируем массив с нодами, где у каждой ноды есть еще список нод:

var arr = new Node[1000]; 
for (int i = 0; i < arr.Length; ++i) {
     arr[i] = new Node() {
         nodes = new List<Node>(),
     }; 
} 

Обычно я встречал запись именно такую. Чем она плоха? Тем, что мы аллоцируем первую ноду, и сразу в ноде аллоцируем еще один объект (а может и несколько). Т.е. мало того, что куча в принципе не очень cache-friedly, так мы отказываемся от кэша совсем, создавая объекты таким образом. Как нужно было бы поступит?

var arr = new Node[1000]; for (int i = 0; i < arr.Length; ++i) {
     arr[i] = new Node(); // Создаем ноды, чтобы GC постарался их разместить в памяти последовательно } 
for (int i = 0; i < arr.Length; ++i) {
     arr[i].nodes = new List<Node>(); // Инициализируем данные каждой ноды 
} 

Таким образом, если мы не готовы писать cache-friedly код в целом, то хотя бы частично небольшими изменениями мы можем постараться помочь GC.

Read More