#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 уровня переходов (переключение, а затем вызов), если она не переходит непосредственно к указателю функции-члена.