Я пытаюсь реализовать абстрактный интерфейс в C, используя указатели на функции внутри структуры.
Что-то вроде следующего
typedef int (*fn_t)(int);
typedef struct
{
int x;
const fn_t fnp;
}struct_t;
__attribute__((optimize("O0"))) int square(int num)
{
return num * num;
}
static struct_t test = {.fnp = square};
int main(void)
{
test.x = 1;
int fnp_ret = test.fnp(3);
return (fnp_ret);
}
При сборке в godbolt с -O3 с использованием ARM-GCC-13.2.0 неизвестного-eabi результат будет следующим.
square:
str fp, [sp, #-4]!
add fp, sp, #0
sub sp, sp, #12
str r0, [fp, #-8]
ldr r3, [fp, #-8]
mov r2, r3
mul r2, r3, r2
mov r3, r2
mov r0, r3
add sp, fp, #0
ldr fp, [sp], #4
bx lr
main:
mov r1, #1
ldr r3, .L5
mov r0, #3
ldr r2, [r3, #4]
str r1, [r3]
bx r2
.L5:
.word .LANCHOR0
Здесь можно видеть, что в main() созданная сборка сначала находит указатель функции в структуре, а затем разыменовывает его. Я нахожу это странным, поскольку указатель на функцию — это const, поэтому я ожидал, что компилятор поймет, что он всегда указывает на функцию square, поэтому это будет эквивалентно прямому вызову функции square. Видимо, здесь дело не в этом.
Во время эксперимента я заметил, что в случае, если оператор test.x = 1; закомментирован, сборка делает то, что я ожидал, вызывая функцию square напрямую.
square:
str fp, [sp, #-4]!
add fp, sp, #0
sub sp, sp, #12
str r0, [fp, #-8]
ldr r3, [fp, #-8]
mov r2, r3
mul r2, r3, r2
mov r3, r2
mov r0, r3
add sp, fp, #0
ldr fp, [sp], #4
bx lr
main:
mov r0, #3
b square
Чего мне не хватает?
Есть ли способ надежно реализовать это без потери производительности, описанной выше?
@gulpr: Эта функция является заполнителем для демонстрации. Его оптимизация или ее отсутствие не имеют значения для демонстрации.
@EricPostpischil Думаю, ОП хотел noinline
Это может быть специфично для компилятора/оптимизатора Arm. Когда я создаю ваш первый пример для x86, под gcc (8.3.1) main есть три строки: movl $1, test(%rip); movl $3, %edi; jmp *test+8(%rip); Это то, что делает ваш второй тест (но не «встраивает» указатель функции). Но в clang (7.0.1) это: movl $9, %eax; retq; Итак, это может быть «преждевременная оптимизация». (т. е. не волнуйтесь — будьте счастливы!). Вы сохраняете только одну ldr инструкцию. Что может быть более интересным, так это зациклить вызов и посмотреть, кэшируется ли адрес в регистре.





noinlineconst__attribute__((noinline)) int square(int num)
{
return num * num;
}
Что мне не хватает? Есть ли способ надежно реализовать это без платить за снижение производительности, описанное выше?
Боюсь, вы ничего не сможете с этим поделать. Скорее всего, оно никогда не будет отсортировано. если для вас это важно, вы можете использовать clang: https://godbolt.org/z/T4bznYE4h
Спасибо за ответ. Действительно, я хотел избежать встраивания в этот атрибут, так что спасибо и за это. Что касается потока оптимизатора GCC, не могли бы вы дать мне несколько советов для его дальнейшего исследования? Вероятно, какая-то проблематическая ссылка или ключевые слова. еще раз спасибо
@user3387106: user3387106: В зависимости от того, что вы хотите предотвратить, вам может потребоваться __attribute__((noinline,noipa)) полностью предотвратить любой межпроцедурный анализ (например, функция является чистой, поэтому вызов все равно можно пропустить, если возвращаемое значение не используется, или создать собственное соглашение о вызовах, например, вызывающая сторона пользуется тем фактом, что функция не затирает регистр, который в ABI обычно затирается вызовом.) Атрибут noclone может быть важен для того, чтобы не позволить GCC изобретать версию функции с одним или больше аргументов распространяется константно, но noipa покрывает это.
Вы заботитесь о производительности, но функциональность определенно не оптимальна