c++ 函数开销

2022/2/21

调用函数的开销大致可分两个部分:传递参数的开销和保存当前程序上下文信息所花费的开销。对于传递参数的开销而言,传递的参数越多开销就越大;对于保存当前程序上下文所花费的开销而言,函数越复杂需要花费的开销就越大。

# 函数调用约定

函数调用约定,是指当一个函数被调用时,函数的参数会被传递给被调用的函数和返回值会被返回给调用函数。函数的调用约定就是描述参数是怎么传递和由谁平衡堆栈的,当然还有返回值。

  • __stdcall 堆栈平衡是由调用函数来执行
  • __cdecl 堆栈平衡操作是由被调用函数执行的
  • __fastcall 参数应该放在寄存器中,而不是在栈中,最左边的两个不大于4个字节(DWORD)的参数分别放在 ecx 和 edx 寄存器。当寄存器用完的时候,其余参数仍然从右到左的顺序压入堆栈。像浮点值、远指针和 __int64 类型总是通过堆栈来传递的。
  • __thiscall 会使用一个寄存器或在函数中添加一个不可见的参数来传递类指针
  • __pascal 参数从左到右依次入栈
  • __nakedcall 编译器不会生成 prolog 和 epilog 代码
  • __vectorcall 使用的参数寄存器多于 __fastcall 或默认的 x64 调用约定,可传递三种参数:整数类型值,矢量类型值和同构向量聚合(HVA)值。

# 函数类型

# 普通函数

auto [__stdcall, __cdecl] add(int x, int y){
  retrun x + y;
}
1
2
3

C++ 默认调用方式为 __cdecl,其中 __cdecl 可以有可变参数

# 模板函数

template<typename T>
auto add(T x, T y){
  retrun x + y;
}
1
2
3
4

模板函数在实例化之后效率是和普通函数一样的

# 内联函数

内联扩展是一种特别的用于消除调用函数时所造成的固有的时间消耗方法。一般用于能够快速执行的函数,因为在这种情况下函数调用的时间消耗显得更为突出。这种方法对于很小的函数也有空间上的益处,并且它也使得一些其他的优化成为可能。

inline auto add(int x, int y){
  retrun x + y;
}
1
2
3

会直接在调用方展开(如果编译器也认为展开更合适)

auto z = add(x, y);
// 展开等效于
auto z = x + y;
1
2
3

# 虚函数

在某基类中声明为 virtual 并在一个或多个派生类中被重新定义的成员函数,用法格式为:virtual 函数返回类型 函数名(参数表) {函数体};实现多态性,通过指向派生类的基类指针或引用,访问派生类中同名覆盖成员函数。

虚函数因为调用机制(进行查表寻址),一般是不会被编译器优化为内联函数(如果确实可以确定被调用的函数, 如果函数适合内联则内联)

正常的带参数的函数,一般有以下几步:

  1. 用 push 指令将参数入栈,如果是基本类型,有几个参数就需要几条 push 指令
  2. call 指令进入被调用函数,先保存 IP 寄存器的值,再将函数入口地址存入 IP
  3. 被调用函数将返回地址和基址 EBP 压入栈,并分配栈空间需要3条指令 pushl %ebp, movl %esp, %ebp, subl $xx, %esp
  4. 函数返回时恢复栈,需要 leave 和 ret 两条指令,leave 释放栈空间并恢复 EBP,恢复 IP 寄存器的值所以需要 6 条以上的指令开销,如果参数是结构体的话,需要拷贝复制指令,这个开销就大了。

另外,函数调用结束返回的时候有可能需要平衡堆栈,这个我觉得也应该算入函数调用开销里面。

Last Updated: 2023-10-29T08:26:04.000Z