- Почему считается, что если все «заинлайнить», то производительность не взлетит а упадет?
- Как JIT инлайнит наш C# код (эвристики)
- Инлайнинг и виртуальные методы
- Инлайнинг и выброс исключений
- Инлайнинг и [AggressiveInlining] атрибут
- Инлайнинг и динамические методы
- Моя попытка написать свою эвристику
- Как компилятор определяет что можно заинлайнить?
- Re: Как компилятор определяет что можно заинлайнить?
- Re: Как компилятор определяет что можно заинлайнить?
Почему считается, что если все «заинлайнить», то производительность не взлетит а упадет?
Собственно Сабж. Чем, кроме разбухания исполняемого модуля грозит повсеместное использование inline и развертка вызовово процдур в код?
И насколько «разбухнет» при таком использовании простейшая программа (надо ведь распотронить libc и библиотеки рантайма)?
если ты про си++ и про inline модификатор, то он уже давно игнорируется компилятором, потому что компилятору виднее где нуна заинлайнить а где нет. к тому же, одна и та же функция может быть в одном месте заинлайнена а в другом нет.
Если ты досконально знаешь характеристики железа, на котором будет запускаться код, и во что компилятор развернёт исходник, инлайни на здоровье.
Неконтроллируемый инлайн, например, не учитывает размеры кешей процессоров/ядер, что влияет на частость обновлений порций исполняемого кода.
Это может сильно замедлить выполнение циклов.
Т.е. решения по инлайну принимаются компилятором. С учетом ограничений.
__forceinline рулит, ибо на примере шариков повышал производительность программы минимум на 15% относительно обычных функций, и где-то на 5-10% относительно inline.
Alexander K
Пишем рекурсивную функцию, делаем её __forceinline, и включаем все опции инлайна на максимум.
Смотрим сколько это компилируется и будет весить. Ещё больше удивляемся скорости исполнения этого мутанта (на порядки медленней, чем без __forceinline).
vonrims
Есть подозрение что компилятор не будет инлайнить рекурсивную функцию )
KpeHDeJIb
> Есть подозрение что компилятор не будет инлайнить рекурсивную функцию )
Честно заинлайнить рекурсивную функцию невозможно.
На это уйдет бесконечное колличество времени.
Поэтому компиляторы пользуются хаком в виде гоуту(и его аналогов в асме).
Производительность действительно падает.
Мой тебе совет. Пиши на Си. Там инлайнов нет и производительность максимальна.
Никто так на вопрос автора не ответил, а мне вот тоже интересно знать. 🙂
Помоему там чтото связанное с количеством кода, мол когда всё инлайнишь, кода слишком много, памяти занимает много и типа по памяти надо далеко скакать. Или чтото вроде того. Может кто подробнее расскажет?
Адд:
Во, гугл как всегда спасает.
http://www.parashift.com/c++-faq-lite/inline-functions.html#faq-9.3
Тут есть ответы почему инлайн может делать хорошо и почему плохо.
my.name
> если ты про си++ и про inline модификатор, то он уже давно игнорируется
> компилятором, потому что компилятору виднее где нуна заинлайнить а где нет. к
> тому же, одна и та же функция может быть в одном месте заинлайнена а в другом
> нет.
inline не игнорируется, откуда такая информация?
Насколько помнится inline это лишь рекомендация для компилятора.
И вопрос как бы не про директиву inline и чего она делает.
Pokimon
> Пиши на Си. Там инлайнов нет и производительность максимальна.
inline там есть. В стандарт вошел в C99. В gcc он есть гораздо дольше. А кривой и медленный код можно писать на абсолютно любом языке.
Executor
> Насколько помнится inline это лишь рекомендация для компилятора.
Так и есть.
Pokimon
> (и его аналогов в асме)
И чем не устраивает «goto» в асме? И как например обходиться без «goto» на, например, ARM’ах? И чем call на Intel’ах идеологически отличается от например jmp или B?
Cpt. Gav
> Собственно Сабж.
Где-то вырастет, где-то упадет. И далеко не все можно заинлайнить.
Executor
> Может кто подробнее расскажет?
На самом деле инлайнить большие функции смысла немного, накладные расходы нивелируются временем работы кода, но опять
же в случае инлайна возможны оптимизации «на месте», которые часто невозможны в случае вызова процедуры. Впрочем в
современных компиляторах спасет функционал оптимизации по всей программе, а не только в модуле трансляции, но это еще
относительно новый функционал для компиляторов.
А вообще не зря стандарт определяет inline лишь как рекомендацию для компилятора, который на момент компиляции обладает
достаточными сведениями для того, чтобы решить необходимо ли инлайнить функцию или нет 🙂
crsib
> И чем call на Intel’ах идеологически отличается от например jmp или B?
Известно чем, в стек, как минимум, сохраняется значение ргеистра EIP/IP.
KpeHDeJIb
> Известно чем, в стек, как минимум, сохраняется значение ргеистра EIP/IP.
А положить его вручную никак нельзя?
ставить в настройках инлайнить любую подходящую и не парецо
X512
> А положить его вручную никак нельзя?
Ну во-первых инструкция CALL не просто так сделана, она еще для разных типов операндов и разных режимов работы процессора ведет себя по разному.
Положить то конечно можно, только вот тогда еще и RET придется эмулировать, оно тебе надо? 🙂
Источник
Как JIT инлайнит наш C# код (эвристики)
Инлайнинг — одна из самых важных оптимизаций в компиляторах. Она не только убирает оверхед от вызова, но и открывает много возможностей для других оптимизаций, например, constant folding, dead code elimination и т.д. Более того, иногда инлайнинг приводит к уменьшению размера вызывающей ф-ции! Я опросил несколько человек на предмет знают ли они по каким правилам инлайнятся ф-ции в C# и большинство ответили, что JIT смотрит на размер IL кода и инлайнит только маленькие ф-ции размером, скажем, до 32 байт. Поэтому я решил написать этот пост, чтобы раскрыть детали реализации при помощи вот такого примера, который покажет сразу несколько эвристик в деле:
Как вы думаете, заинлайнится ли вызов конструктора Volume тут? Очевидно, что нет. Он слишком большой, особенно из-за тяжеловесных throw new операторов, которые приводят к довольно жирному кодгену. Давайте проверим в Disasmo:
Заинлайнился! Более того, все выбросы исключений и их ветки успешно удалились! Вы можете сказать что-то в стиле «А, окей, джит очень умен и проделал полный анализ всех кандидатов к инлайну, посмотрел что будет если передать конкретные аргументы» или «Джит пробует заинлайнить всё что можно, выполняет все оптимизации, а потом решает профитно это или нет» (возьмите в руки комбинаторику и посчитайте сложность этой операции, например, для графа вызовов из десятка-двух методов).
Ну… нет, это нереалистично, особенно в терминах just in time. Поэтому, большинство компиляторов используют так называемые наблюдения и эвристики для решения это классической задачи о рюкзаке и пытаются сами определить себе бюджет и в него максимально эффективно вписаться (и нет, PGO не панацея). RyuJIT имеет положительные и отрицательные наблюдения. Положительные увеличивают коэффициент выгоды (benefit multiplier). Чем больше коэффициент — тем больше кода мы можем заинлайнить. Отрицательные наблюдения наоборот — понижают его или вообще могут запретить инлайнинг. Давайте посмотрим какие наблюдения сделал RyuJIT для нашего примера:
Эти наблюдения можно увидеть в логах из COMPlus_JitDump (например, в Disasmo):
Все эти простые наблюдения повысили коэффициент с 1.0 до 11.5 и помогли успешно побороть бюджет инлайнера, например, тот факт, что мы передаем аргумент-константу и она сравнивается с другой константой говорит нам, что с большой долей вероятности после схлопывания констант удалится одна из веток условия и код станет меньше. Или, например, то, что это конструктор и он вызывается внутри цикла — это тоже намек джиту, что он должен смягчить требования к инлайнингу.
Помимо benefit multiplier, RyuJIT так же использует наблюдения для прогноза размера нативного кода ф-ции и ее performance impact используя магические константы в EstimateCodeSize() и EstimatePerformanceImpact() полученные при помощи ML.
Кстати, а вы заметили этот трюк?:
Это оптимизировання версия к:
Оба выражения являются одним и тем же, но в первом случае у нас один базовый блок, а во втором их целых три. Оказывается, в инлайнере есть строгий лимит на кол-во базовых блоков в ф-ции и если оно превышает 5 то не важно какой большой у нас benefit multiplier — инлайнинг отменяется. Поэтому я применил эту уловку, чтобы вписаться в это строгое требование. Было бы классно если бы Roslyn делал это за меня.
Issue в Roslyn: github.com/dotnet/runtime/issues/13347
PR в RyuJIT (моя неловкая попытка): github.com/dotnet/coreclr/pull/27480
Там же я описал пример почему это имеет смысл сделать не только в Jit но и в компиляторе C#.
Инлайнинг и виртуальные методы
Тут всё понятно, нельзя заинлайнить то, о чем нет информации на этапе компиляции, хотя если тип или метод sealed то почему бы и нет.
Инлайнинг и выброс исключений
Если метод никогда не возвращает значение (например, просто делает throw new . ) то такие методы автоматически помечаются как throw-helpers и не инлайнятся. Это такой способ замести сложный кодген от throw new под ковер и ублажить инлайнер.
Инлайнинг и [AggressiveInlining] атрибут
В этом случае вы рекомендуете инлайнеру заинлайнить метод, но тут надо быть предельно осторожным по двум причинам:
- Возможно, вы оптимизируете один случай и ухудшаете все остальные (например, улучшаете случай константных аргументов) по размеру кодгена.
- Инлайнинг частенько генерирует большое количество временные переменных, которые могут перешагнуть определенный лимит — количество переменных, жизненный цикл которых RyuJIT может отследить (512) и после него код начнет обрастать жуткими спиллами в стек и сильно замедляться. Два хороших примера: тыц и тыц.
Инлайнинг и динамические методы
В данный момент такие методы ничего не инлайнят и сами не инлайнятся: github.com/dotnet/runtime/issues/34500
Моя попытка написать свою эвристику
Недавно я попытался написать собственную эвристику чтобы помочь вот такому случаю:
В своем прошлом посте я упоминал что совсем недавно я оптимизировал в RyuJIT вычисление длины от константных строк ( «Hello».Length -> 5 ), так вот, в примере выше ^ мы видим что если заинлайнить Validate в Test , то мы получим if («hello».Length > 10) что оптимизируется в if (5 > 10) что оптимизируется в удаление всего условия/ветки. Однако, инлайнер отказался инлайнить Validate :
И главная проблема тут в том, что пока нет эвристики, которая подскажет джиту, что мы передаем константную строку в System.String::get_Length , а значит что callvirt-вызов скорее всего свернется в константу и вся ветка удалится. Собственно, моя эвристика и добавляет это наблюдение (единственный минус — приходится резолвить все callvirt’ы что является не очень быстрым).
Существуют и другие ограничения, со списком которых можно в целом ознакомиться вот тут. А тут можно прочитать мысли одного из главных разработчиков JIT о дизайне инлайнера и его статью на тему использования Machine Learning для этого дела.
Источник
Как компилятор определяет что можно заинлайнить?
Вот положим компилятор взял и заинлайнил funca.
А мы, положим, определим функцию в другом юните. Или даже так:
А теперь вопрос:
А зачем её, кстати, удалять при подстановке инлайнов?
А бинарь не разожрется от того что каждая функция может получиться в двух экземплярах?
Инлайн можно отрубить как сущность в gcc.
Компилятор может заинлайнить, но если функция не статическая, то незаинлайненная версия тоже будет доступна.
И попытаюсь дернуть funca. А нету там больше funca!
С тем же успехом ты можешь вызывать funca из, скажем, кирпича. Нет ее там, верно. Не вызывай же. Чтобы вызвать функцию, она должна быть объявлена как extern. Для обычных функций — оно неявно, а inline и extern не совместимы. Но инлайн можно определить в хэдере, и она будет генерироваться в каждой единице компиляции независимо.
Для обычных функций — оно неявно, а inline и extern не совместимы
Проблема не в том что я хочу дернуть inline функцию, а в том что компилятор за меня решил что не смотря на отсутствие inline он функцию funca заинлайнил.
А вот тут товарищ заявляет, что static == static inline. И там же второй чувак говорит что указатели на static функции — основа фабрик. Значит и static (static inline) тоже инлайнится без удаления оригинала.
Хм. Работающий бы пример. Предположу, что функция в модуле сохраняется как обычная out-of-line, но для вызовов внутри модуля она может вставляться inline.
А мы, положим, определим функцию в другом юните.
Получим ошибку линковки
По идее может, потому что на явное объявление он кладет и сам расставляет inlinе так где ему удобно.
Не совсем так, явные объявления могут помочь заинлайнить более «сложную» функцию, которая без объявления не инлайнится.
А вообще основное назначение ключевого слова inline — обход ODR.
Re: Как компилятор определяет что можно заинлайнить?
Как компилятор определяет что можно заинлайнить?
согласно алгоритмам — см сырцы компилятора.
Если хэдер подключить — не получим. Код хэдера опустил по очевидным причинам.
Не разожрётся. Реально имеет смысл подставлять тело функции вместо её вызова только для тех функций, стоимость «вызова и исполнения» которых может оказаться заметно больше стоимости «просто исполнения». Оверхед на вызов может оказаться заметным только тогда, когда сама функция очень мала. То есть бинарнику просто некуда сильно разожраться, если размер дублируемых функций невелик.
Функции, которые библиотека предоставляет наружу, компилятор обязан оставлять в ней. Подстановка тела ( inline ) и ограничение области видимости функции ( static ) — ортогональные понятия.
Ну и как тут сказали выше, inline реально только на ODR влияет, потому что подстановку тела компилятор вправе и не выполнять.
Проблема не в том что я хочу дернуть inline функцию, а в том что компилятор за меня решил что не смотря на отсутствие inline он функцию funca заинлайнил.
Тоже недавно удивился этому. Я думал, что функция для компилятора, это то, чего он не должен касаться. По крайней мере по тому, что я видел с MS компилятором.
GCC в этом плане совсем не нравится.
Значит и static (static inline) тоже инлайнится без удаления оригинала.
Это, кстати, не значит, что она будет видна извне как символ.
А я возьму и залинкую эту штуку как библиотеку. И попытаюсь дернуть funca. А нету там больше funca!
Ну так и собирай её как библиотеку, с -shared. Тогда будет. А в исполняемом файле нет никакой нужды funca держать. Кто в здравом уме будет его как библиотеку использовать? Сильно специфические потребности.
уж определитесь, либо отключите inline флагами, либо через extern например, чтобы символ оставался.
А вот тут товарищ заявляет, что static == static inline.
inline — это всего лишь хинт компилятору, а не требование. И да, для современных компиляторов static == static inline для функций в c-файле.
второй чувак говорит что указатели на static функции — основа фабрик. Значит и static (static inline) тоже инлайнится без удаления оригинала.
Ну да, я озвучил только одно достаточное условие. Очевидно, что если код использует указатель на static-функцию, передавая его куда-то наружу (т.е. компилятор не сможет это оптимизировать), то оригинал удалён не будет.
тоже кстати столкнулся с такой проблемой, в цланге правда.
есть, грубо говоря, либа libfoo с внешними функциями foo1, foo2. foo2 вызывает foo1. обе функции снаружи доступны, но foo1 инлайнится в foo2. поэтому, когда для тестов мокаю foo1 (с помощью LD_PRELOAD), поведение foo2 не меняется.
Как с этим бороться не совсем ясно, совсем отключать инлайнинг не очень хочется, руками расставлять __attribute__ ((noinline)) тоже не нравится (т.к. функций таких значительно больше, чем 2). мокать foo2 и foo1 — копи-паст разводить.
есть ли красивое универсальное решение хотя бы для gcc и clang’а?
Если два объектных файла экспортируют один и тот же символ, то будет ошибка линковки (если один из них weak, то не будет, просто будет приоритет у того, который не weak, инлайн в таких условиях, очевидно, будет невозможен). После же линковки все символы, которые линковщик смог найти в объектных файлах, будут заменены на вызовы конкретных функций. Так что из so-шников они уже импортироваться не могут, потому что о них не останется информации в elf (кроме отладочной информации).
Если функция инлайнится, но не объявлена как static, то компилятор в любом случае запихнёт в объектный файл её нормальную версию. Просто внутри самого модуля она будет инланйнится. Так что никаких проблем с экспортом нет.
есть ли красивое универсальное решение хотя бы для gcc и clang’а?
Можно разместить foo1 в отдельном .c-модуле. Но это может снизить производительность, т.к. компилятор не просто так инлайнит. В sqlite решили эту проблему с производительностью сурово — см. их amalgamation.
Можно для тестирования через LD_PRELOAD собирать спец.версию с отключенным инлайнингом.
Можно попробовать переписать логику работы функций так, чтобы использовать LD_PRELOAD для тестирования не требовалось (ну, не всегда можно, конечно).
конфликт у тебя будет, если две одинаковые функции в разных объектниках. линкер заругается и пошлёт тебя лесом.
и объявляли вы эту функцию так:
а вообще, проблемы того, как функцию определять — ответственность компилятора: может конкретное железо вообще инструкцию call не поддерживает и что тогда?
может конкретное железо вообще инструкцию call не поддерживает и что тогда?
Тогда для этого железа нет компилятора C.
Можно разместить foo1 в отдельном .c-модуле. Но это может снизить производительность, т.к. компилятор не просто так инлайнит.
ну это так себе вариант по двум причинам:
1. во-первых, ухудшается читаемость, т.к. модулей в проекте и так в районе ста, если по одной-две функции начинать выносить, но будет сложно ориентироваться и поддерживать.
2. во-вторых, проблема такая же, как с noinline аттрибутом — кто знает, сколько потенциально придется выносить таких функций
В sqlite решили эту проблему с производительностью сурово — см. их amalgamation.
Можно для тестирования через LD_PRELOAD собирать спец.версию с отключенным инлайнингом.
это тоже не очень красивый вариант, потому что время сборки увеличится значительно.
Можно попробовать переписать логику работы функций так, чтобы использовать LD_PRELOAD для тестирования не требовалось (ну, не всегда можно, конечно).
имеется ввиду вариант типа создания foo_enable_mock(), который меняет поведение функции?
В двух? Ты не обкурился? Инлайнутая функция уже в 100500 местах инлайнута, ещё один инстанс в виде оригинала погоды не сделает.
Re: Как компилятор определяет что можно заинлайнить?
Вот этот неожиданно прав. Что и когда инлайнить — это строго не решается, только эвристиками.
имеется ввиду вариант типа создания foo_enable_mock(), который меняет поведение функции?
Нет, я скорей о том, чтобы foo2 перестала вызывать foo1. 🙂
Ну и вы же всё равно все функции так не протестируете, ведь есть ещё неэкспортируемые. А значит, для тестирования всё равно нужно так или иначе собирать модифицированную версию программы/библиотеки (как сmocka предлагает, например). Ну а раз так, то уж заодно можно собирать и без всяких оптимизаций и инлайнов. А в обычной сборке можно оставить только тесты без mock.
Источник