JacksonDunstan.com | Использование объединений в Burst (2024)

(Russian translation from English by Maxim Voloshin)

Мы знаем как использовать объединения(union) в C#, но работает ли это с новым Burst компилятором? Сегодня мы это протестируем и увидим сможет ли он справиться с некоторыми более продвинутыми особенностями кастомизации структур C#!

Простое объединение

Давайте начнем с объединения, которое может содержать или int, или float. Это может быть полезно для увеличения производительности при битовых операциях без выполнения какого-либо преобразования. Это особенно полезно для представления битов float в виде int для таких задач как сериализация. Вот как выглядит наше объединение:

[StructLayout(LayoutKind.Explicit)]struct IntFloatUnion{ [FieldOffset(0)] public int I; [FieldOffset(0)] public float F;}

Ключевыми моментами являются атрибуты [StructLayout] и [FieldOffset]. Указывая LayoutKind.Explicit, мы получаем контроль над тем, где биты, представляющие поля структуры, расположены в памяти, которую занимает структура. Мы указываем расположение памяти как смещение в байтах, от начала структуры. Передавая 0 в обоих случаях, мы говорим, что оба поля должны занять те же самые биты.

Теперь мы можем обращаться к битам структуры типа intиспользуя union.I или float используя union.F. Это работает и на чтение и на запись. Для того чтобы протестировать это, создадим задачу для Burst компилятора, которая делает следующее:

[BurstCompile]unsafe struct UnionTestJobJob : IJob{ [ReadOnly] public int InIntValue; [ReadOnly] public float InFloatValue; [WriteOnly] public NativeArray<int> OutIntValue; [WriteOnly] public NativeArray<int> OutFloatValue; [WriteOnly] public NativeArray<int> OutSize; public void Execute() { IntFloatUnion u; u.I = InIntValue; OutIntValue[0] = u.I; u.F = InFloatValue; OutFloatValue[0] = u.I; OutSize[0] = sizeof(IntFloatUnion); }}

Все, что мы делаем здесь – записываем значение int и считываем его, затем пишем значение float и читаем его как int. Мы, также проверяем размер структуры, чтобы подтвердить, что она не может содержать одновременно int и float и на самом деле использует одну и ту же память для обоих типов.

Теперь запустим задачу:

NativeArray<int> outIntValue = new NativeArray<int>(1, Allocator.TempJob);NativeArray<int> outFloatValue = new NativeArray<int>(1, Allocator.TempJob);NativeArray<int> outSize = new NativeArray<int>(1, Allocator.TempJob);new UnionTestJobJob{ InIntValue = 123, InFloatValue = 3.14f, OutIntValue = outIntValue, OutFloatValue = outFloatValue, OutSize = outSize}.Run();Debug.Log(outIntValue[0]);Debug.Log(outFloatValue[0]);Debug.Log(outSize[0]);outIntValue.Dispose();outFloatValue.Dispose();outSize.Dispose();

Мы получили на выходе:

12310785233314

Это именно то, что мы ожидали и это означает, что простые объединения работают в Burst!

  • Записали 123 как int и успешно прочитали как int со значением 123
  • Записали 3.14f как float и прочитали как int в виде: 1078523331
  • Ðазмер структуры равен 4, и это означает, что int и float используют одни и те же четыре байта

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

mov rax, qword ptr [rdi + 8]mov rcx, qword ptr [rdi + 64]mov rdx, qword ptr [rdi + 120]mov esi, dword ptr [rdi]mov dword ptr [rax], esimov eax, dword ptr [rdi + 4]mov dword ptr [rcx], eaxmov dword ptr [rdx], 4

Все что мы видим здесь это mov инструкции. Мы не видим никаких преобразований между int и float. Это именно то, что мы ожидаем от подобных объединений!

Объединение с тегом

Теперь немного увеличим сложность объединения добавив поле “тег”. Это поле, которое отображает в каком состоянии находится объединение. Ð’ данном случае, тег указывает чем оно является int или float. Мы можем определить тег как перечисление, тип констант которого int:

enum IntFloatTaggedUnionType : int{ Int, Float}

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

Наконец, определим структуру объединения:

[StructLayout(LayoutKind.Explicit)]struct IntFloatTaggedUnion{ [FieldOffset(0)] public IntFloatTaggedUnionType Type; [FieldOffset(4)] public int I; [FieldOffset(4)] public float F;}

Это очень похоже на первое объединение, за исключением того, что из-за тега int и float сдвинуты на 4 байта. Тег находится в начале структуры и не перекрывает собой часть структуры, выполняющую функцию объединения. Это означает, что структура частично ведет себя как объединение и частично как структура, отсюда и дополнительная сложность.

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

[BurstCompile]unsafe struct TaggedUnionTestJobJob : IJob{ [ReadOnly] public int InIntValue; [ReadOnly] public float InFloatValue; [ReadOnly] public NativeArray<IntFloatTaggedUnionType> InTypeValues; [WriteOnly] public NativeArray<int> OutIntValue; [WriteOnly] public NativeArray<int> OutFloatValue; [WriteOnly] public NativeArray<IntFloatTaggedUnionType> OutTypeValues; [WriteOnly] public NativeArray<int> OutSize; public void Execute() { IntFloatTaggedUnion u; u.I = InIntValue; u.Type = InTypeValues[0]; OutIntValue[0] = u.I; OutTypeValues[0] = u.Type; u.F = InFloatValue; u.Type = InTypeValues[1]; OutFloatValue[0] = u.I; OutTypeValues[1] = u.Type; OutSize[0] = sizeof(IntFloatTaggedUnion); }}

Мы делаем почти такой же тест, что и с простым объединением, если не считать чтение и запись тега Type.

И небольшой скрипт, запускающий задачу:

NativeArray<IntFloatTaggedUnionType> inTypeValues = new NativeArray<IntFloatTaggedUnionType>(2, Allocator.TempJob);inTypeValues[0] = IntFloatTaggedUnionType.Int;inTypeValues[1] = IntFloatTaggedUnionType.Float;NativeArray<int> outIntValue = new NativeArray<int>(1, Allocator.TempJob);NativeArray<int> outFloatValue = new NativeArray<int>(1, Allocator.TempJob);NativeArray<IntFloatTaggedUnionType> outTypeValues = new NativeArray<IntFloatTaggedUnionType>(2, Allocator.TempJob);NativeArray<int> outSize = new NativeArray<int>(1, Allocator.TempJob);new TaggedUnionTestJobJob{ InIntValue = 123, InFloatValue = 3.14f, InTypeValues = inTypeValues, OutIntValue = outIntValue, OutFloatValue = outFloatValue, OutTypeValues = outTypeValues, OutSize = outSize}.Run();Debug.Log(outIntValue[0]);Debug.Log(outFloatValue[0]);Debug.Log(outTypeValues[0]);Debug.Log(outTypeValues[1]);Debug.Log(outSize[0]);inTypeValues.Dispose();outIntValue.Dispose();outFloatValue.Dispose();outTypeValues.Dispose();outSize.Dispose();

Запустим и посмотрим вывод:

1231078523331IntFloat8

Это снова, именно то, что мы ожидали увидеть. Те же 123 и 1078523331 значения соответствуют 123 и 3.14f, так же как с простым объединением. Они не были повреждены присутствием поля с тегом. Сам тег, после присвоения, был корректно прочитан как значения перечисления Int и Float. Ðазмер равен 8, что соответствует тегу (4) и объединению (4).

Все отлично работает, осталось проверить ассемблерный код в Burst инспекторе:

mov rax, qword ptr [rdi + 8]mov r10, qword ptr [rdi + 64]mov rdx, qword ptr [rdi + 176]mov r9, qword ptr [rdi + 120]mov r8, qword ptr [rdi + 232]mov esi, dword ptr [rdi]mov ecx, dword ptr [rax]mov dword ptr [r10], esimov dword ptr [rdx], ecxmov ecx, dword ptr [rdi + 4]mov eax, dword ptr [rax + 4]mov dword ptr [r9], ecxmov dword ptr [rdx + 4], eaxmov dword ptr [r8], 8

Снова мы видим несколько mov инструкций без преобразований между int и float. Все кроме константы sizeof является индексом NativeArray или объединением выровненных значений по 4 байта.

Заключение

Объединения работают в Burst так же хорошо как и в IL2CPP или Mono. Мы можем успешно использовать объединения обоих видов: простые и с тегами. Сложные операции, такие как получение битов float в виде int, тривиальны с использованием объединений. Также немаловажно, что при использовании парадигмы или-или достигается экономия памяти.

JacksonDunstan.com |   Использование объединений в Burst (2024)
Top Articles
Latest Posts
Recommended Articles
Article information

Author: Dean Jakubowski Ret

Last Updated:

Views: 5605

Rating: 5 / 5 (70 voted)

Reviews: 93% of readers found this page helpful

Author information

Name: Dean Jakubowski Ret

Birthday: 1996-05-10

Address: Apt. 425 4346 Santiago Islands, Shariside, AK 38830-1874

Phone: +96313309894162

Job: Legacy Sales Designer

Hobby: Baseball, Wood carving, Candle making, Jigsaw puzzles, Lacemaking, Parkour, Drawing

Introduction: My name is Dean Jakubowski Ret, I am a enthusiastic, friendly, homely, handsome, zealous, brainy, elegant person who loves writing and wants to share my knowledge and understanding with you.