ÐÑÑерты в Burst
(Russian translation from English by Maxim Voloshin)
Примечание: Assertions или утверждениÑ, по текÑту зовутÑÑ Ð¿Ñ€Ð¾Ñто аÑÑерты Ñ‚.к. Ðто уÑтоÑвшееÑÑ Ð¸ широко употреблÑемое понÑтие в руÑÑкоÑзычной Ñреде.
ÐÑÑерты невероÑтно нужный инÑтрумент, но работают ли они в задачах Burst компилÑтора? Ð¡ÐµÐ³Ð¾Ð´Ð½Ñ Ð¼Ñ‹ Ñто узнаем!
ÐÑÑерты Unity
Ð”Ð»Ñ Ð½Ð°Ñ‡Ð°Ð»Ð° попробуем иÑпользовать ÑиÑтему аÑÑертов, поÑтавлÑемую Unity и находÑщуюÑÑ Ð² облаÑти видимоÑти UnityEngine.Assertions
:
[BurstCompile] struct UnityAssertJob : IJob { public int A; public void Execute() { Assert.IsTrue(A != 0); } }
Откроем Unity 2019.1.8f1 Ñ ÑƒÑтановленным Burst 1.0.4, Burst инÑпектор показывает на macOS такой код:
ret
ВеÑÑŒ код метода Execute
ÑкомпилировалÑÑ Ð² Ñквивалент оператора return
. Вызов функции Assert.IsTrue
и Ñравнение A != 0
было удалено без каких-либо предупреждений или ошибок. Ðто опаÑно, ведь мы ожидали увидеть рабочие аÑÑерты, что кажетÑÑ Ñ€Ð°Ð·ÑƒÐ¼Ð½Ñ‹Ð¼ предположением.
Ручные ÐÑÑерты
УчитываÑ, что ÐÑÑерты Unity не работают, давайте Ñоберем Ñвою ÑобÑтвенную верÑию Assert.IsTrue
. Ð’ конце концов Ñто довольно тривиально:
[BurstCompile] struct DirectAssertJob : IJob { public int A; public void Execute() { AssertIsTrue(A != 0); } private static void AssertIsTrue(bool truth) { if (!truth) { throw new Exception("Assertion failed"); } } }
Теперь Burst инÑпектор показывает, что Ñгенерированы актуальные инÑтрукции: (закомментировано мной)
cmp dword ptr [rdi], 0 # Сравнение A и 0 je .LBB0_2 # ЕÑли равно, перейти к .LBB0_2 ret # Ð’ противном Ñлучае, выйти .LBB0_2: 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
Как мы видели ранее, Ñто инÑтрукции, Ñгенерированные чтобы броÑить иÑключение. Я Ñпециально опуÑтил Ñекцию данных, ÐºÐ¾Ñ‚Ð¾Ñ€Ð°Ñ Ð²ÐºÐ»ÑŽÑ‡Ð°ÐµÑ‚ Ñообщение об ошибке и ID Ñ‚.к. Ñто не важно в данном Ñлучае.
Внешние ÐÑÑерты
ДобавлÑÑ‚ÑŒ код аÑÑертов каждый раз очень утомительно, давайте попробуем перемеÑтить его в ÑтатичеÑкий клаÑÑ, который может быть переиÑпользован в любой задаче:
static class OutsideAssert { public static void IsTrue(bool truth) { if (!truth) { throw new Exception("Assertion failed"); } } } [BurstCompile] struct OutsideAssertJob : IJob { public int A; public void Execute() { OutsideAssert.IsTrue(A != 0); } }
ПоÑмотрим в Burst инÑпектор, и мы увидим точно такой же аÑÑемблерный код:
cmp dword ptr [rdi], 0 # Сравнение A и 0 je .LBB0_2 # ЕÑли равно, перейти к .LBB0_2 ret # Ð’ противном Ñлучае, выйти .LBB0_2: movabs rax, offset .Lburst_abort_Ptr # Kод броÑÐ°Ð½Ð¸Ñ Ð¸ÑключениÑ... mov rax, qword ptr [rax] movabs rdi, offset .Lburst_abort.error.id movabs rsi, offset .Lburst_abort.error.message jmp rax
УÑÐ»Ð¾Ð²Ð¸Ñ Ð¸ #if
Раз уж у Ð½Ð°Ñ Ð²Ñе прекраÑно получаетÑÑ, попробуем Ñделать Ñледующий шаг к Ñозданию хорошей функции аÑÑерта. ОпределÑющий аÑпект в реализации функции аÑÑерта – тот факт, что она выполнÑетÑÑ Ñ‚Ð¾Ð»ÑŒÐºÐ¾ Ð´Ð»Ñ Ð½ÐµÑкольких типов Ñборок. Unity предоÑтавлÑет директиву препроцеÑÑора UNITY_ASSERTIONS
, Ð´Ð»Ñ Ñ‚Ð¾Ð³Ð¾ чтобы Ñообщить, когда должны вызыватьÑÑ Ð°ÑÑерты. Изначально, Ñто означает, что они выполнÑÑŽÑ‚ÑÑ Ð² редакторе, но не в релизных Ñборках, но Ñто может быть переопределено. Теперь добавим клаÑÑичеÑкое комбо из [Conditional]
и #if
Ð´Ð»Ñ Ñ‚Ð¾Ð³Ð¾, чтобы убрать Ñамо тело функции и вÑе ее вызовы:
static class OutsideAssertConditionalAndIf { [Conditional("UNITY_ASSERTIONS")] public static void IsTrue(bool truth) { #if UNITY_ASSERTIONS if (!truth) { throw new Exception("Assertion failed"); } #endif } } [BurstCompile] struct OutsideAssertConditionalAndIfJob : IJob { public int A; public void Execute() { OutsideAssertConditionalAndIf.IsTrue(A != 0); } }
Открыв Burst инÑпектор, мы увидим, что вернулиÑÑŒ к тому, Ñ Ñ‡ÐµÐ³Ð¾ начинали Ñо вÑтроенными аÑÑертами Unity:
ret
УчитываÑ, что мы получили Ñто в редакторе, а не в релизной Ñборке, нормально было бы ожидать увидеть код функции аÑÑертов.
Только уÑловие
Попробуем Ñто вылечить. Ð”Ð»Ñ Ð½Ð°Ñ‡Ð°Ð»Ð° уберем #if
, таким образом, тело функции оÑтанетÑÑ, но ее вызовы будут удалены. Вызовы, на Ñамом деле, ÑÐ°Ð¼Ð°Ñ Ð²Ð°Ð¶Ð½Ð°Ñ Ñ‡Ð°ÑÑ‚ÑŒ, поÑтому мы можем пойти на небольшой компромиÑÑ:
static class OutsideAssertConditional { [Conditional("UNITY_ASSERTIONS")] public static void IsTrue(bool truth) { if (!truth) { throw new Exception("Assertion failed"); } } } [BurstCompile] struct OutsideAssertConditionalJob : IJob { public int A; public void Execute() { OutsideAssertConditional.IsTrue(A != 0); } }
И Ñнова, Burst инÑпектор показывает, что код функции аÑÑертов был удален:
ret
Only #if
Ðа Ñтот раз, зайдем Ñ Ð´Ñ€ÑƒÐ³Ð¾Ð¹ Ñтороны и удалим [Conditional]
вмеÑто #if
. Ðто оÑтавит вызовы функции, но они будут пуÑтыми. К ÑчаÑтью, Burst удалит вÑе вызовы Ñ‚.к. поймет, что они беÑÑмыÑленны:
static class OutsideAssertIf { public static void IsTrue(bool truth) { #if UNITY_ASSERTIONS if (!truth) { throw new Exception("Assertion failed"); } #endif } } [BurstCompile] struct OutsideAssertIfJob : IJob { public int A; public void Execute() { OutsideAssertIf.IsTrue(A != 0); } }
Теперь инÑтрукции Ñнова видны в Burst инÑпекторе!
cmp dword ptr [rdi], 0 # Сравнение A и 0 je .LBB0_2 # ЕÑли равно, перейти к .LBB0_2 ret # Ð’ противном Ñлучае, выйти .LBB0_2: movabs rax, offset .Lburst_abort_Ptr # Kод броÑÐ°Ð½Ð¸Ñ Ð¸ÑключениÑ... mov rax, qword ptr [rax] movabs rdi, offset .Lburst_abort.error.id movabs rsi, offset .Lburst_abort.error.message jmp rax
Подтверждение работоÑпоÑобноÑти
ОÑталоÑÑŒ убедитьÑÑ, что у Ð½Ð°Ñ ÐµÑÑ‚ÑŒ дейÑтвительно рабочий аÑÑерт. Ð”Ð»Ñ Ñтого напишем крошечный Ñкрипт, который запуÑкает задачу. Мы оÑтавим A
в его изначальном значении 0
, которое вызовет аÑÑерт.
class TestScript : MonoBehaviour { void Start() { new OutsideAssertIfJob().Run(); } }
ЗапуÑтив его в редакторе, мы получим Ñледующее иÑключение:
Exception: Assertion failed OutsideAssertIf.IsTrue (System.Boolean truth) (at Assets/TestScript.cs:116) OutsideAssertIfJob.Execute () (at Assets/TestScript.cs:129) 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(OutsideAssertIfJob) (at /Users/builduser/buildslave/unity/build/Runtime/Jobs/Managed/IJob.cs:43) TestScript:Start() (at Assets/TestScript.cs:137)
И вот, что мы увидим в файле ~/Library/Logs/Unity/Player.log
, еÑли запуÑтим релизную Ñборку Ð´Ð»Ñ macOS :
Хорошо, что мы получили пуÑтоту, поÑкольку Ñто означает, что аÑÑерт был удален как и должно проиÑходить в релизной Ñборке.
Ð”Ð»Ñ Ñ‚Ð¾Ð³Ð¾, чтобы убедитьÑÑ, что Ñ„ÑƒÐ½ÐºÑ†Ð¸Ñ Ð°ÑÑерта работает в Ñборках Ñ ÑƒÑ‡Ð°Ñтием Burst компилÑтора, например, когда иÑпользуетÑÑ BuildOptions.ForceEnableAssertions
, закомментируем #if
:
static class OutsideAssertIf { public static void IsTrue(bool truth) { //#if UNITY_ASSERTIONS if (!truth) { throw new Exception("Assertion failed"); } //#endif } }
Теперь запуÑтим новый билд Ð´Ð»Ñ macOS и поÑмотрим Player.log:
System.Exception: Assertion failed 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)
Теперь мы доказали, что Ñ„ÑƒÐ½ÐºÑ†Ð¸Ñ Ð°ÑÑерта работает и в редакторе, и в билде Ð´Ð»Ñ macOS Ñ Ð¸Ñпользованием Burst компилÑтора, не завиÑимо от того включена их поддержка или выключена. Мы также подтвердили что #if
не включает в Ñборку код, когда объÑвлен UNITY_ASSERTIONS
.
Проверка производительноÑти
ПоÑледнее подтверждение, которое мы должны получить Ñто то, что Burst удалит вызов функции IsTrue
в Ñлучае, еÑли #if
оÑтавит ее тело пуÑтым. Чтобы проверить Ñто, удалим его вручную:
static class OutsideAssertIf { public static void IsTrue(bool truth) { } }
Теперь Burst инÑпектор показывает только return
:
ret
Вызов пуÑтой функции не был Ñгенерирован, поÑтому не будет дополнительных накладных раÑходов, когда UNITY_ASSERTIONS
не определен.
Заключение
К Ñожалению, аÑÑерты Unity не работают ÑовмеÑтно Ñ Burst. И отвратительно, что об Ñтом нет никакой информации в документации и даже компилÑтор не предупреждает, что они не будут работать. Burst проÑто удалÑет их, не обеÑÐ¿ÐµÑ‡Ð¸Ð²Ð°Ñ Ð²Ð¾Ð¾Ð±Ñ‰Ðµ никаких проверок на ошибки. ВероÑтно, Ñто ÑвÑзано Ñ Ñ‚ÐµÐ¼, что они иÑпользовали [Conditional]
который работал аналогично в наших ÑобÑтвенных теÑтах.
К ÑчаÑтью, мы можем довольно проÑто реализовать Ñвои ÑобÑтвенные функции аÑÑертов. Такие функции работают и в редакторе, и в Burst. Они полноÑтью удалÑÑŽÑ‚ÑÑ ÐºÐ¾Ð³Ð´Ð° аÑÑерты отключены, и не влиÑÑŽÑ‚ на производительноÑÑ‚ÑŒ в релизных Ñборках.