回答这个问题的最好方法是查看反汇编(稍作修改的示例):
fptr1 = □
int result1 = fptr1(5);
int result2 = square(5);
这个 x64 asm 的结果:
fptr1 = □
000000013FA31A61 lea rax,[square (013FA31037h)]
000000013FA31A68 mov qword ptr [fptr1 (013FA40290h)],rax
int result1 = fptr1(5);
000000013FA31A6F mov ecx,5
000000013FA31A74 call qword ptr [fptr1 (013FA40290h)]
000000013FA31A7A mov dword ptr [result1],eax
int result2 = square(5);
000000013FA31A7E mov ecx,5
000000013FA31A83 call square (013FA31037h)
000000013FA31A88 mov dword ptr [result2],eax
如您所见,程序集在直接调用函数和通过指针调用函数之间几乎相同。在这两种情况下,CPU 都需要访问代码所在的位置并调用它。直接调用的好处是不必取消引用指针(因为偏移量将被烘焙到程序集中)。
- 是的,您可以在函数指针的分配中看到,它存储了“square”函数的代码地址。
- 从堆栈设置/拆卸:是的。从性能的角度来看,如上所述,存在细微差别。
- 没有分支,所以这里没有区别。
编辑:如果我们要在上面的示例中插入分支,用完有趣的场景不会花费很长时间,所以我将在这里解决它们:
如果我们在加载(或赋值)函数指针之前有一个分支,例如(在伪汇编中):
branch zero foobar
lea square
call ptr
那我们就可以有所不同了。假设管道选择在 处加载并开始处理指令foobar
,那么当它意识到我们实际上并不打算采用该分支时,它必须停止以加载函数指针并取消引用它。如果我们只是呼叫一个已知地址,那么就不会有一个摊位。
案例二:
lea square
branch zero foobar
call ptr
在这种情况下,直接调用与通过函数指针调用之间没有任何区别,因为我们需要的一切都已经知道处理器是否开始沿着错误的路径执行然后重置以开始执行调用。
第三种情况是分支跟随调用,从管道的角度来看,这显然不是很有趣,因为我们已经执行了子例程。
因此,要完全重新回答问题3,我会说是的,这是有区别的。但真正的问题是编译器/优化器是否足够聪明,可以在函数指针分配后移动分支,所以它属于案例 2 而不是案例 1。