Как Burst компилирует switch и ref параметры
(Russian translation from English by Maxim Voloshin)
Ð¡ÐµÐ³Ð¾Ð´Ð½Ñ Ð¼Ñ‹ вернемÑÑ Ðº оÑновам и увидим как Burst компилирует некоторые фундаментальные оÑобенноÑти Ñзыка: оператор switch
и ref
параметры… Ñ ÑƒÐ´Ð¸Ð²Ð¸Ñ‚ÐµÐ»ÑŒÐ½Ñ‹Ð¼Ð¸ результатами!
Обычный switch
Давайте начнем Ñ Ð½Ð°Ð¿Ð¸ÑÐ°Ð½Ð¸Ñ Ð·Ð°Ð´Ð°Ñ‡Ð¸ Ñ Ð¿Ñ€Ð¾Ñтым switch
:
[BurstCompile] struct PlainSwitchJob : IJob { [ReadOnly] public int InVal; [WriteOnly] public NativeArray<int> OutVal; public void Execute() { int outVal; switch (InVal) { case 10: outVal = 20; break; case 20: outVal = 40; break; case 30: outVal = 50; break; case 40: outVal = 60; break; default: outVal = 100; break; } OutVal[0] = outVal; } }
Теперь, поÑмотрим в Burst ИнÑпекторе Unity 2019.1.10f1 и Burst 1.1.1 как Ñта задача была Ñкомпилирована Ð´Ð»Ñ 64 битной macOS:
mov r10d, dword ptr [rdi] cmp r10d, 20 mov ecx, 40 mov r8d, 20 mov esi, 20 cmovg esi, ecx mov edx, 60 cmovle edx, ecx mov r9d, 30 mov ecx, 10 cmovg ecx, r9d mov eax, 50 cmovle eax, r8d cmp r10d, esi mov esi, 100 cmove esi, edx cmp r10d, ecx cmove esi, eax mov rax, qword ptr [rdi + 8] mov dword ptr [rax], esi ret
ЗдеÑÑŒ мы видим Ñерию инÑтрукций Ñравнений (cmp
) Ñ InVal
(r10d
) и уÑловного перехода завиÑÑщего от того, был ли результат больше (cmovg
), меньше либо равно (cmovle
), или равно (cmove
). По Ñути Ñто ÑÐµÑ€Ð¸Ñ if
и else
, за иÑключением того, что нет инÑтрукций перехода.
Switch Ñ Ð¾Ð¿ÐµÑ€Ð°Ñ‚Ð¾Ñ€Ð¾Ð¼ when
Теперь воÑпользуемÑÑ Ñ„Ð¸Ñ‡ÐµÐ¹ C# 7 и добавим к банальному case
уÑловие Ñ ÐºÐ»ÑŽÑ‡ÐµÐ²Ñ‹Ð¼ Ñловом when
:
[BurstCompile] struct WhereSwitchJob : IJob { [ReadOnly] public int InVal; [WriteOnly] public NativeArray<int> OutVal; public void Execute() { int outVal; switch (InVal) { case int _ when InVal == 10: outVal = 20; break; case int _ when InVal == 20: outVal = 40; break; case int _ when InVal == 30: outVal = 50; break; case int _ when InVal == 40: outVal = 60; break; default: outVal = 100; break; } OutVal[0] = outVal; } }
Обратите внимание, что в Ñтой верÑии, Ñамо уÑловие и результат идентичны предыдущему коду. Увидим ли мы идентичный код, Ñгенерированный Burst? Давайте проверим:
mov ecx, dword ptr [rdi] add ecx, -10 cmp ecx, 30 ja .LBB0_5 mov eax, 20 movabs rdx, offset .LJTI0_0 movsxd rcx, dword ptr [rdx + 4*rcx] add rcx, rdx jmp rcx .LBB0_2: mov eax, 40 .LBB0_6: mov rcx, qword ptr [rdi + 8] mov dword ptr [rcx], eax ret .LBB0_5: mov eax, 100 mov rcx, qword ptr [rdi + 8] mov dword ptr [rcx], eax ret .LBB0_3: mov eax, 50 mov rcx, qword ptr [rdi + 8] mov dword ptr [rcx], eax ret .LBB0_4: mov eax, 60 mov rcx, qword ptr [rdi + 8] mov dword ptr [rcx], eax ret
Результаты определенно не идентичны! Ð’ данном Ñлучае, Burst Ñгенерировал таблицу переходов Ð´Ð»Ñ Ñ‚Ð¾Ð³Ð¾ чтобы выполнить только одно Ñравнение Ñ InVal
. Обратите внимание, что код OutVal[0] = inVal
был продублирован в каждом case
, Ñлегка Ñ€Ð°Ð·Ð´ÑƒÐ²Ð°Ñ ÐºÐ¾Ð´.
Ðе ref параметры
ÐаÑтало Ð²Ñ€ÐµÐ¼Ñ Ð¿Ð¾Ñмотреть как Burst обращаетÑÑ Ñ Ð±Ð¾Ð»ÑŒÑˆÐ¸Ð¼Ð¸ параметрами, когда их передают без иÑÐ¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ð½Ð¸Ñ ref
:
[StructLayout(LayoutKind.Explicit)] struct BigStruct { [FieldOffset(128)] public int Val; } [BurstCompile] struct NonRefJob : IJob { [ReadOnly] public BigStruct Val1; [ReadOnly] public BigStruct Val2; [WriteOnly] public NativeArray<int> Result; public void Execute() { int f = Foo(Val1); f += Foo(Val2); Result[0] = f; } int Foo(BigStruct val) { int outVal; switch (val) { case BigStruct b when b.Val == 10: outVal = 20; break; case BigStruct b when b.Val == 20: outVal = 40; break; case BigStruct b when b.Val == 30: outVal = 50; break; case BigStruct b when b.Val == 40: outVal = 60; break; case BigStruct b when b.Val == 50: outVal = 70; break; case BigStruct b when b.Val == 60: outVal = 80; break; case BigStruct b when b.Val == 70: outVal = 90; break; case BigStruct b when b.Val == 80: outVal = 110; break; default: outVal = 100; break; } return outVal; } }
Структура BigStruct
Ñделана большой путем ÑÐ¼ÐµÑ‰ÐµÐ½Ð¸Ñ Val
на 128 байт. Ðто дает размер в 132 байта, что, неÑомненно, довольно много.
Далее, вы делаем два вызова Foo
Ñ BigStruct
в качеÑтве обычного, не ref
параметра. Оператор switch
внутри Foo
– раÑÑˆÐ¸Ñ€ÐµÐ½Ð½Ð°Ñ Ð²ÐµÑ€ÑÐ¸Ñ Ñ‚Ð¾Ð³Ð¾, что мы только что видели. ДобавлÑÑ Ð·Ð½Ð°Ñ‡Ð¸Ñ‚ÐµÐ»ÑŒÐ½Ð¾Ðµ количеÑтво команд в Foo
и Ð²Ñ‹Ð·Ñ‹Ð²Ð°Ñ ÐµÐµ дважды Ñ Ñ€Ð°Ð·Ð»Ð¸Ñ‡Ð½Ñ‹Ð¼Ð¸ параметрами, мы гарантируем, что Burst не вÑтроит(inline) ее код и не завалит наш теÑÑ‚, полноÑтью удалив параметры.
Теперь можно поÑмотреть во что Ñто вÑе ÑкомпилировалоÑÑŒ:
; Execute push rbp push r14 push rbx sub rsp, 288 mov rbx, rdi movups xmm0, xmmword ptr [rbx] movups xmm1, xmmword ptr [rbx + 16] movups xmm2, xmmword ptr [rbx + 32] movups xmm3, xmmword ptr [rbx + 48] movups xmm4, xmmword ptr [rbx + 64] movups xmm5, xmmword ptr [rbx + 80] movups xmm6, xmmword ptr [rbx + 96] movups xmm7, xmmword ptr [rbx + 112] mov eax, dword ptr [rbx + 128] movaps xmmword ptr [rsp], xmm0 movaps xmmword ptr [rsp + 16], xmm1 movaps xmmword ptr [rsp + 32], xmm2 movaps xmmword ptr [rsp + 48], xmm3 movaps xmmword ptr [rsp + 64], xmm4 movaps xmmword ptr [rsp + 80], xmm5 movaps xmmword ptr [rsp + 96], xmm6 movaps xmmword ptr [rsp + 112], xmm7 mov dword ptr [rsp + 128], eax movabs r14, offset ".LNonRefJob.Foo(NonRefJob* this, BigStruct val)_D56DDBCB4218697B" mov rdi, rsp call r14 mov ebp, eax movups xmm0, xmmword ptr [rbx + 132] movups xmm1, xmmword ptr [rbx + 148] movups xmm2, xmmword ptr [rbx + 164] movups xmm3, xmmword ptr [rbx + 180] movups xmm4, xmmword ptr [rbx + 196] movups xmm5, xmmword ptr [rbx + 212] movups xmm6, xmmword ptr [rbx + 228] movups xmm7, xmmword ptr [rbx + 244] mov eax, dword ptr [rbx + 260] movaps xmmword ptr [rsp + 144], xmm0 movaps xmmword ptr [rsp + 160], xmm1 movaps xmmword ptr [rsp + 176], xmm2 movaps xmmword ptr [rsp + 192], xmm3 movaps xmmword ptr [rsp + 208], xmm4 movaps xmmword ptr [rsp + 224], xmm5 movaps xmmword ptr [rsp + 240], xmm6 movaps xmmword ptr [rsp + 256], xmm7 mov dword ptr [rsp + 272], eax lea rdi, [rsp + 144] call r14 add eax, ebp mov rcx, qword ptr [rbx + 264] mov dword ptr [rcx], eax add rsp, 288 pop rbx pop r14 pop rbp ret ; Foo mov ecx, dword ptr [rdi + 128] add ecx, -10 cmp ecx, 70 ja .LBB1_10 mov eax, 20 movabs rdx, offset .LJTI1_0 movsxd rcx, dword ptr [rdx + 4*rcx] add rcx, rdx jmp rcx .LBB1_2: mov eax, 40 ret .LBB1_10: mov eax, 100 ret .LBB1_3: mov eax, 50 ret .LBB1_4: mov eax, 60 ret .LBB1_5: mov eax, 70 ret .LBB1_6: mov eax, 80 ret .LBB1_7: mov eax, 90 ret .LBB1_8: mov eax, 110 .LBB1_9: ret
Тело функции Foo
похоже на предыдущий код, поÑкольку оно вÑе еще иÑпользует таблицу переходов.
Внутри Execute
, мы видим две инÑтрукции call
, показывающие, что Foo
была вызвана дважды и не была вÑтроена. Ð”Ð»Ñ Ñ‚Ð¾Ð³Ð¾ чтобы Ñделать Ñти вызовы, Execute
помещает в Ñтек BigStruct
целиком.
ref параметры
Ðа Ñтот раз передадим BigStruct
как ref
параметр:
[BurstCompile] struct RefJob : IJob { [ReadOnly] public BigStruct Val1; [ReadOnly] public BigStruct Val2; [WriteOnly] public NativeArray<int> Result; public void Execute() { int f = Foo(ref Val1); f += Foo(ref Val2); Result[0] = f; } int Foo(ref BigStruct val) { int outVal; switch (val) { case BigStruct b when b.Val == 10: outVal = 20; break; case BigStruct b when b.Val == 20: outVal = 40; break; case BigStruct b when b.Val == 30: outVal = 50; break; case BigStruct b when b.Val == 40: outVal = 60; break; case BigStruct b when b.Val == 50: outVal = 70; break; case BigStruct b when b.Val == 60: outVal = 80; break; case BigStruct b when b.Val == 70: outVal = 90; break; case BigStruct b when b.Val == 80: outVal = 110; break; default: outVal = 100; break; } return outVal; } }
Ð’Ñе, что мы Ñделали – добавили ключевое Ñлово ref
, теперь поÑмотрим как Ñто повлиÑет на вывод Burst:
; Execute push rbp push r14 push rbx mov rbx, rdi movabs r14, offset ".LRefJob.Foo(RefJob* this, ref BigStruct val)_765ED9C01E5BA6A1" call r14 mov ebp, eax lea rdi, [rbx + 132] call r14 add eax, ebp mov rcx, qword ptr [rbx + 264] mov dword ptr [rcx], eax pop rbx pop r14 pop rbp ret ; Foo mov ecx, dword ptr [rdi + 128] add ecx, -10 cmp ecx, 70 ja .LBB1_10 mov eax, 20 movabs rdx, offset .LJTI1_0 movsxd rcx, dword ptr [rdx + 4*rcx] add rcx, rdx jmp rcx .LBB1_2: mov eax, 40 ret .LBB1_10: mov eax, 100 ret .LBB1_3: mov eax, 50 ret .LBB1_4: mov eax, 60 ret .LBB1_5: mov eax, 70 ret .LBB1_6: mov eax, 80 ret .LBB1_7: mov eax, 90 ret .LBB1_8: mov eax, 110 .LBB1_9: ret
Foo
не изменилаÑÑŒ, однако, в Ñтот раз Execute
гораздо короче. ВмеÑто запиÑи каждого байта BigStruct
в Ñтек, Foo
проÑто иÑпользует BigStruc
который уже приÑутÑтвует.
Заключение
Когда дело доходит до оператора switch
, Burst не может понÑÑ‚ÑŒ, что обычные case
идентичны варианту Ñ when
и, ÑоответÑтвенно, не может Ñгенерировать одинаковый код. Он, точно так же, не добавлÑет автоматичеÑки ref
к большим параметрам, даже еÑли Ñто было бы гораздо более Ñффективно. Ðта ÑÐ¸Ñ‚ÑƒÐ°Ñ†Ð¸Ñ Ð´Ð¾ÐºÐ°Ð·Ñ‹Ð²Ð°ÐµÑ‚ необходимоÑÑ‚ÑŒ почаще Ñмотреть в Burst ИнÑпектор, чтобы убедитьÑÑ, что мы получаем именно тот код, который должен выполнÑÑ‚ÑŒ процеÑÑор.