回答这个问题的最佳方法是查看反汇编(稍微修改的示例):
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”函数的代码地址。
- 来自堆栈
安装/拆卸:是的。从性能的角度来看,有一个
如上所述,略有差异。
- 没有分支,所以这里没有区别。
Edit:如果我们将分支插入到上面的示例中,不需要很长时间就能穷尽有趣的场景,所以我将在这里解决它们:
例如,在加载(或赋值)函数指针之前我们有一个分支的情况(在伪汇编中):
branch zero foobar
lea square
call ptr
Then we could有区别。假设管道选择加载并开始处理指令foobar
,然后当它意识到我们实际上并不打算采用该分支时,它必须停止以加载函数指针并取消引用它。如果我们只是呼叫一个已知地址,那么就不会出现停顿。
情况二:
lea square
branch zero foobar
call ptr
在这种情况下,直接调用与通过函数指针调用之间不会有任何区别,因为我们需要的一切都已经知道处理器是否开始沿着错误的路径执行,然后重置以开始执行调用。
第三种情况是分支跟随调用,从管道的角度来看,这显然不是很有趣,因为我们已经执行了子例程。
所以要完全重新回答问题3,我会说是的,有区别。但真正的问题是编译器/优化器是否足够聪明来移动分支after函数指针赋值,因此属于情况 2 而不是情况 1。