#cpp #функции #циклы #рекурсия #inline
Почему функции которые содержат циклы, рекурсию или вызываются по указателю, плохо
подвержены встраиванию (inline)
Ответы
Ответ 1
Вопрос построен на терминологической путанице. Встраивание вызовов функции - это
не свойство самой функции, а всегда именно свойство индивидуального вызова функции.
Свойства самой функции, разумеется, играют роль в принятии решения о том, будет ли
конкретный вызов встроен, но все равно в общем случае это решение принимается для каждого
отдельного вызова индивидуально. Одни вызовы функции могут оказаться встроены, в то
время как другие вызовы той же функции могут оказаться не встроены. Даже аргументы
функции, указанные в конкретном вызове, могут влиять на решение о том, встраивать ли
этот конкретный вызов.
Утверждение о том, что вызовы функций с циклами не встраиваются, относится к категории
"это было давно и неправда".
Каждый компилятор реализует какой-то набор эвристических критериев, который дает
ему возможность принимать общие решения о том, заслуживают ли вызовы данной функции
встраивания. В каком-то из старинных компиляторов этот эвристический критерий также
включал проверку тела функции на наличие циклов (Borland или что-то в этом роде).
На самом деле циклы не представляют из себя никаких преград для встраивания. Мне
не известны никакие современные реализации, которые бы отказывались встраивать вызовы
функций с циклами внутри.
Утверждение о том, что функции, которые вызываются через указатель, не встраиваются
- это как раз яркий пример того терминологической путаницы, о которой я писал выше.
Это вызовы, выполненные через указатель, в общем случае не встраиваются. В то же время
вызовы той же самой функции, выполненные напрямую, могут прекрасно встраиваться.
Почему в общем случае невозможно встроить вызов, сделанный через указатель, очевидно:
потому что на стадии компиляции не ясно, какая функция будет вызываться. Однако если
компилятор в состоянии сообразить на стадии компиляции, какая функция будет вызвана
через указатель, он без проблем встроит и такой вызов.
Классический часто задаваемый вопрос, основанный на данной терминологической путанице
- вопрос о встраивании вызовов виртуальных методов классов. Они ведь вызываются "через
указатель" и значит не могут быть встроены, правда? Не правда. Во-первых, сам пользователь
может выполнить вызов виртуального метода напрямую, без использования виртуального
механизма. Во-вторых, в огромном количестве контекстов сам компилятор в состоянии сообразить,
какой конкретный метод вызывается в данном вызове и, соответственно, встроить этот вызов.
Вызовы рекурсивных функций тоже могут встраиваться. Если компилятор на стадии компиляции
в состоянии оценить максимальную глубину рекурсии, то он может встроить эти вызовы
("развернуть рекурсию") на всю глубину. Если компилятор не в состоянии выполнить такой
оценки, то он может встроить рекурсивные вызовы до определенной фиксированной глубины,
после чего выполнить обычный рекурсивный вызов. Встраивание вызовов рекурсивных функции
в этом отношении во многом аналогично развертке циклов.
Одной из простейших и наиболее очевидных ситуаций целесообразности встраивания вызова
функции является, например, ситуация, когда заведомо известно, что некоторая функция
вызывается в программе ровно один раз (в пространственном смысле, т.е. исходный код
программы содержит ровно одно место с ее вызовом). Такой функции нет никакого смысла
существовать в виде отдельной функции, независимо от того, как "тяжела" эта функция.
(С этим идет ряд побочных оговорок, но к данной теме они не относятся.) Например, если
функция имеет внутреннее связывание и вызывается в своей единице трансляции ровно один
раз, то современные компиляторы встроят этот вызов независимо от каких-либо иных критериев.
В компиляторе GCC за это отвечает опция -finline-functions-called-once, которая включается
уже в режиме -O1.
В качестве другого примера, в компиляторе MSVC++ есть #pragma-параметры inline_recursion
и inline_depth, которые управляют встраиванием рекурсивных функций и глубиной развертки
рекурсии.
Ответ 2
Ну, с рекурсией понятно - если она не преобразована компилятором в цикл, то что получится?
встраиваем код, на месте ее вызова опять встраиваем код, в котором на месте вызова...
Сколько раз код встраивать? :)
С вызовом по указателю - опять же: если встроена, то функция не имеет четкого пролога-эпилога,
и не может быть вызвана по адресу - так что при вызове через указатель должен иметься
не встроенный экземпляр функции как минимум - чтобы было, что вызывать.
В самом месте вызова через указатель встроить ничего нельзя - так как встраивает
компилятор, а во время компиляции значение указателя неизвестно.
А про циклы - мне кажется, тут вы что-то напутали. Функция с циклом внутри вполне
встраиваема - если, конечно, это имеет смысл :) Если вы имеете в виду, что сам цикл
раскручивается - то опять же, это делается, но со своими ограничениями - например,
компилятор может не знать, сколько реально итераций будет сделано - так как ему цикл
разворачивать?..