false sharing - это ситуация, когда несколько потоков одновременно обращаются к разным переменным, которые находятся в одной кеш-линии. Кеш-линия - это минимальная единица данных, которая копируется из оперативной памяти в кеш процессора.

Когда несколько потоков работают с переменными, расположенными в одной кеш-линии, происходит постоянный обмен этой кеш-линией между кешами потоков, что приводит к накладным расходам на копирование кеш-линии из оперативной памяти в кеш и обратно. Это может существенно ухудшить производительность программы.

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

Read More  

В Unity добавили аттрибут HideInCallstack, который пока не работает 🙂 Но использовать его уже можно, когда заработает - тогда и заработает.

https://docs.unity3d.com/ScriptReference/HideInCallstackAttribute.html

Read More  

се мы знаем, что в Unity Profiler можно включить Deep и посмотреть что там вообще происходит в ваших методах. Проблема только в том, что в больших проектах это тормозит, занимает кучу времени, а в профайлере нужно долго потом искать проблемные места. Для этого можно использовать вот такую конструкцию:

var marker = new ProfilerMarker("My Marker");
marker.Begin();
// тут код, который мы хотим проверить в профайлере
marker.End();

Более того, этот маркер можно использовать в Burst.

Read More  

MemoryAllocator проще всего представить как один большой неразрывный массив байт. Чтобы положить туда данные - нужно всего лишь знать по какому индексу это делать. Для этого аллокатор разбивается на блоки. В пустом аллокаторе блок всего один, он занимает всю область памяти от начала и до конца. Блок - это структура, у которой есть часть заголовка (с указателями на следующий/предыдущий блоки, состоянием "свободен"/"занят" и размером блока) и следом сами данные.

[block_size][state][prev][next][user_data]

Когда мы просим аллокатор дать нам память определенного размера, нам нужно найти свободный блок памяти. Тут мы просто переходим от первого блока до последнего и ищем блок подходящего размера. Если блок не нашли - добавляем новый. А возвращаем мы не unsafe-указатель, а собственный указатель, в котором записан тот самый индекс в нашем массиве байт. Когда мы просим освободить память, мы выставляем блоку состояние "свободен" и мерджим его с соседними свободными блоками, если такие есть.

Еще есть дополнительные всякие штуки, что перебираем мы не с самого начала, а с первого пустого, можно вообще завести отдельный список пустых блоков и выбирать среди них, можно еще сортировать их по размеру, а потом искать бинарным поиском. Еще есть зоны, которые содержат блоки, это вообще отдельная история.

Таким образом мы получаем следующие бенефиты:

1. Большой кусок памяти, который мы можем скопировать/передать по сети/уничтожить очень быстро;

2. Выдаваемые указатели можно так же передавать по сети, т.к. они будут валидны на любом клиенте, если тот имеет такой же аллокатор;

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

Реализацию можно посмотреть тут:https://github.com/chromealex/csharp-memory-allocator

Read More  

У нас была задача нарисовать траву, много травы, которая шевелится, когда юниты по ней ходят, а еще можно в нее бабахнуть бомбочкой и она сгорит, а если рядом будет еще трава, то она загорится и т.д. 🙂

У нас 2д-игра, карту мы рисуем тайлами, траву мы тоже рисуем тайлами, но на специальной тайлмапе. Во время загрузки игры, эта тайлмапа выключается, а каждая нарисованная травинка превращается в партикл и рисуется уже ParticleSystem 🙂 Т.е. для контенщика ничего не меняется в его пайплайне, а для рендера становится все гораздо лучше.

Как шевелить. Тут мой любимый способ: добавляем камеру, которая снимает исключительно юнитов (можно сетить пиксель в текстуру - не принципиально). Текстура - это вся карта. Дальше в шейдере травы читаем из текстуры пиксель и применяем с этой силой шевеление вертексов (чем выше вертекс, тем больше он шевелится и т.д.).

Как поджигать. Для того, чтобы решить задачу с поджиганием, нам нужно построить граф, мы это делаем на этапе загрузки карты, где каждая травинка знает о соседях (по дистанции). Для того, чтобы найти быстро нужную ноду - у нас есть выключенная тайлмапа, которая даст нам индекс в массиве из мировой точки. Когда травинка загорается - через X секунд горения она поджигает всех своих соседей, если они еще не горят. Когда травинка сгорает - она оставляет след, который тоже пропадает через какое-то время.

Ну и да, горящая трава убивает юнитов 🙂

Read More  

Я на собеседованиях спрашиваю простой вопрос: Почему нельзя написать transform.position.y = 123f;? Ответы бывают разные от банального "не знаю" и "ну юнити не дает" до единственно верного 🙂 

Вообще довольно забавно, что мало кто из разрабов в принципе вникает в суть происходящего, а она банальна: transform.position - это getter, а get - это метод. А Vector3 - это структура. 

Вот и получается, что при вызове get мы делаем копию структуры и пытаемся ее менять. А это очевидно, что не приведет ни к чему.

Read More  

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

public struct Job : IJobSingle {
  public void Execute() {
  }
}

Т.е. мы хотим сделать стандартную однопоточную джобу (многопточные делаются не сильно дольше), что нам для этого необходимо:

1. Интерфейс и скелет

2. Инициализация джобы

3. Сама джоба

С интерфейсом все просто:

[JobProducerType(typeof(IJobSingleExtensions.JobProcess<>))]
public interface IJobSingle {
     void Execute();
}

public static unsafe class IJobSingleExtensions {
     public static JobHandle Schedule<T>(this T jobData, JobHandle inputDeps = default) where T : struct, IJobSingle {}
     internal struct JobProcess<T> where T : struct, IJobSingle {}
}

Обратите внимание, что JobProducerType говорит нам о том, что этот интерфейс не абы кто, а это интерфейс великой джобы ;)

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

public static unsafe class IJobSingleExtensions {
     public static JobHandle Schedule<T>(this T jobData, JobHandle inputDeps = default) where T : struct, IJobSingle {
     var parameters = new JobsUtility.JobScheduleParameters(UnsafeUtility.AddressOf(ref jobData), JobProcess<T>.Initialize(), inputDeps, ScheduleMode.Single);
     return JobsUtility.Schedule(ref parameters);
}

internal struct JobProcess<T> where T : struct, IJobSingle {
     private static System.IntPtr jobReflectionData;
     public static System.IntPtr Initialize() {
          if (jobReflectionData == System.IntPtr.Zero) {
               jobReflectionData = JobsUtility.CreateJobReflectionData(typeof(T), typeof(T), (ExecuteJobFunction)Execute);
          }
          return jobReflectionData;
     }
     
     public delegate void ExecuteJobFunction(ref T jobData, System.IntPtr additionalData, System.IntPtr bufferRangePatchData, ref JobRanges ranges, int jobIndex);
     public static void Execute(ref T jobData, System.IntPtr additionalData, System.IntPtr bufferRangePatchData, ref JobRanges ranges, int jobIndex) {
     }
}

Ну и финально - мы добавляем в наш Execute само выполнение:

public static void Execute(ref T jobData, System.IntPtr additionalData, System.IntPtr bufferRangePatchData, ref JobRanges ranges, int jobIndex) { 
     jobData.Execute();
}
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  

В юнити можно использовать символ ~ в конце имени для исключения папки или файла. В таком случае скрипты в этой папке не будут компилироваться, а ассеты не будут импортироваться. Иногда бывает полезно.

Read More  

Если вам нужен сильный full-screen blur эффект, можно использовать буффер из нужной камеры и шейдер с tex2Dlod для чтения текстуры маленького размера, тем самым будет казаться, что размытие достигается шейдером, хотя на самом деле этот эффект будет от текстуры низкого разрешения + фильтрации. 

На практике мы такое используем для фона в UI, чтобы сильно "размывать" задний фон.

Read More  

Вы наверняка использовали эту структуру данных для хранения уникальных данных, но скорее всего вы слышали, что поиск в этой структуре занимает O(1), поэтому вы ее и используете.  

Давайте разебермся откуда берется этот загадочный O(1) и каким образом он вообще получается. 

Реализаций HashSet может быть много, но смысл остается примерно таким: У вас есть значение, которое вы хотите положить в коллекцию, допустим оно равно 12. Чтобы получить поиск элемента за O(1), самое простое - это завести массив bool, где индекс будет равен этому числу, а значение true или false.

Таким образом мы получаем супер-быстрое добавление элементов и супер-быструю проверку на существование. 

Но что если значение будет 1_000_000 или вообще отрицательным? 

Получается, что использовать массив таким образом мы уже не можем. Для этого давайте вызовем метод GetHashCode(), который нам вернет hash от элемента (т.к. далеко не факт, что мы будет добавлять в коллекцию только числа, там же могут быть и структуры и классы).

Таким образом получим некое число в любом случае. Но снова число может быть любым.

Для того, чтобы решить эту задачу, давайте заведем массив "вёдер" или проще говоря "массив списков" (это для простоты понимания). Мы будем хранить элемент в одном из этих ведер (или buckets). Количество ведер изначально зададим, например, 10. Таким образом, чтобы засунуть число 12 в одно из этих бакетов, нам нужно найти остаток от деления 12 % 10 = 2. Вот и наше ведёрко найдено.

А дальше мы выполняем операцию buckets[index].Add(value), т.е. добавляем наш элемент в список. Получается, что добавление элемента будет действительно O(1).

Но что же с поиском элемента? Нетрудно догадаться, что если мы таким образом добавили элемент в ведро, то и найти его нужно таким же образом. Ищем остаток от деления, находим ведро, а потом ...ой. А потом нам нужно перебрать все элементы в ведре, чтобы найти нужный 🙂

И получается, что никаким O(1) тут и не пахнет. Но мы же знаем, мы же слышали. Мозг скорее всего уловил O(1), но не уловил слова типа "среднее" или "условное", что подразумевает, что в лучшем случае O(1), а в худшем O(n), что понятно, если мы будем добавлять разные истансы одного объекта в HashSet, но при этом метод GetHashCode будет возвращать нам всегда одно и то же число, например.

Read More  

Существует возможность подсказать компилятору что метод должен быть заинлайнен, для этого нужно добавить аттрибут

System.Runtime.CompilerServices.MethodImplAttribute(MethodImplOptions.AggressiveInlining)

или

MethodImpl(MethodImplOptions.AggressiveInlining)

Я обычно пишу гораздо короче: 

[INLINE(256)]

При этом объявляю 

using INLINE = System.Runtime.CompilerServices.MethodImplAttribute 

Но нужно понимать, что аттрибут агрессинвного инлайна не гарантирует факт инлайна, он лишь подсказывает, что этот метод хорошо бы заинлайнить. Если дело касается хот частей, то лучше использовать "ручной инлайн", т.е. нужно переносить код самостоятельно.

Read More