ИÑпользование объединений в Burst
(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();
Мы получили на выходе:
123 1078523331 4
Ðто именно то, что мы ожидали и Ñто означает, что проÑтые Ð¾Ð±ÑŠÐµÐ´Ð¸Ð½ÐµÐ½Ð¸Ñ Ñ€Ð°Ð±Ð¾Ñ‚Ð°ÑŽÑ‚ в 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], esi mov eax, dword ptr [rdi + 4] mov dword ptr [rcx], eax mov 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();
ЗапуÑтим и поÑмотрим вывод:
123 1078523331 Int Float 8
Ðто Ñнова, именно то, что мы ожидали увидеть. Те же 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], esi mov dword ptr [rdx], ecx mov ecx, dword ptr [rdi + 4] mov eax, dword ptr [rax + 4] mov dword ptr [r9], ecx mov dword ptr [rdx + 4], eax mov dword ptr [r8], 8
Снова мы видим неÑколько mov
инÑтрукций без преобразований между int
и float
. Ð’Ñе кроме конÑтанты sizeof
ÑвлÑетÑÑ Ð¸Ð½Ð´ÐµÐºÑом NativeArray
или объединением выровненных значений по 4 байта.
Заключение
ÐžÐ±ÑŠÐµÐ´Ð¸Ð½ÐµÐ½Ð¸Ñ Ñ€Ð°Ð±Ð¾Ñ‚Ð°ÑŽÑ‚ в Burst так же хорошо как и в IL2CPP или Mono. Мы можем уÑпешно иÑпользовать Ð¾Ð±ÑŠÐµÐ´Ð¸Ð½ÐµÐ½Ð¸Ñ Ð¾Ð±Ð¾Ð¸Ñ… видов: проÑтые и Ñ Ñ‚ÐµÐ³Ð°Ð¼Ð¸. Сложные операции, такие как получение битов float
в виде int
, тривиальны Ñ Ð¸Ñпользованием объединений. Также немаловажно, что при иÑпользовании парадигмы или-или доÑтигаетÑÑ ÑÐºÐ¾Ð½Ð¾Ð¼Ð¸Ñ Ð¿Ð°Ð¼Ñти.