(Russian translation from English by Maxim Voloshin)

Обычно, задачи(job), скомпилированные Burst компилятором, не могут использовать управляемые массивы, но есть исключение для static readonly полей. Но, иногда, это может быть опасно, когда именно мы узнаем сегодня.

Как это предполагалось

Давайте посмотрим на пример допустимого использования static readonly управляемых массивов в Burst: таблицу замен. Создадим простую задачу, которая считывает целые числа из массива:

[BurstCompile]
struct ReadManagedArrayJob : IJob
{
    public static readonly int[] Array = { 1, 2, 3 };
 
    public int Index;
    public NativeArray<int> Result;
 
    public void Execute()
 
        Result[0] = Array[Index];
 
}

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

mov     rax, qword ptr [rdi + 8]
movsxd  rcx, dword ptr [rdi]
movabs  rdx, offset ".LSystem.Int32[] ReadManagedArrayJob::Array"
mov     ecx, dword ptr [rdx + 4*rcx]
mov     dword ptr [rax], ecx
<pre>
Это простая индексация в секции данных исполняемого файла <code>.LSystem.Int32[] ReadManagedArrayJob::Array</code>, которую можно увидеть ниже:
 
<pre lang="asm">
".LSystem.Int32[] ReadManagedArrayJob::Array":
    .long    1
    .long    2
    .long    3
    .size    ".LSystem.Int32[] ReadManagedArrayJob::Array", 12
    .section .debug_str,"MS",@progbits,1

Обратите внимание, что секция содержит только то, что мы поместили в массив: 1, 2, и 3.

Writing to the Array

Теперь посмотрим, что произойдет если мы нарушим правила и попробуем что-нибудь записать в управляемый массив:

[BurstCompile]
struct WriteManagedArrayJob : IJob
{
    public static readonly int[] Array = { 1, 2, 3 };
 
    public int Index;
    public NativeArray<int> Result;
 
    public void Execute()
 
        Array[Index] = Result[0];
 
}

Консоль Unity не содержит ошибок, но Burst инспектор говорит об ошибке компиляции:

/path/to/project/Assets/TestScript.cs(31,3): error: Accessing a managed array is not supported by burst
 at WriteManagedArrayJob.Execute(WriteManagedArrayJob* this) (at /path/to/project/Assets/TestScript.cs:31)
 at Unity.Jobs.IJobExtensions.JobStruct`1.Execute(ref WriteManagedArrayJob data, System.IntPtr additionalPtr, System.IntPtr bufferRangePatchData, ref Unity.Jobs.LowLevel.Unsafe.JobRanges ranges, int jobIndex) (at /Users/builduser/buildslave/unity/build/Runtime/Jobs/Managed/IJob.cs:30)
 
While compiling job: System.Void Unity.Jobs.IJobExtensions/JobStruct`1::Execute(T&,System.IntPtr,System.IntPtr,Unity.Jobs.LowLevel.Unsafe.JobRanges&,System.Int32)

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

Передача массивов

Теперь нарушим другое правило и попробуем передать массив в функцию как параметр:

[BurstCompile]
struct PassManagedArrayJob : IJob
{
    public static readonly int[] Array = { 1, 2, 3 };
 
    public int Index;
    public NativeArray<int> Result;
 
    public void Execute()
 
        Do(Array);
 
 
    public void Do(int[] a)
 
        Result[0] = a[Index];
 
}

В данном случае ошибки также отсутствуют в консоли, но присутствуют в Burst инспекторе и во время сборки проекта:

/path/to/project/Assets/TestScript.cs(45,3): error: The managed function `PassManagedArrayJob.Do(PassManagedArrayJob* this, int[] a)` is not supported by burst
 at PassManagedArrayJob.Execute(PassManagedArrayJob* this) (at /path/to/project/Assets/TestScript.cs:45)
 at Unity.Jobs.IJobExtensions.JobStruct`1.Execute(ref PassManagedArrayJob data, System.IntPtr additionalPtr, System.IntPtr bufferRangePatchData, ref Unity.Jobs.LowLevel.Unsafe.JobRanges ranges, int jobIndex) (at /Users/builduser/buildslave/unity/build/Runtime/Jobs/Managed/IJob.cs:30)
 
 
While compiling job: System.Void Unity.Jobs.IJobExtensions/JobStruct`1::Execute(T&,System.IntPtr,System.IntPtr,Unity.Jobs.LowLevel.Unsafe.JobRanges&,System.Int32)

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

Запись извне

А сейчас мы нарушим правило, гласящее, что массив нельзя менять снаружи задачи:

class TestScript : MonoBehaviour
{
    void Start()
 
        ReadManagedArrayJob.Array[0] = 100;
        NativeArray<int> array = new NativeArray<int>(1, Allocator.TempJob);
        new ReadManagedArrayJob { Result = array, Index = 0 }.Run();
        print(array[0]);
        array.Dispose();
 
}

Здесь мы перезаписываем 1 в первом элементе массива и, затем, запускаем задачу, которая читает первый элемент массива и выводит его значение в консоль. Что будет выведено?

100

Учитывая ассемблерный код, приведенный выше, это не имеет смысла. Задача будет считывать данные из константного массива, расположенного в секции данных исполняемого файла. Как он может быть изменен во время выполнения TestScript.Start?

Теперь, мы соберем проект, запустим его в macOS и получим результат:

1

Мы видим другой результат! Задача, скомпилированная Burst и используемая в билде, отличается по функционалу от версии задачи, используемой в редакторе. Опять-таки, может пройти какое-то время, прежде чем мы поймем, что допустили ошибку. Хуже всего, что в редакторе нет ни предупреждений компилятора, ни ошибок, ни даже сообщений во время сборки билда, говорящих о проблеме. Осторожнее!

Массив структур

Давайте попробуем использовать массив структур:

struct TestStruct
{
    public int Value;
 
    public TestStruct(int value)
 
        Value = value;
 
}
 
[BurstCompile]
struct ManagedArrayOfStructsJob : IJob
{
    public static readonly TestStruct[] Array = {
        new TestStruct(1),
        new TestStruct(2)
    };
 
    public int Index;
    public NativeArray<int> Result;
 
    public void Execute()
 
        Result[0] = Array[Index].Value;
 
}

Этот функционал эквивалентен ReadManagedArrayJob и мы видим точно такой же ассемблерный код в Burst инспекторе:

mov     rax, qword ptr [rdi + 8]
movsxd  rcx, dword ptr [rdi]
movabs  rdx, offset ".LTestStruct[] ManagedArrayOfStructsJob::Array"
mov     ecx, dword ptr [rdx + 4*rcx]
mov     dword ptr [rax], ecx

Секции данных, тоже выглядят похожим образом:

".LTestStruct[] ManagedArrayOfStructsJob::Array":
    .long    1
    .long    2
    .size    ".LTestStruct[] ManagedArrayOfStructsJob::Array", 8
    .section .debug_str,"MS",@progbits,1

Таким образом, при использовании структур и, в частности, чтении данных из них, нет дополнительных затрат производительности.

Структуры с условиями

Давайте вернемся к правилам, которые мы нарушали, и воспользуемся условиями, в данном случае тернарным оператором внутри конструктора нашей структуры:

struct TestStructWithConditional
{
    public int Value;
 
    public TestStructWithConditional(bool use1)
 
        Value = use1 ? 1 : 2;
 
}
 
[BurstCompile]
struct ManagedArrayOfStructsWithConditionalJob : IJob
{
    public static readonly TestStructWithConditional[] Array = {
        new TestStructWithConditional(true),
        new TestStructWithConditional(false)
    };
 
    public int Index;
    public NativeArray<int> Result;
 
    public void Execute()
 
        Result[0] = Array[Index].Value;
 
}

Burst инспектор содержит четыре ошибки:

/path/to/project/Assets/TestScript.cs(104,3): error: Accessing a managed array is not supported by burst
 at ManagedArrayOfStructsWithConditionalJob.Execute(ManagedArrayOfStructsWithConditionalJob* this) (at /path/to/project/Assets/TestScript.cs:104)
 at Unity.Jobs.IJobExtensions.JobStruct`1.Execute(ref ManagedArrayOfStructsWithConditionalJob data, System.IntPtr additionalPtr, System.IntPtr bufferRangePatchData, ref Unity.Jobs.LowLevel.Unsafe.JobRanges ranges, int jobIndex) (at /Users/builduser/buildslave/unity/build/Runtime/Jobs/Managed/IJob.cs:30)
 
 
While compiling job: System.Void Unity.Jobs.IJobExtensions/JobStruct`1::Execute(T&,System.IntPtr,System.IntPtr,Unity.Jobs.LowLevel.Unsafe.JobRanges&,System.Int32)/path/to/project/Assets/TestScript.cs(94,2): error: Creating a managed array `TestStructWithConditional[]` is not supported by burst
 at ManagedArrayOfStructsWithConditionalJob..cctor() (at /path/to/project/Assets/TestScript.cs:94)
 at ManagedArrayOfStructsWithConditionalJob.Execute(ManagedArrayOfStructsWithConditionalJob* this) (at /path/to/project/Assets/TestScript.cs:103)
 at Unity.Jobs.IJobExtensions.JobStruct`1.Execute(ref ManagedArrayOfStructsWithConditionalJob data, System.IntPtr additionalPtr, System.IntPtr bufferRangePatchData, ref Unity.Jobs.LowLevel.Unsafe.JobRanges ranges, int jobIndex) (at /Users/builduser/buildslave/unity/build/Runtime/Jobs/Managed/IJob.cs:30)
 
 
While compiling job: System.Void Unity.Jobs.IJobExtensions/JobStruct`1::Execute(T&,System.IntPtr,System.IntPtr,Unity.Jobs.LowLevel.Unsafe.JobRanges&,System.Int32)/path/to/project/Assets/TestScript.cs(94,2): error: Accessing a managed array is not supported by burst
 at ManagedArrayOfStructsWithConditionalJob..cctor() (at /path/to/project/Assets/TestScript.cs:94)
 at ManagedArrayOfStructsWithConditionalJob.Execute(ManagedArrayOfStructsWithConditionalJob* this) (at /path/to/project/Assets/TestScript.cs:103)
 at Unity.Jobs.IJobExtensions.JobStruct`1.Execute(ref ManagedArrayOfStructsWithConditionalJob data, System.IntPtr additionalPtr, System.IntPtr bufferRangePatchData, ref Unity.Jobs.LowLevel.Unsafe.JobRanges ranges, int jobIndex) (at /Users/builduser/buildslave/unity/build/Runtime/Jobs/Managed/IJob.cs:30)
 
 
While compiling job: System.Void Unity.Jobs.IJobExtensions/JobStruct`1::Execute(T&,System.IntPtr,System.IntPtr,Unity.Jobs.LowLevel.Unsafe.JobRanges&,System.Int32)/path/to/project/Assets/TestScript.cs(94,2): error: Accessing a managed array is not supported by burst
 at ManagedArrayOfStructsWithConditionalJob..cctor() (at /path/to/project/Assets/TestScript.cs:94)
 at ManagedArrayOfStructsWithConditionalJob.Execute(ManagedArrayOfStructsWithConditionalJob* this) (at /path/to/project/Assets/TestScript.cs:103)
 at Unity.Jobs.IJobExtensions.JobStruct`1.Execute(ref ManagedArrayOfStructsWithConditionalJob data, System.IntPtr additionalPtr, System.IntPtr bufferRangePatchData, ref Unity.Jobs.LowLevel.Unsafe.JobRanges ranges, int jobIndex) (at /Users/builduser/buildslave/unity/build/Runtime/Jobs/Managed/IJob.cs:30)
 
 
While compiling job: System.Void Unity.Jobs.IJobExtensions/JobStruct`1::Execute(T&,System.IntPtr,System.IntPtr,Unity.Jobs.LowLevel.Unsafe.JobRanges&,System.Int32)

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

И снова, ошибки отсутствуют в консоли редактора. Эти ошибки присутствуют только в Burst инспекторе или при сборке проекта, и могут остаться незамеченными.

Структуры с Исключениями(Exceptions)

И, наконец, попробуем бросить исключение внутри конструктора структуры:

struct TestStructWithException
{
    public int Value;
 
    public TestStructWithException(int value)
 
        Value = value;
        throw new Exception("boom");
 
}
 
[BurstCompile]
struct ManagedArrayOfStructsWithExceptionJob : IJob
{
    public static readonly TestStructWithException[] Array = {
        new TestStructWithException(1),
        new TestStructWithException(2)
    };
 
    public int Index;
    public NativeArray<int> Result;
 
    public void Execute()
 
        Result[0] = Array[Index].Value;
 
}

Можно было ожидать такой же результат, как и в случае с условиями, но это не так:

        mov     rax, qword ptr [rdi + 8]
        movsxd  rcx, dword ptr [rdi]
        movabs  rdx, offset ".LTestStructWithException[] ManagedArrayOfStructsWithExceptionJob::Array"
        mov     ecx, dword ptr [rdx + 4*rcx]
        mov     dword ptr [rax], ecx

И секция данных

".LTestStructWithException[] ManagedArrayOfStructsWithExceptionJob::Array":
    .long    1
    .long    2
    .size    ".LTestStructWithException[] ManagedArrayOfStructsWithExceptionJob::Array", 8
    .type    .Lburst_abort.function.string,@object
    .section .rodata,"a",@progbits

Все следы исключений были удалены при компиляции. Как будто мы вообще не писали строку со throw.

Что же произойдет, если мы попытаемся запустить это?

class TestScript : MonoBehaviour
{
    void Start()
 
        NativeArray<int> array = new NativeArray<int>(1, Allocator.TempJob);
        new ManagedArrayOfStructsWithExceptionJob { Result = array, Index = 0 }.Run();
        print(array[0]);
        array.Dispose();
 
}

В консоли редактора мы увидим брошенное исключение:

Exception: boom
TestStructWithException..ctor (System.Int32 value) (at Assets/TestScript.cs:115)
ManagedArrayOfStructsWithExceptionJob..cctor () (at Assets/TestScript.cs:122)
Rethrow as TypeInitializationException: The type initializer for 'ManagedArrayOfStructsWithExceptionJob' threw an exception.
Unity.Jobs.IJobExtensions+JobStruct`1[T].Execute (T& data, System.IntPtr additionalPtr, System.IntPtr bufferRangePatchData, Unity.Jobs.LowLevel.Unsafe.JobRanges& ranges, System.Int32 jobIndex) (at /Users/builduser/buildslave/unity/build/Runtime/Jobs/Managed/IJob.cs:30)
Unity.Jobs.LowLevel.Unsafe.JobsUtility:Schedule_Injected(JobScheduleParameters&, JobHandle&)
Unity.Jobs.LowLevel.Unsafe.JobsUtility:Schedule(JobScheduleParameters&)
Unity.Jobs.IJobExtensions:Run(ManagedArrayOfStructsWithExceptionJob) (at /Users/builduser/buildslave/unity/build/Runtime/Jobs/Managed/IJob.cs:43)
TestScript:Start() (at Assets/TestScript.cs:147)

В сборке для macOS мы получим результат:

1

Опять же никаких предупреждений и ошибок компилятора ни во время компиляции, ни при запуске, ни при сборке проекта. Будьте внимательны!

Заключение

Управляемые массивы могут быть использованы совместно с Burst, но с многими ограничениями. Эти ограничения проявляют себя случайным образом. Иногда мы получаем ошибки компиляции когда собираем проект или смотрим в Burst инспектор, но иногда нет. Иногда поведение в редакторе и в билде различны, без каких-либо намеков от Unity о проблемах в коде задачи. Таким образом, важно знать правила использования управляемых массивов в Burst и поддерживать высокий уровень дисциплины при их использовании!