- PVSM.RU - https://www.pvsm.ru -
Инлайнинг — одна из самых важных оптимизаций в компиляторах. Она не только убирает оверхед от вызова, но и открывает много возможностей для других оптимизаций, например, constant folding, dead code elimination и т.д. Более того, иногда инлайнинг приводит к уменьшению размера вызывающей ф-ции! Я опросил несколько человек на предмет знают ли они по каким правилам инлайнятся ф-ции в C# и большинство ответили, что JIT смотрит на размер IL кода и инлайнит только маленькие ф-ции размером, скажем, до 32 байт. Поэтому я решил написать этот пост, чтобы раскрыть детали реализации при помощи вот такого примера, который покажет сразу несколько эвристик в деле:
Как вы думаете, заинлайнится ли вызов конструктора Volume тут? Очевидно, что нет. Он слишком большой, особенно из-за тяжеловесных throw new операторов, которые приводят к довольно жирному кодгену. Давайте проверим в Disasmo:
Заинлайнился! Более того, все выбросы исключений и их ветки успешно удалились! Вы можете сказать что-то в стиле «А, окей, джит очень умен и проделал полный анализ всех кандидатов к инлайну, посмотрел что будет если передать конкретные аргументы» или «Джит пробует заинлайнить всё что можно, выполняет все оптимизации, а потом решает профитно это или нет» (возьмите в руки комбинаторику и посчитайте сложность этой операции, например, для графа вызовов из десятка-двух методов).
Ну… нет, это нереалистично, особенно в терминах just in time. Поэтому, большинство компиляторов используют так называемые наблюдения и эвристики для решения это классической задачи о рюкзаке [1] и пытаются сами определить себе бюджет и в него максимально эффективно вписаться (и нет, PGO не панацея). RyuJIT имеет положительные и отрицательные наблюдения. Положительные увеличивают коэффициент выгоды (benefit multiplier). Чем больше коэффициент — тем больше кода мы можем заинлайнить. Отрицательные наблюдения наоборот — понижают его или вообще могут запретить инлайнинг. Давайте посмотрим какие наблюдения сделал RyuJIT для нашего примера:
Эти наблюдения можно увидеть в логах из COMPlus_JitDump (например, в Disasmo):
Все эти простые наблюдения повысили коэффициент с 1.0 до 11.5 и помогли успешно побороть бюджет инлайнера, например, тот факт, что мы передаем аргумент-константу и она сравнивается с другой константой говорит нам, что с большой долей вероятности после схлопывания констант удалится одна из веток условия и код станет меньше. Или, например, то, что это конструктор и он вызывается внутри цикла — это тоже намек джиту, что он должен смягчить требования к инлайнингу.
Помимо benefit multiplier, RyuJIT так же использует наблюдения для прогноза размера нативного кода ф-ции и ее performance impact используя магические константы в EstimateCodeSize() [2] и EstimatePerformanceImpact() [3] полученные при помощи ML.
Кстати, а вы заметили этот трюк?:
if ((value - 'A') > ('Z' - 'A'))
Это оптимизировання версия к:
if (value < 'A' || value > 'Z')
Оба выражения являются одним и тем же, но в первом случае у нас один базовый блок, а во втором их целых три. Оказывается, в инлайнере есть строгий лимит на кол-во базовых блоков в ф-ции и если оно превышает 5 то не важно какой большой у нас benefit multiplier — инлайнинг отменяется. Поэтому я применил эту уловку, чтобы вписаться в это строгое требование. Было бы классно если бы Roslyn делал это за меня.
Issue в Roslyn: github.com/dotnet/runtime/issues/13347 [4]
PR в RyuJIT (моя неловкая попытка): github.com/dotnet/coreclr/pull/27480 [5]
Там же [6] я описал пример почему это имеет смысл сделать не только в Jit но и в компиляторе C#.
Тут всё понятно, нельзя заинлайнить то, о чем нет информации на этапе компиляции, хотя если тип или метод sealed то почему бы и нет [7].
Если метод никогда не возвращает значение (например, просто делает throw new
...) то такие методы автоматически помечаются как throw-helpers и не инлайнятся. Это такой способ замести сложный кодген от throw new
под ковер и ублажить инлайнер.
В этом случае вы рекомендуете инлайнеру заинлайнить метод, но тут надо быть предельно осторожным по двум причинам:
В данный момент такие методы ничего не инлайнят и сами не инлайнятся: github.com/dotnet/runtime/issues/34500 [10]
Недавно я попытался написать собственную эвристику чтобы помочь вот такому случаю:
В своем прошлом посте [11] я упоминал что совсем недавно я оптимизировал в RyuJIT вычисление длины от константных строк ("Hello".Length -> 5
), так вот, в примере выше ^ мы видим что если заинлайнить Validate
в Test
, то мы получим if ("hello".Length > 10)
что оптимизируется в if (5 > 10)
что оптимизируется в удаление всего условия/ветки. Однако, инлайнер отказался инлайнить Validate
:
И главная проблема тут в том, что пока нет эвристики, которая подскажет джиту, что мы передаем константную строку в System.String::get_Length
, а значит что callvirt-вызов скорее всего свернется в константу и вся ветка удалится. Собственно, моя эвристика [12] и добавляет это наблюдение (единственный минус — приходится резолвить все callvirt'ы что является не очень быстрым).
Существуют и другие ограничения, со списком которых можно в целом ознакомиться вот тут [13]. А тут [14] можно прочитать мысли одного из главных разработчиков JIT о дизайне инлайнера и его статью [15] на тему использования Machine Learning для этого дела.
Автор: Егор
Источник [16]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/c-2/351971
Ссылки в тексте:
[1] задачи о рюкзаке: https://ru.wikipedia.org/wiki/%D0%97%D0%B0%D0%B4%D0%B0%D1%87%D0%B0_%D0%BE_%D1%80%D1%8E%D0%BA%D0%B7%D0%B0%D0%BA%D0%B5
[2] EstimateCodeSize(): https://github.com/dotnet/runtime/blob/a605729eee65344b4c63fb036a35405abcc1de31/src/coreclr/src/jit/inlinepolicy.cpp#L1681-L1737
[3] EstimatePerformanceImpact(): https://github.com/dotnet/runtime/blob/a605729eee65344b4c63fb036a35405abcc1de31/src/coreclr/src/jit/inlinepolicy.cpp#L1749-L1766
[4] github.com/dotnet/runtime/issues/13347: https://github.com/dotnet/runtime/issues/13347
[5] github.com/dotnet/coreclr/pull/27480: https://github.com/dotnet/coreclr/pull/27480
[6] Там же: https://github.com/dotnet/runtime/issues/13347#issuecomment-609019221
[7] почему бы и нет: https://sharplab.io/#v2:EYLgxg9gTgpgtADwGwBYA0AXEBDAzgWwB8ABAJgEYBYAKBrIAIBBGgbxvo/uIGZ6A3AJZQMAV2wAbegIB2GegDEIEABQBKegF4AfPXIBuGgF8adUvQBC9EE1btOPehD4woUAQBMYU2QqVrNOuT6RibUuDASMO5cZgDCVjbUbNScXLxOLm6e3nKKKurauqQG1Ma05ab0ACowuBi4tin2vDJyNXXKjPTYBTrYAHR5anr0APSj9NIQcjLiMlF2HA6t1bUYypbAvfTAg36qI+OT095z0gtNSy0+7evxYNtge/mHE7Pz7gCERkA==
[8] тыц: https://twitter.com/damageboy/status/1238724089403097088
[9] тыц: https://github.com/dotnet/runtime/issues/13423#issuecomment-531854959
[10] github.com/dotnet/runtime/issues/34500: https://github.com/dotnet/runtime/issues/34500
[11] прошлом посте: https://habr.com/ru/post/493586/
[12] моя эвристика: https://github.com/EgorBo/runtime-1/commit/3810c2146f7db9deb9f75f486cd2ccb3cc50a620
[13] тут: https://github.com/dotnet/runtime/blob/master/src/coreclr/src/jit/inline.def
[14] А тут: https://github.com/dotnet/runtime/issues/34286#issuecomment-606186300
[15] его статью: https://github.com/AndyAyersMS/PerformanceExplorer/blob/master/notes/notes-aug-2016.md
[16] Источник: https://habr.com/ru/post/496208/?utm_source=habrahabr&utm_medium=rss&utm_campaign=496208
Нажмите здесь для печати.