Впихнуть невпихнуемое
Сейчас я вам покажу как вытащить полноценный цвет изображения из черно-белой картинки. Но прежде чем мы перейдем к магии давайте начнем с маленького.
Все мы с вами знаем, что для того чтобы описать два состояния объекта мы используем тип bool. Только true или false, 0 или 1, всего два значения, нет ничего проще. Но почему же тогда sizeof(bool) вернет 1 байт? Не 1 бит, а именно 1 байт, т.е. целых 8 бит. Не слишком ли много для хранения всего двух значений?
Ноги данной проблемы растут с исторического выбора дизайна языка Си. Это обусловлено процессорами, которые очень требовательны к типам переменных и их выраваниванию в памяти, чтобы процессор мог обновить их за один цикл шины памяти. Кроме того стоит вопрос производительности. Архитектура вычислительной системы не способна адресовать БИТ памяти, минимальная единица - байт. Для извлечения бита из байта требуется дополнительная инструкция, которая не предоставляется бесплатно. Но сейчас мы не будем углубляться в такие дебри, а просто примем за факт, что фактический размер bool в c# составляет 1 байт.
Но, получается тогда что в 1 bool, мы можем вместить 8 bool'ов. С некоторыми оговорками, но получается так. Только вместо того самого одного була мы будем использовать любой тип контейнер (от byte 8 бит до long 64 бита), а вместо тех самых 8 булов мы будем использовать непосредственно биты данного адреса памяти, которых как мы определили выше может быть даже не 8, а целых 64.
Представим, что мы в игре хотим устроить расчлененку. Отрывать руки и ноги персонажу. Но как нам понять какие части тела у него в данный момент присутсвуют на теле, а какие нет? Решение в лоб - задать для каждой части тела по одному bool, который будет отвечать за наличие той или иной части тела.
Но, что если мы все это запихаем в одно число. Всего одна переменная для хранения такого большого числа состояний.
Для этого в c# можно воспользоваться флагами. Выглядят они как обычный enum, с небольшим отличием.
Атрибут Flag указывает, что перечисление может обрабатываться как битовое поле (т.е. набор флагов). Отличие от простого enum будет в том, что диапазон целых чисел, включая те, что не представляют базовые значения типа перечисления, могут быть приведены к типу этого перечисления. Вы наверняка заметили необычную порядковую нумерацию. Дело в том, что атрибут [Flags] не присваивает значениям степень двойки. А это необходимо для правильной работы в битовом поле.
Head: 00000001
LeftArm: 00000010
RightArm: 00000100
Body: 00001000
Таким образом мы получаем enum - как тип хранения наших состояний. А мы знаем, что enum в c# представляет собой Int32. Т.е. 32 бита (4 байта), это конечно немного больше чем мы хотели, но и потенциала больше (32 значения, вместо 4).
Теперь мы можем добавлять нужные нам биты в наше значение и проверять их наличие.
Да это не cупер удобно, но при помощи вспомогательного класса или методов расширения класса, можно сделать нечто подобное.
И тогда теперь наш код может выглядеть следующим образом.
И таким образом вместо 8 булевых переменных, хранимых раздельно и занимающих 8 байт. Мы все засунули в один Int размером 4 байта. Но изначально мы же говорили об одном байте.
Тогда нам нужно обратиться не к Int а к Byte. То есть нам придется работать напрямую с типом byte вместо enum'a.
Итак нам удалось ужать значение bool в 8 раз!
Сильный ли это выигрыш по памяти? В текущих реалиях, когда у всех под капотом гигабайты памяти нет. Влияет ли это на быстродействие системы? В текущих реалиях опять же нет. Круто ли это? Определенно да! Как минимум вы должны это знать, владеть этим знанием и уметь применять его на практике. Так же нельзя не отметить, что это достаточно удобно.
Но давайте пойдем дальше и засунем в нашем число уже на просто какие-то там були. А что-то ПОБОЛЬШЕ. Например цвет. Да тот самый Color (Vector4), т.е. 4 float'a.
Float как мы знаем занимает 4 байта (32 бит). А значит всего нам нужно будет как-то запихнуть 16 байт (128 бит) в один Int (32 бит). Здесь уже у нас не получится поступить так как мы делали выше. Так как нам просто не хватает памяти. Но мы можем немного схитрить. Ведь мы же хотели представить цвет в числе. А как мы знаем цвет имеет значения от 0 до 255 на каждый канал. Остальные значения нам просто не нужны. А диапазон значений от 0 до 255 имеет тип byte как мы уже знаем. Который имеет размер 8 бит. 4 х 8 = 32. Аккурат размер int32. А значит нам должно хватить этого размера для того чтобы сохранить цвет (пусть и с небольшой потерей точности) в Int.
Выглядеть это будет следующим образом.
Казалось бы если с Bool и Int еще куда не шло. Но к чему эти проблемы на ровном месте с цветом. Памяти сейчас вагон, быстродействие на уровне. Но необходимость в этих костылях появляется там где быстродействие и по сей день является узким местом. В шейдерах очень важна как память, так и скорость. И здесь как раз такие хитрости нам и помогают. В cginc есть готовые методы даже для этого. Можете сами попробовать у себя в шейдерах.
EncodeFloatRGBA
DecodeFloatRGBA
Вот например как мы можем преобразить монохромную картинку с одним каналом цвета в цветное изображение.
А на этом я думаю можно остановиться запихивать все подряд куда не поподя. Следующим постом как всегда для подписчиков прилетят исходники данного урока с дополнительными материалами.
unity
shader
c#
шейдер
юнити
оптимизация