- PVSM.RU - https://www.pvsm.ru -
Я большой поклонник всего, что делает Фабьен Санглард [1], мне нравится его блог, и я прочитал обе [2] его книги [3] от корки до корки (о них рассказывали в недавнем подкасте Hansleminutes [4]).
Недавно Фабьен написал отличный пост, где расшифровал крошечный рейтрейсер [5], деобфусцировав код и фантастически красиво объяснив математику. Я действительно рекомендую найти время, чтобы прочитать это!
Но это заставило меня задуматься, можно ли перенести этот код C++ на C#? Поскольку на основной работе [6] мне в последнее время приходится довольно много писать на C++, я подумал, что могу попробовать.
Но что более важно, я хотел получить лучшее представление о том, является ли C# языком низкого уровня?
Немного другой, но связанный с этим вопрос: насколько C# подходит для «системного программирования»? На эту тему я действительно рекомендую отличный пост Джо Даффи от 2013 года [7].
Я начал с простого переноса деобфусцированного кода C++ [8] строчка за строчкой на C#. Это было довольно просто: похоже, всё-таки правду говорят, что C# — это C++++!!!
В примере показана основная структура данных — 'vector', вот сравнение, C++ слева, C# справа:
Итак, есть несколько синтаксических различий, но поскольку .NET позволяет определять собственные типы значений [10], я смог получить ту же функциональность. Это важно, потому что обработка 'vector' как структуры означает, что мы можем получить лучшую «локальность данных», и не нужно вовлекать сборщик мусора .NET, поскольку данные будут поступать в стек (да, я знаю, что это деталь реализации).
Дополнительно о structs
или «типах значений» в .NET см. здесь:
В частности, в последнем посте Эрика Липперта мы находим такую полезную цитату, которая даёт понять, что такое на самом деле «типы значений»:
Конечно, наиболее важным фактом о типах значений являются не детали реализации, как они выделяются, а скорее исконное семантическое значение «типа значения», а именно то, что он всегда копируется «по значению». Если бы важной была информация о выделении, мы бы назвали их «типами кучи» и «типами стека». Но в большинстве случаев это неважно. Большую часть времени актуальной является семантика копирования и идентификации.
Теперь посмотрим, как выглядят некоторые другие методы в сравнении (снова C++ слева, C# справа), сначала RayTracing(..)
:
Затем QueryDatabase (..)
:
(см. пост Фабиана [18] с объяснением, что делают эти две функции)
Но опять же дело в том, что C# позволяет очень легко писать код C++! В этом случае нам больше всего помогает ключевое слово ref
, которое позволяет передавать значение по ссылке [19]. Мы довольно давно использовали ref
в вызовах методов, но в последнее время предпринимаются усилия, чтобы разрешить ref
в других местах:
Теперь иногда использование ref
повысит производительность, потому что тогда структуру не нужно копировать, см. бенчмарки в посте Адама Стиникса [22] и «Ловушки производительности ref locals и ref returns в C#» [23] для дополнительной информации.
Но самое важное то, что такой сценарий обеспечивает нашему порту C# то же поведение, что у исходного кода C++. Хотя хочу отметить, что так называемые «управляемые ссылки» не совсем такие же, как «указатели», в частности, вы не сможете на них выполнять арифметику, подробнее об этом см. здесь:
Таким образом, код хорошо портировался, но производительность тоже имеет значение. Особенно в рейтрейсере, который может обсчитывать кадр несколько минут. Код C++ содержит переменную sampleCount
, которая управляет конечным качеством изображения, при этом sampleCount = 2
выглядит следующим образом:
Явно не очень реалистично!
Но когда доберётесь до sampleCount = 2048
, всё выглядит гораздо лучше:
Но запуск с sampleCount = 2048
отнимает очень много времени, поэтому все остальные прогоны выполняем со значением 2
, чтобы уложиться хотя бы в минуту. Изменение sampleCount
влияет только на количество итераций самого внешнего цикла кода, см. этот gist [27] для объяснения.
Чтобы содержательно сравнить C++ и C#, я использовал инструмент time-windows [28], это порт юниксовой команды time
. Первоначальные результаты выглядели так:
C++ (VS 2017) | .NET Framework (4.7.2) | .NET Core (2.2) | |
---|---|---|---|
Время (сек) | 47,40 | 80,14 | 78,02 |
В ядре (сек) | 0,14 (0,3%) | 0,72 (0,9%) | 0,63 (0,8%) |
В user-space (сек) | 43,86 (92,5%) | 73,06 (91,2%) | 70,66 (90,6%) |
Количество ошибок page fault | 1143 | 4818 | 5945 |
Рабочий набор (КБ) | 4232 | 13 624 | 17 052 |
Вытесняемая память (КБ) | 95 | 172 | 154 |
Невытесняемая память | 7 | 14 | 16 |
Файл подкачки (КБ) | 1460 | 10 936 | 11 024 |
Изначально мы видим, что код C# немного медленнее, чем версия C++, но он становится лучше (см. ниже).
Но давайте сначала посмотрим, что нам делает .NET JIT даже с этим «наивным» построчным портом. Во-первых, он делает хорошую работу во встраивании меньших «хелпер-методов». Это видно на выдаче великолепного инструмента Inlining Analyzer [29] (зелёный = встроенный):
Однако он встраивает не все методы, например, из-за сложности пропускается QueryDatabase(..)
:
Другая функция компилятора .NET Just-In-Time (JIT) — преобразование определённых вызовов методов в соответствующие инструкции CPU. Мы можем видеть это в действии с функцией оболочки sqrt
, вот исходный код C# (обратите внимание на вызов Math.Sqrt
):
// intnv square root
public static Vec operator !(Vec q) {
return q * (1.0f / (float)Math.Sqrt(q % q));
}
И вот ассемблерный код, который генерирует .NET JIT: здесь нет вызова к Math.Sqrt
и используется процессорная инструкция vsqrtsd [32]:
; Assembly listing for method Program:sqrtf(float):float
; Emitting BLENDED_CODE for X64 CPU with AVX - Windows
; Tier-1 compilation
; optimized code
; rsp based frame
; partially interruptible
; Final local variable assignments
;
; V00 arg0 [V00,T00] ( 3, 3 ) float -> mm0
;# V01 OutArgs [V01 ] ( 1, 1 ) lclBlk ( 0) [rsp+0x00] "OutgoingArgSpace"
;
; Lcl frame size = 0
G_M8216_IG01:
vzeroupper
G_M8216_IG02:
vcvtss2sd xmm0, xmm0
vsqrtsd xmm0, xmm0
vcvtsd2ss xmm0, xmm0
G_M8216_IG03:
ret
; Total bytes of code 16, prolog size 3 for method Program:sqrtf(float):float
; ============================================================
(чтобы получить такую выдачу, следуйте этим инструкциям [33], используйте надстройку «Disasmo» VS2019 [34] или посмотрите на SharpLab.io [35])
Эти замены тоже известны как «встроенные» (intrinsics [36]), и в коде ниже мы можем видеть, как JIT генерирует их. Этот фрагмент показывает сопоставление только для AMD64
, но JIT также нацелен на X86
, ARM
и ARM64
, полный метод здесь [37].
bool Compiler::IsTargetIntrinsic(CorInfoIntrinsics intrinsicId)
{
#if defined(_TARGET_AMD64_) || (defined(_TARGET_X86_) && !defined(LEGACY_BACKEND))
switch (intrinsicId)
{
// AMD64/x86 has SSE2 instructions to directly compute sqrt/abs and SSE4.1
// instructions to directly compute round/ceiling/floor.
//
// TODO: Because the x86 backend only targets SSE for floating-point code,
// it does not treat Sine, Cosine, or Round as intrinsics (JIT32
// implemented those intrinsics as x87 instructions). If this poses
// a CQ problem, it may be necessary to change the implementation of
// the helper calls to decrease call overhead or switch back to the
// x87 instructions. This is tracked by #7097.
case CORINFO_INTRINSIC_Sqrt:
case CORINFO_INTRINSIC_Abs:
return true;
case CORINFO_INTRINSIC_Round:
case CORINFO_INTRINSIC_Ceiling:
case CORINFO_INTRINSIC_Floor:
return compSupports(InstructionSet_SSE41);
default:
return false;
}
...
}
Как видим, некоторые методы реализованы так, например, Sqrt
и Abs
, а для других используется функции среды выполнения C++, например, powf [38].
Весь этот процесс очень хорошо объясняется в статье «Как Math.Pow() реализован в .NET Framework?» [39], его можно увидеть также в исходниках CoreCLR:
COMSingle::Pow [40]
, то есть того метода, который выполняется, если вызвать MathF.Pow(..)
из кода C#
Интересно, можно ли с ходу улучшить наивный построчный порт. После некоторого профилирования я сделал два основных изменения:
Math.XXX(..)
на аналоги MathF.ХХХ()
Более подробно эти изменения объясняются ниже.
Для получения дополнительной информации о том, почему это необходимо, см. этот отличный ответ на Stack Overflow [43] от Андрея Акиншина [44], вместе с бенчмарками и ассемблерным кодом. Он приходит к следующему выводу:
Вывод
- Кэширует ли .NET жёстко закодированные локальные массивы? Вроде тех, что помещает в метаданные компилятор Roslyn.
- В этом случае будут накладные расходы? К сожалению, да: для каждого вызова JIT будет копировать содержимое массива из метаданных, что занимает дополнительное время по сравнению со статическим массивом. Среда выполнения также выделяет объекты и создаёт трафик в памяти.
- Стоит ли об этом беспокоиться? Возможно. Если это горячий метод и вы хотите достичь хорошего уровня производительности, нужно использовать статический массив. Если это холодный метод, который не влияет на производительность приложения, вероятно, нужно написать «хороший» исходный код и поместить массив в область метода.
Внесённые изменения можете увидеть в этом diff [45].
Во-вторых, и это самое главное, я значительно улучшил производительность, сделав следующие изменения:
#if NETSTANDARD2_1 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NETCOREAPP3_0
// intnv square root
public static Vec operator !(Vec q) {
return q * (1.0f / MathF.Sqrt(q % q));
}
#else
public static Vec operator !(Vec q) {
return q * (1.0f / (float)Math.Sqrt(q % q));
}
#endif
Начиная с .NET Standard 2.1 существуют конкретные реализации float
общих математических функций. Они расположены в классе System.MathF [46]. Дополнительно об этом API и его реализации см. здесь:
После этих изменений разница в производительности кода на C# и C++ сократилась примерно до 10%:
C++ (VS C++ 2017) | .NET Framework (4.7.2) | .NET Core (2.2) TC OFF | .NET Core (2.2) TC ON | |
---|---|---|---|---|
Время (сек) | 41,38 | 58,89 | 46,04 | 44,33 |
В ядре (сек) | 0,05 (0,1%) | 0,06 (0,1%) | 0,14 (0,3%) | 0,13 (0.3%) |
В user-space (сек) | 41,19 (99,5%) | 58,34 (99,1%) | 44,72 (97,1%) | 44,03 (99,3%) |
Количество ошибок page fault | 1119 | 4749 | 5776 | 5661 |
Рабочий набор (КБ) | 4136 | 13 440 | 16 788 | 16 652 |
Вытесняемая память (КБ) | 89 | 172 | 150 | 150 |
Невытесняемая память | 7 | 13 | 16 | 16 |
Файл подкачки (КБ) | 1428 | 10 904 | 10 960 | 11 044 |
TC — многоуровневая компиляция, Tiered Compilation [53] (полагаю, её включат по умолчанию в .NET Core 3.0)
Для полноты, вот результаты нескольких прогонов:
Прогон | C++ (VS C++ 2017) | .NET Framework (4.7.2) | .NET Core (2.2) TC OFF | .NET Core (2.2) TC ON |
---|---|---|---|---|
TestRun-01 | 41,38 | 58,89 | 46,04 | 44,33 |
TestRun-02 | 41,19 | 57,65 | 46,23 | 45,96 |
TestRun-03 | 42,17 | 62,64 | 46,22 | 48,73 |
Примечание: разница между .NET Core и .NET Framework обусловлена отсутствием MathF API в .NET Framework 4.7.2, дополнительные сведения см. в тикете о поддержке .Net Framework (4.8?) для netstandard 2.1 [54].
Уверен, что код можно ещё улучшить!
Если вы заинтересованы в том, чтобы устранить разницу в производительности, вот код C# [55]. Для сравнения можете смотреть ассемблерный код C++ от великолепного сервиса Compiler Explorer [56].
Наконец, если это поможет, вот выдача профилировщика Visual Studio с отображением «горячего пути» (после улучшений производительности, описанных выше):
Или более конкретно:
Какие языковые особенности C#/F#/VB.NET или функциональность BCL/Runtime означают «низкоуровневое»* программирование?
* да, я понимаю, что «низкий уровень» — это субъективный термин.
Примечание: у каждого разработчика C# своё представление о том, что такое «низкий уровень», эти функции будут приняты как должное программистами C++ или Rust.
Вот список, который я составил:
ref struct
), который обёртывает все шаблоны доступа к памяти, это тип для универсального непрерывного доступа к памяти. Можно представить реализацию Span с фиктивной ссылкой и длиной, которая принимает все три типа доступа к памяти».
System.Runtime.InteropServices
, совместимость C++ и совместимость COM (COM-взаимодействие)».Я также кинул клич в твиттере и получил гораздо больше вариантов для включения в список:
MemoryMarshal
и Unsafe
, и может несколько других вещей в пространствах имён System.Runtime.CompilerServices
»
__makeref
и остальное»
ldftn
и вызывать их через calli
. В VS2017 есть шаблон proj, который делает это тривиальным с помощью перезаписи методов extern + MethodImplOptions.ForwardRef + ilasm.ехе»
inline
, которое выполняет работу на уровне IL до JIT, поэтому оно считалось важным на уровне языка. C# этого не хватает (до сих пор) для лямбд, которые всегда являются виртуальными вызовами, а обходные пути часто странные (ограниченные дженерики)»
Так что в итоге я бы сказал, что C#, безусловно, позволяет писать код, который выглядит как C++, и в сочетании с библиотеками времени выполнения и базового класса предоставляет много низкоуровневых функций.
Компилятор Unity Burst:
Автор: m1rko
Источник [93]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/c-2/311729
Ссылки в тексте:
[1] Фабьен Санглард: http://fabiensanglard.net/
[2] обе: http://fabiensanglard.net/gebbdoom/index.html
[3] книги: http://fabiensanglard.net/gebbwolf3d/index.html
[4] подкасте Hansleminutes: https://hanselminutes.com/666/episode-666-game-engine-black-book-doom-with-fabien-sanglard
[5] расшифровал крошечный рейтрейсер: http://fabiensanglard.net/postcard_pathtracer/index.html
[6] основной работе: https://raygun.com/platform/apm
[7] отличный пост Джо Даффи от 2013 года: http://joeduffyblog.com/2013/12/27/csharp-for-systems-programming/
[8] деобфусцированного кода C++: http://fabiensanglard.net/postcard_pathtracer/formatted_full.html
[9] Image: https://mattwarren.org/images/2019/03/Diff%20-%20C++%20v.%20C%23%20-%20struct%20Vec.png
[10] собственные типы значений: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/value-types
[11] Куча против стека, тип значения против ссылочного типа: http://tooslowexception.com/heap-vs-stack-value-type-vs-reference-type/
[12] Типы значений против ссылочных типов: https://adamsitnik.com/Value-Types-vs-Reference-Types/
[13] Память в .NET: что куда: http://jonskeet.uk/csharp/memory.html
[14] Правда о типах значений: https://blogs.msdn.microsoft.com/ericlippert/2010/09/30/the-truth-about-value-types/
[15] Стек — это деталь реализации, часть первая: https://blogs.msdn.microsoft.com/ericlippert/2009/04/27/the-stack-is-an-implementation-detail-part-one/
[16] Image: https://mattwarren.org/images/2019/03/Diff%20-%20C++%20v.%20C%23%20-%20RayMatching.png
[17] Image: https://mattwarren.org/images/2019/03/Diff%20-%20C++%20v.%20C%23%20-%20QueryDatabase%20(partial).png
[18] пост Фабиана: http://fabiensanglard.net/postcard_pathtracer/
[19] значение по ссылке: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/ref
[20] Ref return и ref local: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/ref-returns
[21] Серия C# 7, часть 9: ref struct: https://blogs.msdn.microsoft.com/mazhou/2018/03/02/c-7-series-part-9-ref-structs/
[22] посте Адама Стиникса: https://adamsitnik.com/ref-returns-and-ref-locals/#passing-arguments-to-methods-by-reference
[23] «Ловушки производительности ref locals и ref returns в C#»: https://blogs.msdn.microsoft.com/seteplia/2018/04/11/performance-traps-of-ref-locals-and-ref-returns-in-c/
[24] ref return — это не указатель: http://mustoverride.com/refs-not-ptrs/
[25] Управляемые указатели: http://mustoverride.com/managed-refs-CLR/
[26] Ссылки — это не адреса: https://blogs.msdn.microsoft.com/ericlippert/2009/02/17/references-are-not-addresses/
[27] этот gist: https://gist.github.com/mattwarren/1580572d9d641147c61caf65c383c3a4
[28] time-windows: https://code.google.com/archive/p/time-windows/source/default/source
[29] Inlining Analyzer: https://marketplace.visualstudio.com/items?itemName=StephanZehetner.InliningAnalyzer
[30] Image: https://mattwarren.org/images/2019/03/Inlining%20Analyzer%20-%20QueryDatabase.png
[31] Image: https://mattwarren.org/images/2019/03/Inlining%20Analyzer%20-%20RayMarching%20-%20with%20ToolTip.png
[32] vsqrtsd: https://software.intel.com/sites/landingpage/IntrinsicsGuide/#text=vsqrtsd&expand=5236
[33] этим инструкциям: https://github.com/dotnet/coreclr/blob/master/Documentation/building/viewing-jit-dumps.md#useful-complus-variables
[34] надстройку «Disasmo» VS2019: https://github.com/EgorBo/Disasmo
[35] SharpLab.io: https://sharplab.io/#v2:EYLgHgbALANALiAhgZwLYB8ACAGABJgRgG4BYAKEwGZ8AmXAYVwG9zc39rMpcBZACgCUzVu1EA3RACdcYXAF5eiOAAsAdAGUAjpLh8C2AaTKjRhAJx8whkWwC+5W0A==
[36] intrinsics: https://en.wikipedia.org/wiki/Intrinsic_function
[37] здесь: https://github.com/dotnet/coreclr/blob/release/2.2/src/jit/importer.cpp#L19144-L19217
[38] powf: https://en.cppreference.com/w/c/numeric/math/pow
[39] «Как Math.Pow() реализован в .NET Framework?»: https://stackoverflow.com/a/8870593
[40] COMSingle::Pow: https://github.com/dotnet/coreclr/blob/release/2.2/src/classlibnative/float/floatsingle.cpp#L205-L212
[41] реализации метода рантайма C: https://github.com/dotnet/coreclr/blob/release/2.2/src/pal/inc/pal.h#L4094-L4198
[42] реализации powf: https://github.com/dotnet/coreclr/blob/release/2.2/src/pal/src/cruntime/math.cpp#L755-L840
[43] этот отличный ответ на Stack Overflow: https://stackoverflow.com/a/39106675
[44] Андрея Акиншина: https://twitter.com/andrey_akinshin
[45] этом diff: https://gist.github.com/mattwarren/d17a0c356bd6fdb9f596bee6b9a5e63c/revisions#diff-ab5447b35812d457232030d7d2577458R114
[46] System.MathF: https://apisof.net/catalog/System.MathF
[47] Новый API для математики с одинарной точностью: https://github.com/dotnet/corefx/issues/1151
[48] Добавление математических функций с одинарной точностью: https://github.com/dotnet/coreclr/pull/5492/files
[49] Обеспечение набора модульных тестов для новых математических API с одинарной точностью: https://github.com/dotnet/coreclr/issues/7690
[50] System.Math и System.MathF должны быть реализованы в управляемом коде, а не как FCALL для среды выполнения C: https://github.com/dotnet/coreclr/issues/14155
[51] Перемещение Math.Abs(double) и Math.Abs(float) для реализации в управляемом коде: https://github.com/dotnet/coreclr/pull/14156
[52] Конструкция и процесс добавления платформозависимых встроенных средств в .NET: https://github.com/dotnet/designs/issues/13
[53] Tiered Compilation: https://devblogs.microsoft.com/dotnet/tiered-compilation-preview-in-net-core-2-1/
[54] тикете о поддержке .Net Framework (4.8?) для netstandard 2.1: https://github.com/dotnet/standard/issues/859
[55] вот код C#: https://gist.github.com/mattwarren/d17a0c356bd6fdb9f596bee6b9a5e63c
[56] Compiler Explorer: https://godbolt.org/z/l2QZLY
[57] Image: https://mattwarren.org/images/2019/03/Call%20Tree%20(tidied%20up)%20-%20Report20190221-2029-After-MathF-Changes-NetCore.png
[58] ref returns и ref locals: https://adamsitnik.com/ref-returns-and-ref-locals/
[59] Небезопасный код в .NET: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/unsafe-code
[60] Управляемые указатели в .NET: http://tooslowexception.com/managed-pointers-in-net/
[61] Серия C# 7, часть 10: Span<T> и управление универсальной памятью: https://blogs.msdn.microsoft.com/mazhou/2018/03/25/c-7-series-part-10-spant-and-universal-memory-management/
[62] Совместимость («Руководство по программированию на C#»): https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/interop/
[63] Бен Адамс: https://twitter.com/ben_a_adams/status/1097876408775442432
[64] Марк Грэвелл: https://twitter.com/marcgravell/status/1097877192745336837
[65] Марк Грэвелл: https://twitter.com/marcgravell/status/1097878317875761153
[66] Кевин Джонс: https://twitter.com/vcsjones/status/1097877294864056320
[67] Теодорос Чацигианнакис: https://twitter.com/Pessimizations/status/1097877381296066560
[68] damageboy: https://twitter.com/damageboy/status/1097877247120326658
[69] Роберт Хэкен: https://twitter.com/RobertHaken/status/1097880613988851712
[70] Виктор Байбеков: https://twitter.com/buybackoff/status/1097885830364966914
[71] Виктор Байбеков: https://twitter.com/buybackoff/status/1097887318806093824
[72] Бен Адамс: https://twitter.com/ben_a_adams/status/1097885533508980738
[73] Виктор Байбеков: https://twitter.com/buybackoff/status/1097893756672581632
[74] Александре Мютель: https://twitter.com/xoofx/status/1097895771142320128
[75] Александре Мютель: https://twitter.com/xoofx/status/1097896059236466689
[76] OmariO: https://twitter.com/0omari0/status/1097916897952235520
[77] BinaryPrimitives: https://docs.microsoft.com/en-us/dotnet/api/system.buffers.binary.binaryprimitives?view=netcore-3.0
[78] Кодзи (Kozy) Мацуи: https://twitter.com/kozy_kekyo/status/1097982126190878720
[79] Фрэнк A. Крюгер: https://twitter.com/praeclarum/status/1098002275891642368
[80] Конрад Кокоса: https://twitter.com/konradkokosa/status/1098155819340828672
[81] Себастьяно Мандала: https://twitter.com/sebify/status/1098161110476312582
[82] Нино Флорис: https://twitter.com/NinoFloris/status/1098433286899146753
[83] Шаблоны для высокопроизводительного C#. Федерико Андрес Лоис: https://www.youtube.com/watch?v=7GTpwgsmHgU
[84] Performance Quiz #6 — Китайско-английский словарь: https://blogs.msdn.microsoft.com/ricom/2005/05/10/performance-quiz-6-chineseenglish-dictionary-reader/
[85] Performance Quiz #6 — Заключение, изучение пространства: https://blogs.msdn.microsoft.com/ricom/2005/05/20/performance-quiz-6-conclusion-studying-the-space/
[86] Насколько C++ быстрее, чем C#?: https://stackoverflow.com/a/138406
[87] Оптимизация управляемого C# и нативного кода C++: https://blogs.msdn.microsoft.com/jonathanh/2005/05/20/optimizing-managed-c-vs-native-c-code/
[88] Как Unity сделала (подмножество) C# таким же быстрым, как C++: https://blogs.unity3d.com/2019/02/26/on-dots-c-c/
[89] Компилятор Unity Burst: простая оптимизация производительности: http://infalliblecode.com/unity-burst-compiler/
[90] Daily Pathtracer, часть 3: C# и Unity Burst: http://aras-p.info/blog/2018/03/28/Daily-Pathtracer-Part-3-CSharp-Unity-Burst/
[91] C++, C# и Unity: https://lucasmeijer.com/posts/cpp_unity/
[92] Глубокое погружение в компилятор Burst — Unite LA: https://www.youtube.com/watch?v=QkM6zEGFhDY
[93] Источник: https://habr.com/ru/post/443804/?utm_source=habrahabr&utm_medium=rss&utm_campaign=443804
Нажмите здесь для печати.