Вы выигрываете в скорости, но теряете в читабельности и обслуживании.Вместо дерева if-then-else, если a затем fun_a (), иначе, если b, то fun_b (), еще, если c, тогда fun_c (), иначе fun_default (), и приходится делать это каждый раз, вместо этого, если тогда a fun= fun_a, иначе, если b, тогда fun = fun_b и т. д., и вы делаете это один раз, с этого момента просто вызывайте fun ().Намного быстрее.Как уже указывалось, вы не можете встроить, что является еще одним приемом скорости, но встраивание в дерево if-then-else не обязательно делает его быстрее, чем без встраивания, и в целом не так быстро, как указатель на функцию.
Вы теряете немного читабельности и обслуживания, потому что вам нужно выяснить, где установлена функция fun (), как часто она меняется, если вообще когда-либо, убедитесь, что вы не вызываете ее до установки, но она по-прежнему доступна для поиска.имя, которое вы можете использовать, чтобы найти и сохранить все места, где оно используется.
Это в основном трюк скорости, позволяющий избегать деревьев if-then-else каждый раз, когда вы хотите выполнить функцию.Если производительность не критична, если ничто иное, fun () не может быть статическим и иметь в нем дерево if-then-else.
РЕДАКТИРОВАТЬ Добавление нескольких примеров для объяснения того, о чем я говорил.
extern unsigned int fun1 ( unsigned int a, unsigned int b );
unsigned int (*funptr)(unsigned int, unsigned int);
void have_fun ( unsigned int x, unsigned int y, unsigned int z )
{
unsigned int j;
funptr=fun1;
j=fun1(z,5);
j=funptr(y,6);
}
Компиляция дает следующее:
have_fun:
stmfd sp!, {r3, r4, r5, lr}
.save {r3, r4, r5, lr}
ldr r4, .L2
mov r5, r1
mov r0, r2
mov r1, #5
ldr r2, .L2+4
str r2, [r4, #0]
bl fun1
ldr r3, [r4, #0]
mov r0, r5
mov r1, #6
blx r3
ldmfd sp!, {r3, r4, r5, pc}
Я предполагаю, что Клиффорд говорил о том, чтопрямой вызов, если он достаточно близок (в зависимости от архитектуры), - это одна инструкция
bl fun1
, где указатель на функцию обойдется вам как минимум в два
ldr r3, [r4, #0]
blx r3
Я такжеупомянул разницу между прямой и косвенной дополнительной нагрузкой.
Прежде чем двигаться дальше, стоит упомянуть плюсы и минусы навески.В случае ARM, который используют эти примеры, соглашение о вызовах использует r0-r3 для входящих параметров функции и r0 для возврата.Таким образом, вход в have_fun () с тремя параметрами означает, что r0-r3 имеют содержимое.В ARM также предполагается, что функция может уничтожить r0-r3, поэтому have_fun () необходимо сохранить входы и затем поместить два входа в fun1 () в r0 и r1, так что происходит немного танца регистра.
mov r5, r1
mov r0, r2
mov r1, #5
ldr r2, .L2+4
str r2, [r4, #0]
bl fun1
Компилятор был достаточно умен, чтобы понять, что нам никогда не требовался первый ввод для функции have_fun (), поэтому r0 был отброшен и разрешен для немедленного изменения.Кроме того, компилятор был достаточно умен, чтобы знать, что нам никогда не понадобится третий параметр, z (r2), после отправки его в fun1 () при первом вызове, поэтому ему не нужно сохранять его в старшем регистре.R1, тем не менее, второй параметр функции have_fun () должен быть сохранен, поэтому он помещается в регистр, который не будет уничтожен fun1 ().
Вы можете видеть, что то же самое происходит со второй функциейcall.
Предполагая, что fun1 () - это простая функция:
inline unsigned int fun1 ( unsigned int a, unsigned int b )
{
return(a+b);
}
Когда вы вставляете fun1 (), вы получаете что-то вроде этого:
stmfd sp!, {r4, lr}
mov r0, r1
mov r1, #6
add r4, r2, #5
Компилятор делаетне нужно перетасовывать нижние регистры о готовящемся звонке.Также вы могли заметить, что r4 и lr сохраняются в стеке, когда мы входим в hello_fun ().С этим соглашением о вызовах ARM функция может уничтожить r0-r3, но должна сохранить все остальные регистры, поскольку в этом случае для функции have_fun () в этом случае требуется более четырех регистров, она сохраняет содержимое r4 в стеке, чтобы она могла использоватьЭто.Аналогично, эта функция, когда я ее компилировал, вызывала другую функцию, инструкция bl / blx использует / уничтожает регистр lr (r14), поэтому для возврата функции have_fun () мы также должны сохранить lr в стеке.Упрощенный пример для fun1 () не показал этого, но другая экономия, которую вы получаете от встраивания, заключается в том, что при входе вызываемая функция не должна устанавливать кадр стека и сохранять регистры, это действительно так, как если бы вы взяли код из функциии вставил его в строку с вызывающей функцией.
Почему бы вам не вставлять все время?Ну, во-первых, он может и будет использовать больше регистров, что может привести к большему использованию стека, и стек будет медленным по сравнению с регистрами.Самое главное, что это увеличиваетразмер вашего двоичного файла, если fun1 () была функцией хорошего размера, и вы вызвали
это в 20 раз в have_fun () ваш двоичный файл будет значительно больше. За
современные компьютеры с гигабайтами оперативной памяти, несколько сотен или несколько десятков тысяч
байты не имеют большого значения, но для встроенных с ограниченными ресурсами это может
сделать или сломать тебя. На современном гигагерцовом многоядерном рабочем столе, как часто
тебе нужно побрить инструкцию или пять? Иногда да но
не все время для каждой функции. Так что только потому, что вы, вероятно, можете
сойти с рук на рабочем столе, вероятно, не стоит.
Вернуться к указателям на функции. Таким образом, момент, который я пытался сделать с моим
ответ, в каких ситуациях вы бы хотели использовать указатель на функцию
во всяком случае, каковы случаи использования и в этих случаях использования сколько
это помогает или больно?
Типами случаев, о которых я думал, являются плагины или код, специфичный для
вызывающий параметр или общий код, реагирующий на конкретное оборудование
обнаружено. Например, гипотетическая программа tar может захотеть вывести
на ленточный накопитель, в файловую систему или другое, и вы можете написать
код с обобщенными функциями, вызываемыми с помощью указателей на функции. При входе
в программе параметры командной строки указывают на вывод и при
в этот момент вы устанавливаете указатели функций для конкретного устройства
функции.
if(outdev==OUTDEV_TAPE) data_out=data_out_tape;
else if(outdev==OUTDEV_FILE)
{
//open the file, etc
data_out=data_out_file;
}
...
Или, возможно, вы не знаете, работаете ли вы на процессоре с
FPU или какой тип FPU у вас есть, но вы знаете, что деление с плавающей запятой
Вы можете сделать это может работать намного быстрее, используя fpu:
if(fputype==FPU_FPA) fdivide=fdivide_fpa;
else if(fputype==FPU_VFP) fdivide=fdivide_vfp;
else fdivide=fdivide_soft;
И, конечно, вы можете использовать оператор case вместо if-then-else
дерево, плюсы и минусы каждому, некоторые компиляторы превращают оператор
дерево if-then-else в любом случае, так что это не всегда имеет значение. Дело в том, что я
пытался сделать, если вы делаете это один раз:
if(fputype==FPU_FPA) fdivide=fdivide_fpa;
else if(fputype==FPU_VFP) fdivide=fdivide_vfp;
else fdivide=fdivide_soft;
И делайте это везде в программе:
a=fdivide(b,c);
По сравнению с альтернативой без указателя функции, где вы делаете это каждый
где вы хотите разделить:
if(fputype==FPU_FPA) a=fdivide_fpa(b,c);
else if(fputype==FPU_VFP) a=fdivide_vfp(b,c);
else a=fdivide_soft(b,c);
Подход с использованием указателя на функцию, даже если это стоит вам дополнительного ldr
на каждый звонок, намного дешевле, чем много инструкций, необходимых для
дерево if-then-else. Вы платите немного вперед, чтобы настроить fdivide
указатель один раз, а затем платить дополнительный LDR в каждом случае, но в целом
это быстрее чем это:
unsigned int fun1 ( unsigned int a, unsigned int b );
unsigned int fun2 ( unsigned int a, unsigned int b );
unsigned int fun3 ( unsigned int a, unsigned int b );
unsigned int (*funptr)(unsigned int, unsigned int);
unsigned int have_fun ( unsigned int x, unsigned int y, unsigned int z )
{
unsigned int j;
switch(x)
{
default:
case 1: j=fun1(y,z); break;
case 2: j=fun2(y,z); break;
case 3: j=fun3(y,z); break;
}
return(j);
}
unsigned int more_fun ( unsigned int x, unsigned int y, unsigned int z )
{
unsigned int j;
j=funptr(y,z);
return(j);
}
дает нам это:
cmp r0, #2
beq .L3
cmp r0, #3
beq .L4
mov r0, r1
mov r1, r2
b fun1
.L3:
mov r0, r1
mov r1, r2
b fun2
.L4:
mov r0, r1
mov r1, r2
b fun3
вместо этого
mov r0, r1
ldr r3, .L7
mov r1, r2
blx r3
В случае по умолчанию дерево if-then-else записывает два сравнения и два
перед вызовом функции напрямую. В основном иногда
Дерево if-then-else будет быстрее и иногда указатель на функцию
быстрее.
Еще один комментарий, который я сделал, это то, что если бы вы использовали встраивание, чтобы сделать это
дерево if-then-else быстрее, вместо указателя функции, вставка
всегда быстрее, верно?
unsigned int fun1 ( unsigned int a, unsigned int b )
{
return(a+b);
}
unsigned int fun2 ( unsigned int a, unsigned int b )
{
return(a-b);
}
unsigned int fun3 ( unsigned int a, unsigned int b )
{
return(a&b);
}
unsigned int have_fun ( unsigned int x, unsigned int y, unsigned int z )
{
unsigned int j;
switch(x)
{
default:
case 1: j=fun1(y,z); break;
case 2: j=fun2(y,z); break;
case 3: j=fun3(y,z); break;
}
return(j);
}
дает
have_fun:
cmp r0, #2
rsbeq r0, r2, r1
bxeq lr
cmp r0, #3
addne r0, r2, r1
andeq r0, r2, r1
bx lr
LOL, ARM помог мне в этом. Это мило. Вы можете себе представить, хотя
для универсального процессора вы получите что-то вроде
cmp r0, #2
beq .L3
cmp r0, #3
beq .L4
and r0,r1,r2
bx lr
.L3:
sub r0,r1,r2
bx lr
.L4:
add r0,r1,r2
bx lr
Вы все еще записываете сравнение, чем больше у вас дел, тем дольше
дерево если-то-еще. Для среднего случая не требуется много
длиннее, чем указатель на функцию.
mov r0, r1
ldr r1, .L7
ldr r3,[r1]
mov r1, r2
blx r3
Тогда я также упомянул читабельность и обслуживание, используя функцию
Подход указателя вы должны всегда знать, или нет
указатель функции был назначен перед его использованием. Ты не можешь
всегда просто grep для этого имени функции и найти то, что вы ищете
для кого-то другого кода, в идеале вы найдете одно место, где это
указатель назначен, тогда вы можете grep для реальных имен функций.
Да, есть много других вариантов использования для указателей функций, и
те, которые я описал, могут быть решены многими другими способами, эффективными
или нет. Я пытался дать плакату некоторые идеи о том, как думать
через разные сценарии.
Я думаю, что самый важный ответ на этот вопрос интервью не
что есть правильный или неправильный ответ, потому что я думаю, что нет.Но чтобы увидеть, что интервьюируемый знает о том, что компиляторы делают или не делают, о вещах, которые я описал выше.Вопрос для меня - это несколько вопросов, понимаете ли вы, что на самом деле делает компилятор, какие инструкции он генерирует.Вы понимаете, что меньше или больше инструкций не обязательно быстрее?понимаете ли вы эти различия между разными процессорами или у вас есть хотя бы практические знания хотя бы для одного процессора?Затем речь идет о удобочитаемости и обслуживании.Это еще один поток вопросов, связанный с вашим опытом чтения кода других людей, а затем поддержки вашего собственного кода или кода других людей.На мой взгляд, это продуманный вопрос.