Оптимизируют ли компиляторы вызовы тривиальных функций, выполняемых с помощью указателей?

#c #optimization #function-pointers

#c #оптимизация #указатели на функции

Вопрос:

Допустим, у меня есть функция, которая принимает указатель на функцию:

 int funct(double (*f)(double));
  

И я передаю ему функцию, которая на самом деле ничего не делает:

 double g(double a) { return 1.0;}
//...
funct(g);
  

Оптимизирует ли компилятор вызовы g ? Или это все еще будет иметь накладные расходы? Если это действительно накладные расходы, то насколько? Достаточно, чтобы стоило перегружать функцию для получения как указателей на функции, так и постоянных значений?

Ответ №1:

Более новые версии GCC (4.4 и более поздние) могут встроить и оптимизировать известный указатель на функцию, используя опцию -findirect-inlining . Это работает только тогда, когда GCC также знает весь код, использующий указатель.

Например, библиотека C функция qsort будет не воспользоваться такой оптимизации. Скомпилированный машинный код для qsort находится в библиотеке, он ожидает указатель на функцию, и компилятор не может это изменить.

Однако, если бы у вас была ваша собственная реализация qsort и вы поместили ее в файл заголовка или вы использовали совершенно новые функции оптимизации времени соединения GCC, то GCC смог бы взять ваш вызывающий код, функцию, на которую указано, и ваш исходный код qsort и скомпилировать все это вместе, оптимизировав для ваших типов данных и вашей функции сравнения.

Теперь, единственные случаи, когда это действительно имеет значение, — это когда накладные расходы на вызов функции намного больше, чем сама функция. В вашем примере функции, которая ничего особенного не делает, использование указателя на функцию сопряжено с серьезными накладными расходами. В моем примере сравнения qsort вызов указателя функции также является довольно дорогостоящим. Но в некоторых других приложениях, таких как Windows event dispatch, это вряд ли имеет значение.

Поскольку вы используете C , вам следует изучить шаблоны. Шаблонная функция может принимать так называемый, function object который является просто объектом, который реализует, operator() и он может принимать указатели на функции. Передача объекта function позволит компилятору C встроить и оптимизировать почти весь задействованный код.

Комментарии:

1. Шаблоны были другим способом управления этим, и я все еще мог бы использовать их, однако я не очень хорошо их понимаю (я все еще учусь), поэтому указатели на функции были бы простым выходом.

2. GCC будет знать весь код, который использует указатель, однако funct вероятно, он будет частью класса в другом файле (и будет использовать ряд различных методов в этом классе funct ). Сможет ли GCC оптимизировать это? К сожалению, код достаточно чувствителен к этому с точки зрения производительности, что я должен подумать об этом ( g вероятно, будет вызван более пары сотен тысяч раз)

Ответ №2:

Любой современный компилятор может (и будет) легко оптимизировать исходящие (т. Е. встроенные) вызовы, выполняемые с помощью указателей на функции, в ситуациях, когда компилятор знает, на что указывает указатель во время компиляции.

В вашем конкретном примере в общем случае значение указателя не может быть предсказано во время компиляции в момент вызова, поскольку оно передается извне как параметр функции funct . В ситуациях, когда funct сама функция достаточно мала, чтобы ее можно было встроить, параметр времени выполнения удаляется, и весь код funct «растворяется» в funct контексте вызывающего объекта. Если значение указателя известно в контексте вызывающего объекта, то, опять же, компилятор может легко исключить вызов через указатель.

Наконец, если функция funct относительно тяжелая (то есть она не становится встроенной), то, вероятно, следует ожидать, что компилятор сгенерирует обычный вызов во время выполнения через указатель изнутри funct . В этом случае также существует вероятность исключения вызова, поскольку компилятор теоретически может сгенерировать отдельную версию funct для каждого значения g во время компиляции. Например, если у вас есть

 int funct(int x1, int x2, int x3, double (*f)(double));
  

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

 funct(rand(), rand(), rand(), g1);
...
funct(rand(), rand(), rand(), g2);
  

затем компилятор может «сократить» его до двух функций

 int funct_g1(int x1, int x2, int x3);
int funct_g2(int x1, int x2, int x3);
  

без параметров указателя функции и с прямыми вызовами либо g1 , либо g2 внутри. (Конечно, эта оптимизация никоим образом не привязана к указателям на функции и может быть применена к любому параметру функции, который используется только с небольшим фиксированным набором аргументов. Фактически, это похоже на шаблоны C .) Но я не знаю ни одного компилятора, который бы делал что-либо подобное.

Ответ №3:

Компилятор, вероятно, не будет оптимизировать это, потому что функция funct может получать указатели на разные функции, а не только g , и они не обязательно должны исходить из одного и того же модуля компиляции (таким образом, компилятор не может предполагать, что он знает обо всех возможных вызовах).

Вам нужно протестировать свой код, чтобы увидеть, необходима ли оптимизация, о которой вы говорите, и если это так — просто сделайте это. Но если funct вызовов g не будет много, я бы не ожидал, что это будет иметь значение.

Комментарии:

1. Компилятор может оптимизировать вызов функции с помощью указателя, даже если существует более одного вызова, даже если функции находятся в разных единицах преобразования. Это называется полной оптимизацией программы, которая выполняется во время компоновки.

2. Различие между компилятором и компоновщиком не такое четкое, как это было в прошлом. В настоящее время оптимизация и генерация кода выполняются во время компоновки, во время выполнения, …

3. Иногда в такой ситуации gcc просто создает две функции: одну для случая с известным указателем на функцию и одну для общего случая. При таком подходе компоновщику не нужно делать ничего особенного.

4. funct вероятно, вызовет g от пары сотен тысяч до пары миллионов раз… может быть, больше. 🙂

5. @spot — тогда я бы посоветовал не полагаться на оптимизацию компилятора.