Почему gcc не может встроить указатели на функции, которые можно определить?

Следующая программа, скомпилированная в gcc 4.6.2 на centos с -O3:

#include <iostream>
#include <vector>
#include <algorithm>
#include <ctime>
using namespace std;

template <typename T>
class F {
public:
     typedef void (T::*Func)();

     F(Func f) : f_(f) {}

     void operator()(T& t) {
         (t.*f_)();
     }
private:
     Func f_;
};

struct X {
    X() : x_(0) {}

    void f(){
        ++x_;
    }

    int x_;
};

int main()
{
     const int N = 100000000;
     vector<X> xv(N);
     auto begin = clock();
     for_each (xv.begin(), xv.end(), F<X>(&X::f));
     auto end = clock();
     cout << end - begin << endl;
}

objdump -D показывает, что сгенерированный код для цикла:

  40097c:       e8 57 fe ff ff          callq  4007d8 <[email protected]>
  400981:       49 89 c5                mov    %rax,%r13
  400984:       0f 1f 40 00             nopl   0x0(%rax)
  400988:       48 89 ef                mov    %rbp,%rdi
  40098b:       48 83 c5 04             add    $0x4,%rbp
  40098f:       e8 8c ff ff ff          callq  400920 <_ZN1X1fEv>
  400994:       4c 39 e5                cmp    %r12,%rbp
  400997:       75 ef                   jne    400988 <main+0x48>
  400999:       e8 3a fe ff ff          callq  4007d8 <[email protected]>

Очевидно, что gcc не встраивает функцию. Почему gcc не способен на эту оптимизацию? Есть ли какой-либо флаг компилятора, который может заставить gcc выполнить требуемую оптимизацию?

Ответ 1

Некоторые хорошие материалы для чтения по этому поводу - Скотт Adams Эффективный С++ Майерса (третье издание). Пункт 30: Понимать все входы и выходы, где он утверждает, что вызов функционального указателя никогда не привязан, Третье издание было опубликовано в 2008 году, и я действительно смог получить gcc для встроенного вызова функции с помощью команды compile-time-constant-pointer, начиная с gcc 4.6, которая вышла в 2011 году (может быть, 2010?). Однако это было на C и сложно. В одном из сценариев мне пришлось объявить вызывающую функцию __attribute__((flatten)), прежде чем она введет вызов (в этой ситуации я передал указатель функции как член структуры, указатель которой затем передал встроенной функции, которая сделала бы вызов функции указателем, который получил встроенный).

Короче говоря, нет, это не ошибка gcc, но это не означает, что gcc (и/или другие компиляторы), возможно, не смогут вставить это в какой-то день. Но реальная проблема, я думаю, заключается в том, что вы не понимаете, что на самом деле происходит здесь. Чтобы понять это, вы должны думать больше как программист по сборке или программист компилятора.

Вы передаете объект типа F<X> и инициализируете его указателем на функцию-член другого класса. Вы не указали свой экземпляр объекта F<X>, он Func f_ член как константа, а ваш член void F::operator()(T& t) как константа. На уровне языка С++ компилятор должен рассматривать его как непостоянный. Это все еще не означает, что на этапе оптимизации не может быть установлено, что ваш указатель функции не изменяется, но вы делаете его невероятно тяжелым на данный момент. Но, по крайней мере, это локальный. Если ваш объект F<X> был глобальным и не был объявлен static, он запретил бы его полностью считать постоянным.

Надеюсь, вы делаете это при упражнении с инкрустацией указателем на функцию, а не как реальное решение для косвенности. Когда вы хотите, чтобы С++ создавал реальную производительность, вы используете силу типов. В частности, когда я объявляю параметр шаблона в качестве указателя функции-члена, это не просто константа, это часть типа. Я никогда не видел случая, когда этот метод генерирует вызов функции.

#include <iostream>
#include <vector>
#include <algorithm>
#include <ctime>
using namespace std;

template <typename T, void (T::*f_)()>
class F {
public:
     void operator()(T& t) {
         (t.*f_)();
     }
};

struct X {
    X() : x_(0) {}

    void f(){
        ++x_;
    }

    int x_;
};

int __attribute__((flatten)) main()
{
     const int N = 100000000;
     vector<X> xv(N);

     auto begin = clock();
     for_each (xv.begin(), xv.end(), F<X, &X::f>());
     auto end = clock();
     cout << end - begin << endl;

}

Ответ 2

Я думаю, что GCC пытается оптимизировать целую функцию main, но не удается (много косвенных вызовов глобальных функций для выделения/освобождения памяти для xv, получения значения таймера, ввода/вывода и т.д.). Таким образом, вы можете попытаться разделить свой код на две (или более) независимые части, например:

inline
void foobar(vector<X>& xv)
{
  for_each (xv.begin(), xv.end(), F<X>(&X::f));
}

int main()
{
  const int N = 100000000;
  vector<X> xv(N);
  auto begin = clock();
  foobar(xv);
  auto end = clock();
  cout << end - begin << endl;
}

Итак, теперь у нас есть "эквивалентный" код, как и раньше, но оптимизатор GCC теперь имеет более легкую задачу. Я не вижу никаких вызовов ZN1X1fEv в списке ассемблера.

Ответ 3

Вы можете добавить inline __attribute__((__always_inline__)) к своей функции и -Winline флаг в компилятор, поэтому вы заметите, когда компилятор не сможет выполнить встроенную функцию.

К сожалению, атрибут не сделает вашу функцию встроенной, а Winline не будет звучать. До 4.8. НО!!! С 4.9 этот вопрос кажется исправленным!

Итак, возьмите gcc 4.9, добавьте флаг always_inline, установите оптимизатор на уровень -O3. И будьте счастливы!

Доказательство: http://goo.gl/kkuXzb