Это последняя статья из цикла: "CoreCLR - это круто", где я стараюсь рассказать про основные, важные изменения, которые CoreCLR может превнести.
Предыдущие статьи: раз, два.
На этот раз поговорим про различия в runtime'ах, которые непосредственно исполняются на пользовательских девайсах (а не в редакторе, как в прошлых статьях).
🔸VTable
Когда вызываешь виртуальный метод, runtime должен понять какой именно код исполнить.
У базового класса и наследника методы разные, но вызов 📞 выглядит одинаково.
VTable — это массив указателей на методы. Каждый тип хранит свою таблицу.
Вызов виртуального метода = взять указатель из таблицы по номеру слота и сделать jmp на значение указателя в слоте.
🔸Где IL2CPP добавляет работы
В CoreCLR vtable — просто массив указателей на код. 8 байт на слот.
В IL2CPP каждый слот содержит ДВА указателя: на код и на метаданные. 16 байт.
Зачем? IL2CPP — это AOT. Нет JIT, который достроил бы информацию в runtime. Всё предсгенерировано заранее.
Но главное размер. CoreCLR разделяет информацию о типе на "горячую" (MethodTable — только для исполнения) и "холодную" (EEClass: reflection, interop).
При виртуальном вызове CPU загружает в кэш только MethodTable.
IL2CPP держит всё в одной Il2CppClass — 200+ байт.
При частых вызовах методов разных типов процессор постоянно выгружает одни данные из кэша и загружает другие.
Отсюда и сложно писать производительный код без Burst: ты теряешь контроль над тем, что по факту у тебя исполняется на устройстве 😬
🔸Interface Dispatch — тут разница огромная
Интерфейсы сложнее классов. Один тип может реализовать много интерфейсов, и runtime должен найти нужную реализацию по указателю на интерфейс.
IL2CPP хранит массив interfaceOffsets — пары "указатель на интерфейс + смещение в vtable".
При каждом вызове метода интерфейса il2cpp идёт циклом по этому массиву, сравнивая указатели:
if (interfaceOffsets[i].interfaceType == declaringInterface)
Линейный поиск короче.
Можешь посмотреть сам, файл:
ClassInlines.h в исходниках il2cpp.Т.е. если тип реализует 5 интерфейсов — в среднем 2-3 сравнения указателей. 20 интерфейсов — ~10 сравнений.
С одной стороны копеечки, а с другой часто от других разрабов слышу что это всегда jmp на указатель с кодом 🤔
CoreCLR же использует Virtual Stub Dispatch.
При первом вызове генерируется stub — кусок машинного кода, который запоминает результат поиска.
Все последующие вызовы — один jmp без цикла. O(1).
Т.е. IL2CPP платит за каждый interface-вызов, CoreCLR — только за первый.
🔸GC
IL2CPP сидит на Boehm GC — консервативный сборщик без поколений и без уплотнения памяти. Объекты никогда не перемещаются. При долгих сессиях память фрагментируется, для аллокации идет поиск "дырок" в free list.
И тут можно долго говорить о различиях, но самое крутое — это наличие Background GC в CoreCLR.
Если эту штуку смогут хотябы частично адаптировать под unity будет пушка бомба.
Меньше заботы о spike'ах при работе GC это всегда приятно 😊
И вот тут прикольно:
Большая часть "рекомендаций" с оптимизациями и уменьшению аллокаций с переходом CoreCLR могут кардинально измениться и начнется снова рассвет полезных постов с лайфхаками в соц. сетях 😬
🔻 Для тех кто задавался вопросом: "Чего так все с этим CoreCLR бегают?", мой короткий ответ:
Та прост эта штука может оч сильно изменить подход к тому как мы привыкли писать код под unity.
Ну и просто если у unity получится, unity ведь один из самых часто-используемых движков и успех может значительно укрепить доверие компаний и разработчиков.
Но, мне кажется, у них значительно лучше получается делать как раз наоборот 🤣
Ставь 😁 если задушил жестко и надо проще или 👍 если все норм и тебе заходит такой контент!
#unite2025@UniArchitect
#проект_в_разработке@UniArchitect
