您在速度上有所提高,但在可读性和维护方面有所损失。而不是 if-then-else 树,如果 a 然后 fun_a(),else if b 然后 fun_b(),else if c 然后 fun_c() else fun_default(),并且每次都必须这样做,而不是 if a then fun =fun_a, else if b then 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 );
无符号整数(* funptr)(无符号整数,无符号整数);
void have_fun(无符号整数 x,无符号整数 y,无符号整数 z)
{
无符号整数 j;
乐趣=乐趣1;
j=fun1(z,5);
j=funptr(y,6);
}
编译给出了这个:
玩得开心:
stmfd sp!, {r3, r4, r5, lr}
.save {r3, r4, r5, lr}
ldr r4, .L2
移动 r5, r1
移动 r0, r2
移动 r1, #5
ldr r2, .L2+4
str r2, [r4, #0]
乐趣1
ldr r3, [r4, #0]
移动 r0, r5
移动 r1, #6
blx r3
ldmfd sp!, {r3, r4, r5, pc}
我认为克利福德所说的是直接呼叫,如果附近
足够(取决于架构),是一条指令
乐趣1
一个函数指针,至少要花你两个
ldr r3, [r4, #0]
blx r3
我还提到了直接和间接之间的区别是
你产生的额外负担。
在继续之前,值得一提的是内联的优缺点。
对于这些示例所使用的 ARM,调用
约定使用 r0-r3 作为函数的传入参数和 r0
返回。所以用三个参数进入 have_fun() 意味着 r0-r3
有内容。对于 ARM,还假设一个函数可以破坏
r0-r3,因此 have_fun() 需要保留输入,然后将
r0 和 r1 中 fun1() 的两个输入,因此发生了一些寄存器舞蹈。
移动 r5, r1
移动 r0, r2
移动 r1, #5
ldr r2, .L2+4
str r2, [r4, #0]
乐趣1
编译器足够聪明,可以看出我们从来不需要第一个
have_fun() 函数的输入,因此 r0 被丢弃并允许
马上改。编译器也很聪明,知道
发送后我们永远不需要第三个参数 z (r2)
在第一次调用时将其发送到 fun1(),因此无需将其保存在高位
登记。虽然 R1,have_fun() 的第二个参数确实需要
被保存,所以它被放在一个不会被 fun1() 破坏的注册器中。
你可以看到第二个函数调用发生了同样的事情。
假设 fun1() 是这个简单的函数:
内联 unsigned int fun1 ( unsigned int a, unsigned int b )
{
返回(a+b);
}
当你内联 fun1() 时,你会得到这样的结果:
stmfd sp!, {r4, lr}
移动 r0, r1
移动 r1, #6
添加 r4, r2, #5
编译器不需要将低位寄存器洗牌
准备打电话。同样,您可能已经注意到 r4 和
当我们进入 hello_fun() 时,lr 被保存在堆栈中。有了这个
ARM 调用约定一个函数可以破坏 r0-r3 但必须保留
所有其他寄存器,因为在这种情况下 have_fun() 需要更多
比四个寄存器做它的事情它保存了 r4 的内容
堆栈以便它可以使用它。同样,我编译的这个函数
它确实调用了另一个函数,bl/blx 指令使用/销毁
lr register (r14) 所以为了让 have_fun() 返回我们也有
将 lr 保留在堆栈上。 fun1() 的简化示例
不显示这一点,但您从内联中获得的另一个节省是在进入时
调用的函数不必设置堆栈帧并保留
寄存器,就好像你从函数中获取代码一样
将其与调用函数内联。
你为什么不一直内联?首先它可以并且将使用
更多的寄存器,这会导致更多的堆栈使用,而且堆栈很慢
相对于寄存器。最重要的是,它增加了
你的二进制文件的大小,如果 fun1() 是一个大小合适的函数并且你调用了
在 have_fun() 中它 20 次,你的二进制文件会大得多。为了
具有千兆字节内存的现代计算机,几百或几十万
字节没什么大不了的,但是对于资源有限的嵌入式,这可以
成就或毁掉你。在现代千兆赫多核桌面上,多久
无论如何,你需要刮一个或五个指令吗?有时是但
并非所有功能都适用。所以只是因为你可能可以
在您可能不应该在桌面上使用它。
回到函数指针。所以我试图用我的
答案是,您可能希望在什么情况下使用函数指针
无论如何,用例是什么,在这些用例中有多少
有帮助还是有伤害?
我想到的案例类型是插件,或者特定于
响应特定硬件的调用参数或通用代码
检测到。例如,一个假设的 tar 程序可能想要输出
到磁带驱动器、文件系统或其他,您可以选择将
具有使用函数指针调用的通用函数的代码。入境时
对于程序,命令行参数指示输出,并且在
那一点您将函数指针设置为特定于设备的
功能。
如果(outdev==OUTDEV_TAPE)data_out=data_out_tape;
否则如果(outdev==OUTDEV_FILE)
{
//打开文件等
数据输出=数据输出文件;
}
...
或者也许你不知道你是否在一个处理器上运行
fpu 或您拥有的 fpu 类型,但您知道浮点除法
你想做的事情可以使用 fpu 运行得更快:
如果(fputype==FPU_FPA)fdivide=fdivide_fpa;
否则如果(fputype==FPU_VFP)fdivide=fdivide_vfp;
否则 fdivide=fdivide_soft;
而且你绝对可以使用 case 语句代替 if-then-else
树,各有优缺点,一些编译器将 case 语句转换为 int
无论如何都是一棵 if-then-else 树,所以它并不总是重要的。我的重点
试图做的是,如果你这样做一次:
如果(fputype==FPU_FPA)fdivide=fdivide_fpa;
否则如果(fputype==FPU_VFP)fdivide=fdivide_vfp;
否则 fdivide=fdivide_soft;
并且在程序的其他任何地方都这样做:
a=fdivide(b,c);
与您每次都执行此操作的非函数指针替代方案相比
你想划分的地方:
if(fputype==FPU_FPA) a=fdivide_fpa(b,c);
否则 if(fputype==FPU_VFP) a=fdivide_vfp(b,c);
否则 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);
无符号整数(* funptr)(无符号整数,无符号整数);
unsigned int have_fun ( unsigned int x, unsigned int y, unsigned int z )
{
无符号整数 j;
开关(x)
{
默认:
案例1:j=fun1(y,z);休息;
案例2:j=fun2(y,z);休息;
案例3:j=fun3(y,z);休息;
}
返回(j);
}
unsigned int more_fun ( unsigned int x, unsigned int y, unsigned int z )
{
无符号整数 j;
j=funptr(y,z);
返回(j);
}
给我们这个:
cmp r0, #2
贝克.L3
cmp r0, #3
贝克.L4
移动 r0, r1
移动 r1, r2
乐趣1
.L3:
移动 r0, r1
移动 r1, r2
乐趣2
.L4:
移动 r0, r1
移动 r1, r2
乐趣3
而不是这个
移动 r0, r1
ldr r3, .L7
移动 r1, r2
blx r3
对于默认情况,if-then-else 树会烧掉两个比较和两个
beq 在直接调用函数之前。基本上有时
if-then-else 树会更快,有时函数指针会更快
更快。
我提出的另一条评论是,如果你使用内联来做到这一点
if-then-else 树更快,而不是函数指针,内联是
总是更快吧?
unsigned int fun1 ( unsigned int a, unsigned int b )
{
返回(a+b);
}
unsigned int fun2 ( unsigned int a, unsigned int b )
{
返回(a-b);
}
unsigned int fun3 ( unsigned int a, unsigned int b )
{
返回(a&b);
}
unsigned int have_fun ( unsigned int x, unsigned int y, unsigned int z )
{
无符号整数 j;
开关(x)
{
默认:
案例1:j=fun1(y,z);休息;
案例2:j=fun2(y,z);休息;
案例3:j=fun3(y,z);休息;
}
返回(j);
}
给予
玩得开心:
cmp r0, #2
rsbeq r0, r2, r1
bxeq lr
cmp r0, #3
添加 r0, r2, r1
与 r0, r2, r1
bx lr
大声笑,ARM 让我参与其中。那很好。你可以想象虽然
对于通用处理器,您会得到类似的东西
cmp r0, #2
贝克.L3
cmp r0, #3
贝克.L4
和 r0,r1,r2
bx lr
.L3:
子 r0,r1,r2
bx lr
.L4:
添加 r0,r1,r2
bx lr
你仍然烧掉比较,你拥有的案例越多,时间越长
if-then-else 树。一般情况下不需要太多
比函数指针解决方案更长。
移动 r0, r1
ldr r1, .L7
ldr r3,[r1]
移动 r1, r2
blx r3
然后我还提到了可读性和维护性,使用函数
指针方法您需要始终注意是否
函数指针在使用前已分配。你不能
总是只是 grep 为那个函数名并找到你正在寻找的东西
因为在别人的代码中,理想情况下你会找到一个地方
指针已分配,然后您可以 grep 获取真正的函数名称。
是的,函数指针还有许多其他用例,并且
我所描述的可以通过许多其他方式解决,高效
或不。我试图给海报一些关于如何思考的想法
通过不同的场景。
我认为这个面试问题最重要的答案不是
有一个正确或错误的答案,因为我认为没有。
但是要看看受访者对编译器做什么或不做什么的了解
做,我上面描述的那种事情。面试题
对我来说是几个问题,你了解编译器实际上是什么吗
确实,它生成什么指令。你明白吗
更少或更多指令不一定更快。你明白吗
不同处理器之间的这些差异,或者你至少有
至少一个处理器的工作知识。然后它继续
可读性和维护性。这是另一个问题
与您阅读他人代码的经验有关,并且
然后维护您自己的代码或其他人的代码。这是一个巧妙的
我认为设计的问题。