Разница между указателями функций на функции-члены класса и указателем на произвольную функцию

#c #assembly #x86-64 #function-pointers #member-function-pointers

#c #сборка #x86-64 #указатели на функции #указатель на член

Вопрос:

Я пытаюсь протестировать самый быстрый способ вызова указателя на функцию, чтобы обойти шаблоны для конечного количества аргументов. Я написал этот тест: https://gcc.godbolt.org/z/T1qzTd

Я замечаю, что указатели функций на функции-члены класса имеют много дополнительных накладных расходов, которые мне трудно понять. Я имею в виду следующее:

Со структурой bar и функцией foo, определенной следующим образом:

 template<uint64_t r>
struct bar {
    template<uint64_t n>
    uint64_t __attribute__((noinline))
    foo() {
        return r * n;
    }
    
    // ... function pointers with pointers to versions of foo below
  

Первый вариант ( #define DO_DIRECT в коде godbolt) вызывает шаблонную функцию путем индексации в массив указателей функций на функцию-член класса, определенную как

    /* all of this inside of struct bar */
   typedef uint64_t (bar::*foo_wrapper_direct)();
   const foo_wrapper_direct call_foo_direct[NUM_FUNCS] = {
      amp;bar::foo<0>,
      // a bunch more function pointers to templated foo...
   };

   // to call templated foo for non compile time input
   uint64_t __attribute__((noinline)) foo_direct(uint64_t v) {
      return (this->*call_foo_direct[v])();
   }
   
  

Однако в сборке для этого, похоже, много пуха:

 bar<9ul>::foo_direct(unsigned long):
        salq    $4, %rsi
        movq    264(%rsi,%rdi), %r8
        movq    256(%rsi,%rdi), %rax
        addq    %rdi, %r8
        testb   $1, %al
        je      .L96
        movq    (%r8), %rdx
        movq    -1(%rdx,%rax), %rax
.L96:
        movq    %r8, %rdi
        jmp     *%rax
  

Что мне трудно понять.

В отличие от #define DO_INDIRECT метода, определенного как:

 // forward declare bar and call_foo_wrapper
template<uint64_t r>
struct bar;

template<uint64_t r, uint64_t n>
uint64_t call_foo_wrapper(bar<r> * b);


/* inside of struct bar */
typedef uint64_t (*foo_wrapper_indirect)(bar<r> *);
const foo_wrapper_indirect call_foo_indirect[NUM_FUNCS] = {
    amp;call_foo_wrapper<r, 0>
    // a lot more templated versions of foo ...
};

uint64_t __attribute__((noinline)) foo_indirect(uint64_t v) {
    return call_foo_indirect[v](this);
}
/* no longer inside struct bar */

template<uint64_t r, uint64_t n>
uint64_t
call_foo_wrapper(bar<r> * b) {
    return b->template foo<n>();
}
  

имеет очень простую сборку:

 bar<9ul>::foo_indirect(unsigned long):
        jmp     *(%rdi,%rsi,8)
  

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

Примечание: у меня есть __attribute__((noinline)) только для того, чтобы упростить проверку сборки.

Спасибо.

ps если есть лучший способ преобразования параметров среды выполнения в параметры шаблона, я был бы признателен за ссылку на пример / справочную страницу.

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

1. Потому что в одном случае нужно: 1) умножить индекс на sizeof (указатель), 2) извлечь соответствующее значение из массива, доступного только для чтения, 3) иметь дело с несколькими различными возможностями интерпретации указателя на функцию в коде PIC и 4), наконец, вызвать функцию через указатель ив другом случае он переходит непосредственно к шагу 4?

2. Похоже addq %rdi, %r8 , это смещение this в случае возможного подобъекта? Я также не уверен, зачем ему нужно переходить на младший бит указателя функции-члена. Но обратите внимание, что указатели на функции-члены составляют 16 байт. Кстати, ваша таблица отправки отсутствует static , поэтому в каждом экземпляре класса есть ее копия. Вот почему он индексирует большое смещение от входящего this . Вероятно, без этого он все равно работал бы так же, но это крайне неэффективно.

3. Значение функции-указателя на функцию-член не включает адрес / идентификатор объекта / и т.д. Когда шаблон класса объявляет static член, это означает один объект на экземпляр класса. Таким образом, должно быть возможно bar<r> иметь статический элемент массива amp;bar<r>::foo<0> , содержащий указатели и т.д. Если у вас возникли проблемы с синтаксисом для этого, это может стоить другого вопроса.

4. @PeterCordes Я протестировал оператор switch, но он работает заметно хуже, просто выполняя цикл, и это будет просто больше нагрузки на icache, когда он фактически используется в программе.

5. @PeterCordes Во всех версиях, если объект bar<x>::arr odr используется для некоторого определенного значения x , тогда определение должно быть создано с тем же x самым . В C 14 объявление элемента статических данных в определении класса никогда не считается определением, даже если оно есть constexpr и имеет инициализатор. Если TU, который не содержит определения odr-использует объект, это неявно создает экземпляр только объявления члена, и определение должно существовать.

Ответ №1:

Указатель C на функцию-член должен быть способен указывать на невиртуальную функцию или виртуальную функцию. В типичной реализации vtable / vptr вызов виртуальной функции включает поиск правильного адреса кода из vptr в выражении объекта и, возможно, применение смещения к адресу параметра объекта.

g использует Itanium ABI, поэтому сборка для foo_direct интерпретирует значение доступного указателя на функцию-член, как описано в разделе 2.3 . Он находит адрес кода через vptr выражения объекта, если функция виртуальная, или просто копирует адрес кода из значения указателя на член, если он не виртуальный.

Я полагаю, что оптимизация может пропустить логику вызова виртуальной функции, если она видит, что тип класса не имеет виртуальных функций и является final . Однако я не знаю, есть ли у g или других компиляторов такая оптимизация.

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

1. Кстати, я думаю add , что это необходимо для поддержки случая, когда функция-член была определена в родительском классе . В этом случае this для экземпляра родительского класса может быть другим из-за другой виртуальной таблицы. В случае использования OP это все 0 .

2. Возможно switch{ case: } , таблица была бы лучшим способом заставить компилятор создать таблицу отправки, хотя у нее все еще может быть 2 уровня переходов (переключение, а затем вызов), если она не переходит непосредственно к указателю функции-члена.