cpp-ru / ideas

Идеи по улучшению языка C++ для обсуждения
https://cpp-ru.github.io/proposals
Creative Commons Zero v1.0 Universal
89 stars 0 forks source link

Уточнить условие "inline-hint" для inline-функций #507

Open Izaron opened 2 years ago

Izaron commented 2 years ago

Так вышло, что сейчас в C++ ключевое слово inline для функций (точнее, само понятие inline-функции) наделяет их двумяя абсолютно ортогональными друг другу effective смыслами. Понятно, что один смысл вытек из другого, но все равно это раздельные сущности. Это:

  1. [dcl.inline].2: Подсказка компилятору о том, что более предпочтительна подстановка кода из функции в место вызова, чем сам вызов функции. (В компиляторе Clang в LLVM IR этот атрибут у функции называется inlinehint)
  2. [dcl.inline].6: Правило, что у функции может быть несколько определений (definition) за программу. Для отсутствия UB это должны быть одинаковые определения - что естественным образом соблюдается, т.к. обычно метод определен в хидере. (В компилятора атрибут у функции называется linkonce_odr, для удобства далее linkonce)

Итого условно у функции есть два атрибута - inlinehint и linkonce.

Проблема в том, что: Стандарт написан так, что inlinehint дается только функциям, где явно написан спецификатор inline.

Clang выполняет именно это, т.е. inlinehint ставится тогда и только тогда, когда есть спецификатор inline.

То есть получается вот что: inline int random() { return 4; } будет И inlinehint, И linkonce.

А вот методы, которые по Стандарту implicit inline functions, как то:

Они только linkonce. Надо все равно писать ключевое слово inline, чтобы было linkonce+inlinehint.

Отсюда возникают разные неприкольные штуки:

  1. Стандарт пишет, что такие-то методы являются implicitly inline, но по факту разница между constexpr int foo() и inline constexpr int foo() есть, это гига контринтуитивно звучит.
  2. Пусть у нас функция "обычная" (не входит в список implicitly inline), мы хотим чтобы она была linkonce, но не хотим чтобы она была inlinehint (т.е. чтобы компилятор ее заоптимайзил сам по-нормальному). То мы не можем этого сделать, функция по-любому будет linkonce+inlinehint.

Методы починки:

  1. Status quo (не хотелось бы такого): все остаётся как есть
  2. Малый путь (я имхо верю в него): inlinehint объявляется для всех inline functions независимо от способа, каким они стали inline functions (т.е. неважно был ли у них написан спецификатор inline или нет)
  3. Большой путь (я в него не особо верю): вводится новое ключевое слово linkonce, понятие inline functions переименовывается в linkonce functions, всё остальное остаётся по-прежнему. Если keyword inline наделяет метод атрибутами inlinehint+linkonce, то keyword linkonce мог бы наделять его только атрибутом linkonce.

(По методам надо провести голосование и потом доработать paper в сторону выбора)

Ответы на возможные вопросы:

  1. Компиляторы же давно не обращают внимание на "подсказки" от программистов Я тоже так думал, но оказалось что нет, если в 2022 году запустить Clang под флагами без оптимизации LLVM IR, то атрибут inlinehint можно увидеть. А в самом LLVM IR он судя по исходникам таки влияет на анализ каких-то костов, т.е. это не дохлый атрибут.

  2. Почему один атрибут вытекает из другого, если они ортогональны? Чтобы компилятор мог в translation unit заинлайнить (inlinehint) функцию, TU нужно "видеть" исходник этой функции, то есть её тело. В общем случае это невозможно обеспечить, потому что если у нас N штук TU, то будет N определений одной и той же функции и линкер сломается. Поэтому эта функция должна являться linkonce, чтобы не нарушился ODR.

Такая же логика для constexpr-функций - TU должен "видеть" ее, чтобы вычислить в compile-time, поэтому тут уж удобнее сделать linkonce автоматически.

Конечно, можно было бы подойти с другого пути и объявлять такие функции static, но это все равно немного не то - а если внутри static-функции есть статические переменные, то они будут займут память не 1 раз, а N раз, и будут не "расшариваемы" среди разных TU.

pavelkryukov commented 2 years ago

Подсказка компилятору о том, что более предпочтительна подстановка кода

Вброшу пару идей:

методы, которые по Стандарту implicit inline functions, как то инстанциации шаблонов

Компиляторы разве не должны их инлайнить по максимуму? Гарантируется, что другие единицы трансляции на них ссылаться не будут, если не сказано extern template. Про остальное не уверен.

Izaron commented 2 years ago

Из соображений симметрии можно добавить атрибут «не инлайнить, экономить размер кода»

С этим всё сложно - атрибутов довольно много, вот где я их смотрел https://github.com/llvm/llvm-project/blob/main/clang/lib/CodeGen/CodeGenModule.cpp#L1829-L1990

Атрибут inlinehint здесь Attribute::InlintHint. Атрибут "не инлайнить" (т.е. в смысле вообще никогда) это Attribute::NoInline. Атрибут "экономить размер кода" это то ли Attribute::OptimizeForSize, то ли Attribute::MinSize. И там еще несколько есть.

То что вы предлагаете это "атрибут чтобы не было атрибута inlinehint", я думаю так не сработает

Компиляторы разве не должны их инлайнить по максимуму?

Шаблонные inline functions или вообще? В общем случае всё что меняется у первоначального LLVM IR, это наличие атрибута linkonce_odr вместо dso_local, больше ничего. Частные кейсы надо по Стандартам поискать

Izaron commented 2 years ago

Со скобочными атрибутами, если не менять Стандарт в указанном месте, то выйдет наверное как [[noinlinehint]] inline int foo() (чтобы оставить linkonce, а не linkonce+inlinehint), думаю такое не понравится всем((

Кстати, если даже качественные изменения не примут, всё равно бы наверное хотелось, чтобы понятие inline functions в текущем состоянии переименовали в linkonce functions, потому что дикая путаница в данный момент.

pavelkryukov commented 2 years ago

Шаблонные inline functions или вообще?

Шаблоны. Про остальное тоже есть вопросы – например, default конструкторы на то и есть, чтобы они как можно сильнее выродились в memset/memcpy. Но пока не готов подкрепить практикой.

pavelkryukov commented 2 years ago

чтобы оставить linkonce, а не linkonce+inlinehint), думаю такое не понравится всем((

Да, в этом контексте абсурдно. Но если inlinehint по умолчанию навесить всему, что implict inline, то будет осмысленно:


class Bar {
    void foo() [[noinline]] {
         // very long code
    }
};
pavelkryukov commented 2 years ago

inlinehint объявляется для всех inline functions независимо от способа, каким они стали inline functions

Но если inlinehint по умолчанию навесить всему, что implict inline

Почитал тред из архивов LLVM, производительность не очень хорошая: https://lists.llvm.org/pipermail/llvm-dev/2015-July/087668.html

pavelkryukov commented 2 years ago

В той переписке мне приглянулась мысль, что раз стандарт количественных данных не содержит, то и предлагать оптимизации не может. Поэтому мне показалось правильным, если касательно оптимизаций стандарт даст мотивировочную часть и опишет необходимые условия, но не будет иметь резолютивную часть:

The inline specifier indicates to the implementation that inline substitution of the function body at the point of call is to be preferred to the usual function call mechanism must be possible. An implementation is not required to perform this inline substitution at the point of call; however, even if this inline substitution is omitted, the other rules for inline functions specified in this subclause shall still be respected.

В результате:

A function declaration ([dcl.fct], [class.mfct], [class.friend]) with an inline specifier declares an inline function a linkonce function.

[Note 4: A constexpr function is implicitly inline linkonce. In the global module, a function defined within a class definition is implicitly inline linkonce ([class.mfct], [class.friend]). — end note]

Izaron commented 2 years ago

Спасибо! Да - в paper надо бы написать, что из возможных исходов нарушение статус-кво повлечет непредсказуемые последствия: из переписки 2015 года стало видно, что где-то ускорился код, где-то бинарник увеличился, где-то и то и другое. На такое пойти опасно, это по эффекту почти как легкий слом ABI.

Я думаю что понятие inline variable (в том же параграфе) тоже надо заменить на linkonce variable. Сейчас слово inline там совершенно точно неправильно. Читатели могут думать что компилятор такие переменные всегда "заинлайнит", но это не так - такие переменные кроме отличающегося ODR ничем не отличаются от "обычных".

Если например какой-то TU возьмет адрес у такой переменной, то компилятор ее не сможет заинлайнить в 100% случаев и поместит ее в секцию .rdata и будет обращаться к ней читая .rdata. То есть понятия inlinehint у переменнных by desing не бывает.

Пониже [dcl.inline].2 можно бы написать [Note 2], который явно скажет, что у всех остальных linkonce функций нет рекомендации про inline substitution.

В paper в качестве мотивирующего примера (что текущие понятия запутывают) можно еще положить попытку написания в clang-tidy фиксера "лишних inline", который вы кидали в прошлом issue =) Там тоже далеко не сразу поняли что что-то не так.

P.S. @pavelkryukov , как с вами связаться? =) Если у вас есть желание вместе подготовить paper и послать. Если захотите, то моя почта izaronplatz@gmail.com.