Начнем с того, что это регистры. По сути отличие между ними в том, что xmm (128 bits) хранит меньше данных, чем ymm (256 bits).

Вы можете встретить такое в burst-generated коде, когда разглядываете бесконечные регистры в инспекторе берста.

Пример:

struct MyStruct { 
     public float a1; 
     public float a2; 
     public float a3; 
     public float a4; 
}

myStruct1 = myStruct2 будет использовать xmm (4 флота в 128 битах).

Если же добавить полей:

struct MyStruct { 
     public float a1; 
     public float a2; 
     public float a3; 
     public float a4; 
     public float a5; 
     public float a6; 
     public float a7; 
     public float a8; 
}

то теперь myStruct1 = myStruct2 будет использовать ymm (8 флотов в 256 битах).

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

Но если же мы оставим 6 полей, то будет использован один vmovups xmm, а остальные два поля будут считываться mov rdx.

Читать далее  

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

 Метод void (объект params [] arr)

Но есть некоторые особенности, которые необходимо учитывать:

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

 void Method1 (объект параметров [] arr)
void Method2 (объект [] обр)

вар arr = новый int[10];
Метод1(обр); // будет создан массив object[1] и к первому элементу будет применен массив arr
Метод2(обр); // будет ошибка компиляции, т.к. объект[] не соотвествует типу int[]

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

 void Method1 (параметры int [] arr)
недействительный метод2 (int [] обр)

вар arr = новый int[10];
Метод1(обр);
Метод2(обр);
// Эти 2 вызова ответственных

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

 Метод void (params int[] arr)

Метод(1, 2, 3)
Метод(1)

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

Читать далее  

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

WaitForTargetFPS: Время, потраченное на ожидание целевого значения FPS, указанного в Application.targetFrameRate. Редактор не использует VSync на GPU, а вместо этого использует WaitForTargetFPS для имитации задержки VSync.

Gfx.ProcessCommands: Поток рендеринга охватывает всю обработку команд рендеринга. Часть этого времени может быть потрачена на ожидание VSync или новых команд из основного потока, что можно увидеть в Gfx.WaitForPresent.

Gfx.WaitForCommands: Поток рендеринга готов к новым командам, и может указывать на узкое место в основном потоке.

Gfx.PresentFrame: Поток рендеринга представляет собой время, затраченное на ожидание рендеринга и представления кадра графическим процессором, что может включать ожидание VSync.

Gfx.WaitForPresent: Когда основной поток готов начать рендеринг следующего кадра, но поток рендеринга еще не завершил ожидание представления кадра GPU. Это может указывать на то, что узкое место в GPU. Посмотрите на представление временной шкалы, чтобы узнать, проводит ли поток рендеринга одновременно время в Gfx.PresentFrame. Если поток рендеринга все еще проводит время в Camera.Render, узкое место в CPU, т.е. тратит слишком много времени на отправку вызовов отрисовки/текстур на GPU.

Читать далее  

Мы знаем, что производительность - это метрика, которую можно оценить по fps или frames per second, т.е. сколько раз мы можем за секунду выполнить всю логику в кадре и отрисовать все, что мы посчитали.

Логика - это все то, что мы пишем в наших методах Update, которые являются частью основного цикла кадра и это обрабатывается на cpu, а вот рендер - это то, что обрабатывает видяха.

В целом эти два процесса никак не связаны, то есть в какой-то момент времени cpu посчитал что-то и готов передать на gpu некие данные, которые будут обрабатываться уже там и как результат - будет картинка. Существуют еще compute shaders, которые по сути используют проц на видяхе, чтобы посчитать результат.

И вот тут нам нужно синхронизировать данные. То есть при использовании синхронизации нам нужно задать фиксированное количество времени, которое мы готовы потратить на кадр, например, 30 кадров в секунду или 33мс на кадр. И при синхронизации существует время ожидания, когда cpu ждет gpu или наоборот. В профайлере такие ожидания обозначаются как WaitForPresentOnGfxThread. А вот WaitForTargetFps - это время, которое нужно, чтобы поддержать заданный frame rate.

Читать далее  

В юнити есть замечательная штука, которую мало кто получает. На самом деле даёт возможность считать примитивами. В качестве примера я такое часто использую для того, чтобы знать, какие объекты нужно просчитывать, а какие — нет. Наверное, это апи можно считать уже в обстановке, т.к. Приходите всякие брг, которые умеют выбраковывать, плюс это апи не умеет всплески. Но на самом деле я все равно его использую, т.к. даже на уровне представления он дает прирост, если самому отключать аниматоры/рендеры и прочие штуки.

https://docs.unity3d.com/Manual/CullingGroupAPI.html

Читать далее  

Это приведение к единичному размеру, при этом направление сохраняется. Обычно мы используем для этого v.normalized или v.Normalize(). Второй вариант будет немного быстрее первого, т.к. мы не создаем копию вектора, а изменяем существующий.

Но как оно работает внутри? Как нетрудно догадаться из определения нормализации, чтобы привести вектор к единичному - нам нужна его длина, а длина вектора - это корень по теореме Пифагора. Поэтому если вы по какой-то причине уже получили длину вектора, то просто поделите (ну поделите, ага https://t.me/unsafecsharp/150):

var inv_length = 1f / length;
v.x *= inv_length;
v.y *= inv_length;

И получите нормализованный вектор.

А то я часто встречаю примерно такой код в хот частях:

var length = v.magnitude;
var n = v.normalized;

Хотя на деле проще было бы просто получить длину один раз и использовать ее дважды (ну или сделать свой метод для этого).

Читать далее  

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

А теперь самое инетерсное, начал разбираться и оказалось, что наш картоделатель посадил по 5к юнитов в домики "за картой", чтобы потом оттуда пускать их по триггеру. А рендер работает таким образом, что ему нужно знать верхнее ограничение по отрисовке количества юнитов. Для нормальной игры это вычислялось по формуле: "берем сумму всех юнитов во всех домиках на карте и умножаем на количество игроков (т.к. в теории каждый игрок может применить какой-нибудь скилл, который сдублирует юнитов, например)". Но если вдруг мы что-то не учли - мы просто не рисуем юнитов и все, что в принципе ок, т.к. когда у тебя 1к юнитов на экране - ты уже не понимаешь где кто, а когда 4к - так и подавно.

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

Так вот мы жили с такой игровой картой уже месяца 3 и только сейчас заметили проблему на девайсе с памятью, хотя у нас вместо того, чтобы рисовать и обрабатывать 4к юнитов максимум (+рендер на 20к), мы вдруг начали обрабатывать 220к и никто этого не заметил... И, наверное, если бы не этот девайс - так бы и пошли в релиз 🙂

А вы все "преждевременная оптимизация, преждевременная оптимизация..." 😂

Читать далее  

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

Я часто встречаю вот такой код:

 а += б/2ф;

И мне все время такое хочется, напиши такой код:

 а += б * 0,5f;

Также можно заменить любые другие константы по тому же принципу. Но что делать, если у нас разделение не на константу, а на x? Да все просто, делаем y = 1f / x и используем уже y.

Читать далее  

Условие всегда ленивое и хочет побыстрее выйти. Если v1 будет правдой, то что там дальше его не будет интересовать:

 if (v1 == true || v2 == true) {...}

Таким образом, если у нас есть такой код:

 вар v1 = CalcV1();
вар v2 = CalcV2();
if (v1 == true || v2 == true) {...}

Выглядит хоть и симпатично, но совершенно непроизводительно.

Лучше написать так:

 if (CalcV1() == true || CalcV2() == true) {...}

Естественно нужно понимать, что CalcV2 не будет возвращаться, если CalcV1 вернет true, поэтому не нужно это расчитывать. Но я надеюсь, что вы это :) знаете

Читать далее  

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

 текст = здоровье.ToString();

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

 текст = $"{value}/{maxValue}";

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

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

 текст = arr[здоровье];

или

 текст = arr[maxValue][значение];

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

Читать далее  

Мы знаем, что if в шейдере - плохо. На самом деле бывает плохо, а бывает и нормально. Тут зависит от условия и что мы в нем делаем. Но я предпочитаю не полагаться на компилятор, а просто не использовать if-конструкции.

Давайте разберем простой пример:

half4 color = ...;
if (color.a < _Threshold) {
     color = float4(1, 0, 0, 1);
}

Если альфа у нас меньше чем какое-то значение, то закрасим красным цветом, если больше - оставим цвет как был.

Реальный код может быть сложнее, могут быть вложенные if-конструкции и т.д.

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

if (color.a < _Threshold) {
    color = XXX;
}

Теперь разберемся с условием, давайте перенесем все в одну сторону:

color.a - _Threshold < 0

Теперь наша задача записать все в одну строку:

color = lerp(XXX, color, color.a - _Threshold);

Т.е. мы возьмем XXX, если значение будет ноль и оставим color, если значение будет единица.

Теперь нам нужно избавиться от этой "плавности", одним из способов это сделать - добавить 0.5 и выполнить round (можно еще +0.5 и floor):

color = lerp(XXX, color, round((color.a - _Threshold) + 0.5));

Таким образом мы избавились от условия, чего и добивались :)


Читать далее  

public T A<T>() where T : struct {
    var t = new T();
    ...
    return t;
} 

Вот такой код мы обычно воспринимаем как "сделать default значение и потом мы его вернем". Все бы ничего, но это не совсем так. Вот как будет выглядеть этот код:

public T A<T>() where T : struct {
    var t = System.Activator.CreateInstance<T>();
    ...
    return t;
} 

Исправить это довольно просто:

public T A<T>() where T : struct {
    T t = default;
    ...
    return t;
} 
Читать далее