深度解读《深度探索C++对象模型》之虚继承的实现分析和效率评测(二)

news2025/1/21 22:04:03

目录

通过子类的指针存取虚基类成员的实现分析

通过第一基类的指针存取虚基类成员的实现分析

通过第二基类的指针存取虚基类成员的实现分析

通过虚基类的指针存取虚基类成员的实现分析

小结

存取虚基类成员与普通类成员的效率对比


接下来我将持续更新“深度解读《深度探索C++对象模型》”系列,敬请期待,欢迎左下角点击关注!也可以关注公众号:iShare爱分享,或文章末尾扫描二维码,自动获得推文和全部的文章列表。 

阅读这一篇,请结合上一篇一起阅读,因为有些图表在第一篇文章中,这里就没有再贴出来,上一篇请从这里阅读:

        深度解读《深度探索C++对象模型》之虚继承的实现分析和效率评测(一)

例子的代码这里再贴一下,方便查看:

class Grand {
public:
    virtual ~Grand() {}
    int g;
};
class Base1: virtual public Grand {
public:
	int b1;
};
class Base2: virtual public Grand {
public:
	int b2;
};
class Derived: public Base1, public Base2 {
public:
	int d;
};
 
int main() {
    Derived d;
    d.g = 5;
    Derived* pd = &d;
    pd->g = 6;
    Base1* pb1 = &d;
    pb1->g = 7;
    Base2* pb2 = &d;
    pb2->g = 8;
    Grand* pg = &d;
    pg->g = 9;
    
    return 0;
}

通过子类的指针存取虚基类成员的实现分析

        C++代码的第22、23行代码,是通过Derived类型的指针来存取数据成员g,对应的汇编代码:

lea     rax, [rbp - 56]
mov     qword ptr [rbp - 64], rax
mov     rax, qword ptr [rbp - 64]
mov     rcx, qword ptr [rax]
mov     rcx, qword ptr [rcx - 24]
mov     dword ptr [rax + rcx + 8], 6

        前两行是取得对象d的地址然后存放在rbp - 64空间,后面几行是计算得到虚基类的成员g的地址,这个跟上面通过对象来存取的代码是一模一样的。这里看到生成的汇编代码中,通过对象来存取虚基类的成员和通过指针来存取虚基类的成员的代码是一样的,按道理这里是可以有优化的地方的,通过对象来存取的话,因为在编译期间它的类型是确定,所以它的偏移值也是确定的,可以不需要通过虚表来得到偏移值,从而省掉几个步骤,但编译器在这里并没有优化。

通过第一基类的指针存取虚基类成员的实现分析

        C++代码的第24、25行代码,是转换成Base1类型的指针来存取成员g,对应的汇编代码如下:

lea     rax, [rbp - 56]
mov     qword ptr [rbp - 72], rax
mov     rax, qword ptr [rbp - 72]
mov     rcx, qword ptr [rax]
mov     rcx, qword ptr [rcx - 24]
mov     dword ptr [rax + rcx + 8], 7

        因为Base1类是第一继承的父类,所以这个子类部分排在最前面,起始地址和对象d的起始地址一致的,所以上面的代码跟通过Derived类型指针来存取是一模一样的,这里就不再赘述。

通过第二基类的指针存取虚基类成员的实现分析

        C++代码的第26、27行代码是转换成Base2类型的指针来存取成员g,对应的汇编代码如下:

xor     eax, eax
lea     rcx, [rbp - 56]
cmp     rcx, 0
mov     qword ptr [rbp - 96], rax       # 8-byte Spill
je      .LBB0_2
lea     rax, [rbp - 56]
add     rax, 16
mov     qword ptr [rbp - 96], rax       # 8-byte Spill
.LBB0_2:
mov     rax, qword ptr [rbp - 96]       # 8-byte Reload
mov     qword ptr [rbp - 80], rax
mov     rax, qword ptr [rbp - 80]
mov     rcx, qword ptr [rax]
mov     rcx, qword ptr [rcx - 24]
mov     dword ptr [rax + rcx + 8], 8

        这次的汇编代码复杂好多,Base2类是第二继承的父类,在布局上,它和对象d的起始地址之间相差了Base1子类。所以Derived类型指针转换成Base2类型指针需要进行this指针的调整,伪代码像下面这样:

        Base2* pb2 = &d : &d + sizeof(Base1) ? 0;

        Base1子类的大小是16字节,所以上面汇编代码第7行是对象d的地址加上16的偏移值,接着把它存放在rbp - 80空间中,第12、13行代码是取Base2子类起始地址的内容,它在构造时被设置了一个虚函数表指针,见下面的汇编代码:

lea     rcx, [rip + vtable for Derived]
add     rcx, 64
mov     qword ptr [rax + 16], rcx

        请参考着Derived虚表看,Derived虚表的内容请往上翻,上面三行汇编代码就是将虚表的起始地址偏移64后写入Base2子类的起始地址(对应Derived虚表中的第10行)。接着看上面汇编代码的第14、15行,将这个地址减去24即rcx - 24再取它的内容,它的值是16,第15行代码就是将Base2子类的地址加上偏移值16(也就是Base2子类的大小),再加上8(Grand类的虚函数表指针),最后得到虚基类成员g的地址,对其赋值为8。

通过虚基类的指针存取虚基类成员的实现分析

        最后一种情形,就是C++代码中的第28、29行,是转换成虚基类Grand类型的指针来存取成员g,这两行代码对应的汇编代码如下:

xor     eax, eax
lea     rcx, [rbp - 56]
cmp     rcx, 0
mov     qword ptr [rbp - 104], rax      # 8-byte Spill
je      .LBB0_4
mov     rcx, qword ptr [rbp - 56]
lea     rax, [rbp - 56]
add     rax, qword ptr [rcx - 24]
mov     qword ptr [rbp - 104], rax      # 8-byte Spill
.LBB0_4:
mov     rax, qword ptr [rbp - 104]      # 8-byte Reload
mov     qword ptr [rbp - 88], rax
mov     rax, qword ptr [rbp - 88]
mov     dword ptr [rax + 8], 9

        这段汇编代码跟上面转换成Base2类型的代码基本一样,区别的地方在调整成Base2类型指针时,对象d的首地址加上偏移量16,这个16是编译器直接计算好了Base1子类的大小,而这里转换成Grand类型指针的偏移值的计算是通过虚表取得的(对应第6到第8行代码),最后以Grand子类的起始地址为基址,加上8(前面8字节的虚函数表指针)的偏移值,得到成员g的地址,再对其赋值9。

小结

        虚继承为了解决重复继承的问题,采用了共享子类的方法,所有的派生类中共享同一份虚基类,虚基类一般在内存布局上是存放在整个类的最尾端,借助虚表来访问虚基类中的数据成员,每个派生类中都有编译器为它生成的虚表,里面记录了这个派生类的起始地址相对于虚基类成员的偏移值,这个偏移值在编译期间计算确定下来的。通过指针或者引用类型来访问虚基类的数据成员时,因为不知道它具体指向的类型是什么,所以需要等到运行时,根据它的具体类型找到它对应的虚表,从虚表中得到它的偏移值。通过对象来访问虚基类成员时,因为编译时已经知道了它的具体类型,理论上是可以直接计算它的偏移值是多少,从而得到一个确切的内存地址,不需要经过虚表来访问,但是从上面生成的汇编代码来看,clang编译器并没有做这个优化,仍然采用跟通过指针来访问一样的实现方法。

存取虚基类成员与普通类成员的效率对比

        最后来测试对比下存取虚基类成员的效率和存取普通类成员的效率,将下面的测试代码:

#include <cstdio>
#include<chrono>
using namespace std::chrono;

class Grand {
public:
    int g = 1;
};
class Base1: virtual public Grand {
public:
    int b1 = 2;
};
class Base2: virtual public Grand {
public:
    int b2 = 3;
};
class Derived: public Base1, public Base2 {};

void foo() {
    Derived* pd1 = new Derived;
    Derived* pd2 = new Derived;
    auto start = system_clock::now();
    for (auto i = 0; i < 10000000; ++i) {
        pd2->g = pd1->g + pd1->b1 - pd1->b2;
        pd2->b1 = pd1->b1 + pd1->b2 - pd2->g;
        pd2->b2 = pd1->b1 + pd1->g - pd2->b1;
    }
    auto end = system_clock::now();
    auto duration = duration_cast<milliseconds>(end-start);
    printf("spend %lldms\n", duration.count());
}

class Object {
public:
    int a = 1;
    int b = 2;
    int c = 3;
};

void bar() {
    Object* pb1 = new Object;
    Object* pb2 = new Object;
    auto start = system_clock::now();
    for (auto i = 0; i < 10000000; ++i) {
        pb2->a = pb1->a + pb1->b - pb1->c;
        pb2->b = pb1->b + pb1->c - pb2->a;
        pb2->c = pb1->b + pb1->a - pb2->b;
    }
    auto end = system_clock::now();
    auto duration = duration_cast<milliseconds>(end-start);
    printf("spend %lldms\n", duration.count());
}

int main() {
    foo();
    //bar();
    
    return 0;
}

有几点需要说明下:

  1. foo函数和bar函数分开测试,即每次只运行一个函数,之后屏蔽掉另外一个再重新编译运行,怕会对运行效率有影响。
  2. 类中没有加入成员函数来存取数据成员,采用直接存取的方式,主要是加入函数后,会有调用函数的开销,就算是使用inline,也是会多了很多行的汇编代码,这里抛开这些影响因素。
  3. 编译时没有使用优化选项,因为使用优化选项的话编译器的优化太过于激进,会把整个循环都优化掉,直接在编译期间即运算完成了,不能反映实际的情况。
  4. 每个测试情况分别都运行10次,然后取其平均值。

测试结果:

虚继承

64.9ms

普通类

51.7ms

        从测试结果可以看出,虚继承的情况下存取虚基类的成员效率要比普通类存取数据成员的效率要低一些,大约增加了26%的时间。


本主页会定期更新,为了能够及时获得更新,敬请关注我:点击左下角的关注。也可以关注公众号:请在微信上搜索公众号“iShare爱分享”并关注,或者扫描以下公众号二维码关注,以便在内容更新时直接向您推送。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1673756.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

WS2812B-2020 智能控制LED集成光源芯片IC

一般说明 WS2812B-2020是一款智能控制LED光源&#xff0c;它的外部采用了最新的模压封装技术&#xff0c;控制电路和RGB芯片集成在一个2020组件中。其内部包括智能数字端口数据锁存器和信号整形放大驱动电路。还包括一个精密的内部振荡器和一个电压可编程恒流控制部分&…

对关系型数据库管理系统的介绍

1.数据库的相关介绍 关系型数据库管理系统&#xff1a;&#xff08;英文简称&#xff1a;RDBMS&#xff09; 为我们提供了一种存储数据的特定格式&#xff0c;所谓的数据格式就是表&#xff0c; 在数据库中一张表就称为是一种关系. 在关系型数据库中表由两部分组成&#xf…

嵌入式和单片机的区别在哪?

嵌入式和单片机是两个不同的概念&#xff0c;它们在很多方面都存在着差异。嵌入式系统是一种专用的计算机系统&#xff0c;通常用于控制和监测其他设备。它通常由微处理器、存储器、输入/输出接口和其他外围设备组成。嵌入式系统可以运行各种操作系统&#xff0c;如 Linux、Win…

TCP/UDP通信中的部分函数

UDP&#xff08;User Datagram Protocol&#xff0c;用户数据报协议&#xff09;和TCP&#xff08;Transmission Control Protocol&#xff0c;传输控制协议&#xff09;是互联网协议套件中最常用的两种传输层协议&#xff0c;它们负责在互联网中端到端地传输数据。尽管它们服务…

web网页录音(recorder.js)并上传后端语音转文字(Vosk)

我是一个后端开发人员&#xff0c;现在都快进化成全栈了。操了&#xff0c;是谁有好的项目让我跳跳槽&#xff0c;转转行吧 写在前面&#xff0c;很重要 这是官方文档的说明 翻译如下&#xff1a; 我们有两种型号-大型号和小型号&#xff0c;小型号非常适合在移动应用程序上执…

IT行业现状与未来趋势分析

IT行业现状与未来趋势显示出持续的活力和变革&#xff0c;以下是上大学网&#xff08;www.sdaxue.com&#xff09;关于IT行业现状与未来趋势分析&#xff0c;供大家参考。 当前现状&#xff1a; 市场需求持续增长&#xff1a;随着信息时代的深入发展&#xff0c;各行各业对信息…

一条查询SQL的执行过程

1.1 假设 查询语句为&#xff1a;mysql> select * from T where ID 10 1.2 总体执行流程 1.2.1 连接器 -> 连接 作用&#xff1a;负责跟客户端建立连接、获取权限、维持和管理连接等工作流程&#xff1a; 一个用户成功建立连接后&#xff0c;如果客户端太长时间没有请…

跨ROS系统通信:使用TCP实现节点间的直连

当涉及到在机器人操作系统&#xff08;ROS&#xff09;环境中的通信时&#xff0c;标准做法通常是在同一个ROS网络内通过话题和服务进行。但在某些特定情况下&#xff0c;比如当你有两个分布在不同网络中的ROS系统时&#xff0c;标准的通信方法可能不太适用。此时&#xff0c;一…

SpringBoot集成Seata分布式事务OpenFeign远程调用

Docker Desktop 安装Seata Server seata 本质上是一个服务&#xff0c;用docker安装更方便&#xff0c;配置默认&#xff1a;file docker run -d --name seata-server -p 8091:8091 -p 7091:7091 seataio/seata-server:2.0.0与SpringBoot集成 表结构 项目目录 dynamic和dyna…

EmotiVoice 实时语音合成TTS;api接口远程调用

参考:https://github.com/netease-youdao/EmotiVoice 测试整体速度可以 docker安装: 运行容器:默认运行了两个服务,8501 一个streamlit页面,另外8000是一个api接口服务 docker run -dp 8501:8501 -p 8250:8000 syq163/emoti-voice:latest##gpu运行 (gpu运行遇到CUDA er…

山东齐鲁文化名人颜廷利:朱郭有文才,曲高‘菏’寡星光路

山东齐鲁文化名人颜廷利教授表示&#xff0c;朱郭&#xff08;谐音‘祖国’&#xff09;有文才&#xff0c;《曲高‘菏’寡》星光路… 山东菏泽歌手朱之文在2011年凭借一首《滚滚长江东逝水》一夜成名&#xff0c; 十多年之后的今天&#xff0c;菏泽市网络红人郭有才靠一首《诺…

LeetCode---循环队列

循环队列就是只有固定的内存&#xff0c;存数据&#xff0c;出数据&#xff0c;但是也和队列一样&#xff0c;先进先出。如下图所示&#xff0c;这是他的样子 在head出&#xff0c;tail进&#xff0c;但是这个如果用数组解决的话&#xff0c;就有问题&#xff0c;力扣给我们的接…

Django模型进阶-多对多关系

在Django中&#xff0c;多对多&#xff08;Many-to-Many&#xff09;关系是一种数据库关系&#xff0c;表示一个模型的实例可以与另一个模型的多个实例相关联&#xff0c;同时另一个模型的实例也可以与这个模型的多个实例相关联。换句话说&#xff0c;就是两个模型之间可以存在…

免费SSL证书获取与部署教程

在互联网时代&#xff0c;HTTPS已成为网站安全的基石&#xff0c;为用户数据传输提供加密保障。免费SSL证书的出现降低了部署HTTPS的门槛&#xff0c;尤其对于个人网站、小微企业及测试环境而言&#xff0c;它们是理想的选择。本文旨在提供一份详尽指南&#xff0c;帮助您轻松获…

找到字符串中所有的字母异位词 ---- 滑动窗口

题目链接 题目: 分析: 要找的是在s中和p是异位词的子串, 也就是说子串大小和p相同, 那么就是窗口大小固定的滑动窗口问题可以使用哈希数组来记录每个元素出现的个数, 定义hash1存放p中的各元素个数定义left 0; right 0;进窗口 让right指向的元素进窗口, 即更新hash2中的元素…

基于若依的ruoyi-nbcio流程管理系统支持指定接收人的流程审批

更多ruoyi-nbcio功能请看演示系统 gitee源代码地址 前后端代码&#xff1a; https://gitee.com/nbacheng/ruoyi-nbcio 演示地址&#xff1a;RuoYi-Nbcio后台管理系统 http://218.75.87.38:9666/ 更多nbcio-boot功能请看演示系统 gitee源代码地址 后端代码&#xff1a; h…

【SRC实战】无限获取优惠码

挖个洞先 https://mp.weixin.qq.com/s/HgMK4S8275VvFVbnSp6Qsw “ 以下漏洞均为实验靶场&#xff0c;如有雷同&#xff0c;纯属巧合 ” 01 — 漏洞证明 “ 获取优惠码有次数限制的情况下&#xff0c;如何绕过&#xff1f;” 1、新用户专属福利&#xff0c;免费领100元优惠…

Can not add resource (com.android.aaptcompiler.ParsedResource@a980fbb) to table

具体原因 资源合并时出现编译问题。 1. 什么是资源&#xff1f; 就是res目录下面的values目录下的文件。以及&#xff01;以及&#xff01;你所引入的其他依赖&#xff08;第三方库&#xff09;的values.xml文件 2. 一般什么原因会导致合并出错? 你的资源文件中的内容错误&…

python批量为图片做灰度处理

欢迎关注我👆,收藏下次不迷路┗|`O′|┛ 嗷~~ 目录 一.前言 二.代码 三.使用 四.总结

攻防世界(CTF)~web-supersqli(详细解题思路)

题目介绍 题目描述“随便注” 先看一下是否存在注入 判断闭合方式 输入1’ and 11-- -正常回显 输入1and 12-- -无回显,确认是单引号闭合 看一下列数 输入1 order by 2-- - 有回显 输入1 order by 3-- - 报错&#xff0c;由此判断两列 使用union联合注入发现select被过滤了&a…