虚函数是一种成员函数,其行为可以在派生类中被覆盖,支持动态调用派发。
使用示例代码如下:
extern "C" {
// 避免 operator<< 多次调用,简化汇编代码
void println(const char *s) { std::cout << s << std::endl; }
}
void *operator new(size_t n) {
void *p = malloc(n);
std::cout << "new " << n << " ptr " << p << std::endl;
return p;
}
void operator delete(void *ptr) noexcept {
std::cout << "delete " << ptr << std::endl;
free(ptr);
}
struct Fd {
Fd() : fd_(12345) {}
virtual ~Fd() { println("Fd::~Fd"); }
virtual void close() { println("Fd::close"); }
void setsockopt() { println("Fd::setsockopt"); }
int fd_;
};
struct Conn : public Fd {
virtual ~Conn() { println("Conn::~Conn"); }
virtual void connect() { println("Conn::connect"); }
};
struct TCPConn : public Conn {
~TCPConn() { println("TCPConn::~TCPConn"); }
void connect() override { println("TCPConn::connect"); }
void local_addr() {}
};
struct UDPConn : public Conn {
~UDPConn() { println("UDPConn::~UDPConn"); }
void connect() override { println("UDPConn::connect"); }
void local_addr() {}
};
typedef void (*Fn)(void *);
template <typename T>
void call_by_vtable(T *v, size_t idx) {
uintptr_t *vtable_ptr = reinterpret_cast<uintptr_t *>(*reinterpret_cast<uintptr_t *>(v));
// std::cout << "vtable address " << vtable_ptr << std::endl;
void *vtable_entry_ptr = reinterpret_cast<void *>(*(vtable_ptr + idx));
// std::cout << " vtable entry " << idx << "th address " << vtable_entry_ptr << std::endl;
Fn f = (Fn)vtable_entry_ptr;
f(v);
}
int main() {
Fd *conn = new TCPConn;
conn->close();
conn->setsockopt();
delete conn;
println("");
conn = new UDPConn;
call_by_vtable(conn, 1);
}
虚表
虚表是在编译期进行生成,在构造函数内设置对象的虚表地址,先于成员对象的初始化。
通过反汇编 TCPConn 相关代码,得到虚表的定义如下(比实际的要大一些,对象的虚表起始地址一般有两个偏移)
.weak _ZTV7TCPConn
.section .data.rel.ro.local._ZTV7TCPConn,"awG",@progbits,_ZTV7TCPConn,comdat
.align 8
.type _ZTV7TCPConn, @object
.size _ZTV7TCPConn, 48
_ZTV7TCPConn:
.quad 0
.quad _ZTI7TCPConn
.quad _ZN7TCPConnD1Ev
.quad _ZN7TCPConnD0Ev
.quad _ZN2Fd5closeEv
.quad _ZN7TCPConn7connectEv
虚表大小为 48 (_.size ZTV7TCPConn, 48),分别是
- 0 填充
- _ZTI7TCPConn 构造函数
- _ZN7TCPConnD1Ev 普通的析构函数函数
- _ZN7TCPConnD0Ev 调用 delete 的析构函数
- _ZN2Fd5closeEv 成员函数 close,继承自父类
- _ZN7TCPConn7connectEv 成员函数 connect
构造函数的汇编代码如下
.section .text._ZN7TCPConnC2Ev,"axG",@progbits,_ZN7TCPConnC5Ev,comdat
.align 2
.weak _ZN7TCPConnC2Ev
.type _ZN7TCPConnC2Ev, @function
_ZN7TCPConnC2Ev:
.LFB1796:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movq %rdi, -8(%rbp)
movq -8(%rbp), %rax
movq %rax, %rdi
call _ZN4ConnC2Ev
leaq 16+_ZTV7TCPConn(%rip), %rdx
movq -8(%rbp), %rax
movq %rdx, (%rax)
nop
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1796:
.size _ZN7TCPConnC2Ev, .-_ZN7TCPConnC2Ev
.weak _ZN7TCPConnC1Ev
.set _ZN7TCPConnC1Ev,_ZN7TCPConnC2Ev
- 调用父类构造函数
- 设置虚表指针,将虚表第三个条目的地址(_ZTV7TCPConn+16)也就是 _ZN7TCPConnD1Ev 的地址存在 this 指针的内存中 movq %rdx, (%rax)。
leaq 16+_ZTV7TCPConn(%rip), %rdx
movq -8(%rbp), %rax
movq %rdx, (%rax)
虚函数调用
虚函数的调用需要查询虚函数表,找到对应的函数地址进行调用。
使用 conn->close() 来说明,其部分反汇编代码如下:
movq -24(%rbp), %rax ; this 指针
movq (%rax), %rax ; 解地址引用,取到对象虚表地址,地址为 _ZN7TCPConnD1Ev
addq $16, %rax ; 取对象虚表的第三个条目
movq (%rax), %rdx ; 解地址引用得到 _ZN2Fd5closeEv
movq -24(%rbp), %rax
movq %rax, %rdi ; 将 this 指针作为第一个参数
call *%rdx ; 调用 Fd::close
非虚函数调用
普通函数调用 conn->setsockopt(); 无额外动作,其汇编代码如下
movq -24(%rbp), %rax
movq %rax, %rdi
call _ZN2Fd10setsockoptEv
析构函数
TCPConn 反汇编看到实现为两个函数,
- 普通的析构函数
- 额外调用 delete 的析构函数
为保持可读性,删除了部分 cfi 等内容,一下为汇编代码
.align 2
.weak _ZN7TCPConnD2Ev
.type _ZN7TCPConnD2Ev, @function
_ZN7TCPConnD2Ev:
.LFB1779:
pushq %rbp
movq %rsp, %rbp
subq $16, %rsp
movq %rdi, -8(%rbp)
leaq 16+_ZTV7TCPConn(%rip), %rdx
movq -8(%rbp), %rax
movq %rdx, (%rax)
leaq .LC7(%rip), %rax
movq %rax, %rdi
call println
movq -8(%rbp), %rax
movq %rax, %rdi
call _ZN4ConnD2Ev
nop
leave
ret
.weak _ZN7TCPConnD1Ev
.set _ZN7TCPConnD1Ev,_ZN7TCPConnD2Ev
.type _ZN7TCPConnD0Ev, @function
_ZN7TCPConnD0Ev:
.LFB1781:
pushq %rbp
movq %rsp, %rbp
subq $16, %rsp
movq %rdi, -8(%rbp)
movq -8(%rbp), %rax
movq %rax, %rdi
call _ZN7TCPConnD1Ev
movq -8(%rbp), %rax
movl $16, %esi
movq %rax, %rdi
call _ZdlPvm@PLT
leave
ret
析构函数的实现和虚成员函数没有区别,都是偏移查表然后调用。
movq (%rax), %rdx
addq $8, %rdx
movq (%rdx), %rdx
movq %rax, %rdi
call *%rdx
注意这里偏移为 8,也就是将调用 _ZN7TCPConnD0Ev。
观察 _ZN7TCPConnD0Ev 的实现,先调用普通的析构函数 _ZN7TCPConnD1Ev ,然后再调用 _ZdlPvm@PLT 也就是 operator delete。
在析构函数的调用链中,不再进行查表的动作,也就是说整个 delete 过程只需要查一次虚表,两次解指针引用
- 获取虚表地址
- 获取虚函数地址
手动通过 this 指针来调用虚函数
针对以上代码,使用 C++ 代码来手动调用虚函数.
typedef void (*Fn)(void *);
template <typename T>
void call_by_vtable(T *v, size_t idx) {
uintptr_t *vtable_ptr = reinterpret_cast<uintptr_t *>(*reinterpret_cast<uintptr_t *>(v));
// std::cout << "vtable address " << vtable_ptr << std::endl;
void *vtable_entry_ptr = reinterpret_cast<void *>(*(vtable_ptr + idx));
// std::cout << " vtable entry " << idx << "th address " << vtable_entry_ptr << std::endl;
Fn f = (Fn)vtable_entry_ptr;
f(v);
}
T *v 是对象地址,也就是 this 指针。两次解指针引用获取到虚表中第 idx 个虚函数地址。由于示例中所有代码都是无参函数,所以直接传入 this 指针即可进行调用。
测试手动执行虚析构函数
int main() {
Fd *conn = new TCPConn;
conn->close();
conn->setsockopt();
delete conn;
println("");
conn = new UDPConn;
call_by_vtable(conn, 1);
}
可以看到 delete 和 手动寻址调用 的输出相同
new 16 ptr 0x560d34c13eb0
Fd::close
TCPConn::~TCPConn
Conn::~Conn
Fd::~Fd
delete 0x560d34c13eb0
new 16 ptr 0x560d34c13eb0
UDPConn::~UDPConn
Conn::~Conn
Fd::~Fd
delete 0x560d34c13eb0
虚函数性能
使用简单的一个用例来测试两个函数的性能
static void BM_call_normal_fn(benchmark::State &state) {
Fd *conn = new TCPConn;
for (auto _ : state)
conn->setsockopt();
delete conn;
}
static void BM_call_virtual_fn(benchmark::State &state) {
Fd *conn = new TCPConn;
for (auto _ : state)
conn->close();
delete conn;
}
未开启优化的情况下,虚函数的性能大概比普通函数低 30% 左右。
2024-05-15T09:34:23+08:00
Running ./benchmark
Run on (20 X 4800 MHz CPU s)
CPU Caches:
L1 Data 48 KiB (x10)
L1 Instruction 32 KiB (x10)
L2 Unified 1280 KiB (x10)
L3 Unified 25600 KiB (x1)
Load Average: 0.37, 0.19, 0.08
***WARNING*** CPU scaling is enabled, the benchmark real time measurements may be noisy and will incur extra overhead.
-------------------------------------------------------------
Benchmark Time CPU Iterations
-------------------------------------------------------------
BM_call_normal_fn 1.35 ns 1.35 ns 516029631
BM_call_virtual_fn 1.74 ns 1.74 ns 410454516
使用 O2 的优化选项后,因为是非常简单的函数,普通函数直接被优化了,虚函数相比没有开优化也有提升。
-------------------------------------------------------------
Benchmark Time CPU Iterations
-------------------------------------------------------------
BM_call_normal_fn 0.000 ns 0.000 ns 1000000000000
BM_call_virtual_fn 0.309 ns 0.309 ns 2213725270
优化的点在于直接把空函数消除,但是虚函数调用那一套绕不过去,只是消除了部分 call 指令。
截取部分 benchmark 的汇编代码,setsockopt 完全被消除,只剩下 new/delete (也就是 _Znwm@PLT 和 _ZdlPvm@PLT)
.L241:
movq %rbp, %rdi
call _Znwm@PLT
movq 16(%rbx), %rdx
movq %rax, %rcx
testq %rdx, %rdx
jne .L251
.L242:
movq %rcx, 8(%rbx)
addq $32, %rbx
movq %rbp, -8(%rbx)
cmpq %rbx, %r12
je .L239
addq $8, %rsp
.cfi_remember_state
.cfi_def_cfa_offset 40
movq %r13, %rsi
movq %r12, %rdi
popq %rbx
.cfi_def_cfa_offset 32
popq %rbp
.cfi_def_cfa_offset 24
popq %r12
.cfi_def_cfa_offset 16
popq %r13
.cfi_def_cfa_offset 8
jmp _ZdlPvm@PLT
虚函数 close 的反汇编代码如下,可以看到虚函数也被优化了,但是查表的过程被保留。
call _Znwm@PLT
movl $12345, 8(%rax)
movq %rax, %rbx
leaq 16+_ZTV7TCPConn(%rip), %rax
movq %rax, (%rbx)
movl 28(%rbp), %eax
testl %eax, %eax
je .L253
movq %rbp, %rdi
call _ZN9benchmark5State16StartKeepRunningEv@PLT
.L254:
movq %rbp, %rdi
call _ZN9benchmark5State17FinishKeepRunningEv@PLT
movq (%rbx), %rax
leaq _ZN7TCPConnD0Ev(%rip), %rdx
movq 8(%rax), %rax
cmpq %rdx, %rax
jne .L255
movq %rbx, %rdi
movl $16, %esi
popq %rbx
.cfi_remember_state
.cfi_def_cfa_offset 24
popq %rbp
.cfi_def_cfa_offset 16
popq %r12
.cfi_def_cfa_offset 8
jmp _ZdlPvm@PLT
.p2align 4,,10
.p2align 3
.L253:
.cfi_restore_state
movq 16(%rbp), %r12
movq %rbp, %rdi
call _ZN9benchmark5State16StartKeepRunningEv@PLT
testq %r12, %r12
jns .L254
所以借助编译器的优化,除非是在CPU密集型的地方,否则虚函数该用还是用,性能损失并不大。
标签:函数,rdx,rbp,call,探虚,rax,movq From: https://www.cnblogs.com/shuqin/p/18204073