(Russian translation from English by Maxim Voloshin)

Burst компилятор Unity навязывает использование интересного подмножества C#. Правило “никаких управляемых объектов” не всегда справедливо. Сегодня мы посмотрим на исключения (Exception), которые являются управляемыми объектами, но частично поддерживаются Burst. Что же разрешено, а что запрещено? Читайте дальше, чтобы узнать.

Документация Burst гласит:

Burst работает с подмножеством .NET, которое не допускает использования любых управляемых объектов/ссылочных типов в вашем коде (классы в C#).

Позже указан список запрещенных типов:

string так как это управляемый тип

И еще раз позже:

Любые методы, относящиеся к управляемым объектам (например, методы, использующие string и т.д.)

Атрибут [BurstDiscard] может быть полезен, когда вы хотите использовать эти самые управляемые объекты

При запуске некоторого кода на полноценном C# (не внутри кода, компилируемого через Burst) Вы можете захотеть использовать некоторые управляемые объекты, но при этом не компилировать эти части кода при использовании Burst.

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

Давайте попробуем написать несколько тестовых задач в Job System, которые используют исключения и строки

using System;
using Unity.Burst;
using Unity.Jobs;
using UnityEngine;
 
class TestScript : MonoBehaviour
{
    [BurstCompile]
    struct ExceptionJob : IJob
    {
        public void Execute()
        {
            throw new ArgumentException("boom");
        }
    }
 
    [BurstCompile]
    struct BeginExceptionJob : IJob
    {
        public int I;
 
        public void Execute()
        {
            throw new ArgumentException("boom" + I);
        }
    }
 
    [BurstCompile]
    struct EndExceptionJob : IJob
    {
        public int I;
 
        public void Execute()
        {
            throw new ArgumentException(I + "boom");
        }
    }
 
    [BurstCompile]
    struct MiddleExceptionJob : IJob
    {
        public int I;
 
        public void Execute()
        {
            throw new ArgumentException(I + "boom" + I);
        }
    }
 
    void Start()
    {
        new ExceptionJob().Schedule().Complete();
        new BeginExceptionJob { I = 10 }.Schedule().Complete();
        new EndExceptionJob { I = 10 }.Schedule().Complete();
        new MiddleExceptionJob { I = 10 }.Schedule().Complete();
    }
}

Теперь мы запустим это в Unity 2019.1.3f1 и Burst 1.0.0 на macOS и посмотрим на результат. Во-первых, это прекрасно компилируется в редакторе и билд для macOS тоже работает. Никаких ошибок или предупреждений. Если запустить macOS приложение, то мы увидим в консоли следующее:

System.ArgumentException: boom
This Exception was thrown from a job compiled with Burst, which has limited exception support. Turn off burst (Jobs -> Enable Burst Compiler) to inspect full exceptions & stacktraces.
 
(Filename:  Line: -1)
 
System.ArgumentException: boom
This Exception was thrown from a job compiled with Burst, which has limited exception support. Turn off burst (Jobs -> Enable Burst Compiler) to inspect full exceptions & stacktraces.
 
(Filename:  Line: -1)
 
System.ArgumentException: boom
This Exception was thrown from a job compiled with Burst, which has limited exception support. Turn off burst (Jobs -> Enable Burst Compiler) to inspect full exceptions & stacktraces.
 
(Filename:  Line: -1)
 
System.ArgumentException: boom
This Exception was thrown from a job compiled with Burst, which has limited exception support. Turn off burst (Jobs -> Enable Burst Compiler) to inspect full exceptions & stacktraces.
 
(Filename:  Line: -1)

Обратите внимание, что все четыре задачи бросили исключение System.ArgumentException, содержащее сообщение boom. Ни одна из конкатенаций не отработала и мы остались только со строкой "boom".

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

; ExceptionJob
movabs  rax, offset .Lburst_abort_Ptr
mov     rax, qword ptr [rax]
movabs  rdi, offset .Lburst_abort.error.id
movabs  rsi, offset .Lburst_abort.error.message
jmp     rax
 
; BeginExceptionJob
movabs  rax, offset .Lburst_abort_Ptr
mov     rax, qword ptr [rax]
movabs  rdi, offset .Lburst_abort.error.id
movabs  rsi, offset .Lburst_abort.error.message
jmp     rax
 
; EndExceptionJob
movabs  rax, offset .Lburst_abort_Ptr
mov     rax, qword ptr [rax]
movabs  rdi, offset .Lburst_abort.error.id
movabs  rsi, offset .Lburst_abort.error.message
jmp     rax
 
; MiddleExceptionJob
movabs  rax, offset .Lburst_abort_Ptr
mov     rax, qword ptr [rax]
movabs  rdi, offset .Lburst_abort.error.id
movabs  rsi, offset .Lburst_abort.error.message
jmp     rax

Все четыре задачи были скомпилированы в абсолютно одинаковый код. Отсутствуют инструкции чтения значения переменной I и его конкатенации со строкой "boom". Вместо этого мы видим, что .Lburst_abort.error.id и .Lburst_abort.error.message выводятся вследствие возникновения исключения, и программа переходит в .Lburst_abort_Ptr. Для того чтобы узнать больше об этих символах, давайте посмотрим на следующий ассемблерный код:

.Lburst_abort.error.id:
        .asciz  "System.ArgumentException"
        .size   .Lburst_abort.error.id, 25
 
        .type   .Lburst_abort.error.message,@object
.Lburst_abort.error.message:
        .asciz  "boom"
        .size   .Lburst_abort.error.message, 5
 
        .type   .Lburst_abort_Ptr,@object
        .local  .Lburst_abort_Ptr
        .comm   .Lburst_abort_Ptr,8,8
        .type   .Lburst_abort.function.string,@object
.Lburst_abort.function.string:
        .asciz  "burst_abort"
        .size   .Lburst_abort.function.string, 12
 
        .section        .debug_str,"MS",@progbits,1

Здесь мы видим, что .Lburst_abort.error.id содержит ASCII (.asciz) строку "System.ArgumentException", размер которой (.size) равен 25, что соответствует количеству символов в строке плюс символ конца строки NUL.

.Lburst_abort.error.message содержит ASCII строку "boom", размер которой равен 5 все по тем же причинам.

.Lburst_abort_Ptr внутри .Lburst_abort.error.message это адрес в памяти, по которому перейдет выполнение программы во время бросания исключения.

Таким образом, когда бросается исключение в коде, скомпилированном через Burst, указатель на строку с типом исключения (System.ArgumentException, в данном случае) и указатель на строку с сообщением (boom) будут записаны в определенные регистры и выполнение программы перейдет по адресу.Lburst_abort_Ptr, где исключение, по всей видимости, обрабатывается чтением из этих регистров.

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

Так что не стесняйтесь использовать исключения в Job System с Burst компилятором до тех пор, пока сообщение является строковым литералом или конкатенации не нужны. Только имейте в виду, что catch и finally нельзя использовать с Burst, следовательно, бросание исключения всегда будет приводить к фатальной ошибке.