C++编译器的程序转化

news2024/11/19 19:37:58

编译器在某些情况下会对程序进行转化,有些是编译器需要的,有些是出于性能考虑的,转化可能会产生出乎意料的结果

文章目录

  • 明确的初始化操作
  • 参数的初始化
  • 返回值的初始化
  • 在使用者层面做优化
  • 在编译器层面做优化
    • NRV 优化
    • NRV优化的弊端
  • 参考资料


明确的初始化操作

已知有这样的定义

X x0;

下面有三个定义,每一个都明显地以 x0 来初始化其类对象

void foo_bar() {
	X x1( x0 );
	X x2 = x0;
	X x3 = X( x0 );
}

必要的程序转化有两个阶段:

  1. 重写每一个定义,其中的初始化操作会被剥离。
  2. 类的拷贝构造函数的会被安插进去

其 C++ 伪码可能像下面这样:

void foo_bar() {
	X x1;
	X x2;
	X x3;

	x1.X::X( x0 );
	x2.X::X( x0 );
	x3.X::X( x0 );
}

参数的初始化

C++ 标准说,把一个类对象当做参数传给一个函数(或是作为一个函数的返回值),相当于以下形式的初始化操作:

X xx = arg;

其中 xx 代表形式参数 (或返回值) 而 arg 代表真正的参数值。因此,若已知这个函数:

void foo( X x0 );

下面这样的调用方式:

X xx;
foo( xx );

将会要求局部实体 x0 以逐成员的方式将 xx 当作初值。在编译器实现技术上,有一种策略是导入所谓的暂时性对象(或临时对象),并调用拷贝构造函数将它初始化,然后将该暂时性对象(或临时对象)交给函数。

例如将前一段程序代码转换如下:

X __temp0;
__temp0.X::X( xx );
foo( __temp0 ):

然而这样的转换是有问题的,因为我们又要调用 foo( X x0 ) 再展开下去,产生无穷无尽的调用,因此这种情况,函数声明也相当于改为 void foo( X& x0 )

下面的为vs2022下的汇编代码
可以看到在调用 foo 之前先调用了X的拷贝构造函数

在拷贝构造函数中可以看到对于变量的地址

之后将rax的值传给了rcx,可以看到rax中的值就是之前this指针的地址,然后调用 foo

可以看到 x0 的地址为之前 this 指针的地址,说明确实是 void foo( X& x0 )


返回值的初始化

已知下面这个函数定义

X bar()
{
	X xx;
	return xx;
}

那么 bar() 的返回值如何从局部对象 xx 中拷贝过来,在 cfront 中的解决方法是一个双阶段转化:

  1. 首先加上一个额外参数,类型是类对象的一个引用。这个数将用来放置拷贝构造得到的返回值
  2. 在return指令之前安插一个拷贝构造调用操作,以便将想传回的对象的内容当做上述新增参数的初值

真正的返回值是什么?最后一个转化操作会重新改写函数,使它不传回任何值,bar() 转换如下:

void bar( X& __result )
{
	X xx;
	xx.X::X();
	__result.X::XX( xx );
	
	return;
}

现在编译器必须转换每一个 bar() 调用操作,以反映其新定义。例如:

X xx = bar();

将被转换为下列两个指令句:

X xx;
bar( xx );

将被转换为下列两个指令句:

X xx;
bar( xx );

而对于语句:

bar().memfunc();

可能被转化为:

X __temp0;
( bar( __temp0 ), __temp0 ).memfunc();

同样道理,如果程序声明了一个函数指针,像这样:

X ( *pf )();
pf = bar;

它也必须被转化为:

void ( *pf )( X& );
pf = bar;

在使用者层面做优化

比如下面的函数

X bar (const T &y, const T &z)
{
	X xx;
	xx.m_x = y + z;
	return xx;
}

如果使用者将这个函数改为

X bar( const T &y, const T &z)
{
	return X( y, z );
}

于是当 bar() 的定义被转换之后,效率会比较高:

void bar( X &__result )
{
	__result.X::X( y, z );
	return;
}

__result 被直接计算出来,而不是经由拷贝构造拷贝而得。


在编译器层面做优化

NRV 优化

在一个如 bar() 这样的函数中,所有的 return 指令传回相同的具名数值,比如上面实例中的局部变量 xx,编译器有可能自己做优化,方法是以 result 参数取代 named return value。例如下面的 bar() 定义:

X bar()
{
	X xx;
	// ... 处理 xx
	return xx;
}

编译器把其中的 xx__result 取代:

void bar( X& __result )
{
	__result.X::X();
	// ... 直接处理__result
	return ;
}

这样的编译器优化操作,有时候被称为 Named Return Value(NRV) 优化。

看下 VS2022中,下面的实例

X bar(const int y, const int z)
{
    X xx;
    xx.m_x = y + z;
    return xx;
}

int main()
{
	X xxx = bar(1, 2);
}

可以看到像实参一样将 xxx 的地址进行了压栈

可以看到,这里的 xx 的值实际就是 xxx 的地址,然后调用了默认构造函数,就是相当于xxx.X::X()

VS2022貌似默认就算未有明确定义的拷贝构造函数,也会进行NRV优化,看下面这个case

#include <iostream>

class test {
	friend test foo(double);
public:
	test()
	{
		memset(array, 0, 100 * sizeof(double));
	}
private:
	double array[100];
};

test foo(double val)
{
	test local;

	local.array[0] = val;
	local.array[99] = val;
	return local;
}

int main()
{
	for (int cnt = 0; cnt < 100000000; cnt++)
	{
		test t = foo(double(cnt));
	}
	return 0;
}

这里可以看到 ebp-334h 就等于 &t


foo 函数中的 local 可以看到就是 t 的地址,说明进行了 NRV 优化

NRV优化的弊端

虽然NRV优化提供了重要的效率改善,还是有有一些弊端:

  1. 优化由编译器默默完成,而它是否真的被完成,并不十分清楚(因为很少由编译器会说明其实现程度,或是否实现)。
  2. 一旦函数变得比较复杂,优化也就变得比较难以施行。在 cfront 中,只有当所有的 named return 指令句发生于函数的 top level 时,优化才施行。如果导入 “a nested local block with a return statement”,cfront 就会静静地将优化关闭
  3. NRV 优化可能会带来意想不到的问题

第三点,可以考虑下面这个case,我们在上面的 test 类中,加入一个类静态变量 count,其初始值为 0,加入一个析构函数

	~test()
	{
		count++;
	}

最后输出这个 test::cout,完整代码如下:

#include <iostream>
using namespace std;
class test {
	friend test foo(double);
public:
	static int count;
	test()
	{
		memset(array, 0, 100 * sizeof(double));
	}
	~test()
	{
		count++;
	}
private:
	double array[100];
};

int test::count = 0;

test foo(double val)
{
	test local;

	local.array[0] = val;
	local.array[99] = val;
	return local;
}

int main()
{
	for (int cnt = 0; cnt < 100000000; cnt++)
	{
		test t = foo(double(cnt));
	}
	cout << test::count << endl;
	return 0;
}

正常不被NRV优化,我们期望的应该是 200000000,但实际输出了 100000000


参考资料

《深度探索C++对象模型》—— Stanley B.Lippman著,侯捷译

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

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

相关文章

【MyBatis】 MyBatis框架下的高效数据操作:深入理解增删查改(CRUD)

&#x1f493; 博客主页&#xff1a;从零开始的-CodeNinja之路 ⏩ 收录文章&#xff1a;【MyBatis】 MyBatis框架下的高效数据操作&#xff1a;深入理解增删查改&#xff08;CRUD&#xff09; &#x1f389;欢迎大家点赞&#x1f44d;评论&#x1f4dd;收藏⭐文章 目录 My …

算法入门<二>:分治算法之汉诺塔问题及递归造成的栈溢出

1、分治算法 分治&#xff08;divide and conquer&#xff09;&#xff0c;全称分而治之&#xff0c;是一种非常重要且常见的算法策略。分治通常基于递归实现&#xff0c;包括“分”和“治”两个步骤。 分&#xff08;划分阶段&#xff09;&#xff1a;递归地将原问题分解为两…

【C语言】指针篇-精通库中的快速排序算法:巧妙掌握技巧(4/5)

&#x1f308;个人主页&#xff1a;是店小二呀 &#x1f308;C语言笔记专栏&#xff1a;C语言笔记 &#x1f308;C笔记专栏&#xff1a; C笔记 &#x1f308;喜欢的诗句:无人扶我青云志 我自踏雪至山巅 文章目录 一、回调函数二、快速排序(Qsort)2.1 Qsort参数部分介绍2.2 不…

数据仓库和数据仓库分层

一、数据仓库概念 数据仓库(Data Warehouse)&#xff0c;可简写为DW或DWH。数据仓库&#xff0c;是为企业所有级别的决策制定过程&#xff0c;提供所有类型数据支持的战略集合。它是单个数据存储&#xff0c;出于分析性报告和决策支持目的而创建。 为需要业务智能的企业&#…

计算机网络4——网络层4内部路由选择协议

文章目录 一、有关路由选择协议的几个基本概念1、理想的路由算法2、分层次的路由选择协议 二、内部网关协议 RIP1、协议 RIP 的工作原理2、特点3、距离向量算法4、坏消息传播慢 三、内部网关协议 OSPF1、基本特点2、OSPF 的五种分组类型 本节将讨论几种常用的路由选择协议&…

uniapp 自定义 App启动图

由于uniapp默认的启动界面太过普通 所以需要自定义个启动图 普通的图片不可以过不了苹果的审核 所以使用storyboard启动图 生成 storyboard 的网站&#xff1a;初雪云-提供一站式App上传发布解决方案

从零入门区块链和比特币(第一期)

欢迎来到我的区块链与比特币入门指南&#xff01;如果你对区块链和比特币感兴趣&#xff0c;但不知道从何开始&#xff0c;那么你来对地方了。本博客将为你提供一个简明扼要的介绍&#xff0c;帮助你了解这个领域的基础知识&#xff0c;并引导你进一步探索这个激动人心的领域。…

使用RTSP将笔记本摄像头的视频流推到开发板

一、在Windows端安装ffmpeg 1. 下载ffmpeg:下载ffmpeg 解压ffmpeg-master-latest-win64-gpl.zip bin 目录下是 dll 动态库 , 以及 可执行文件 ;将 3 33 个可执行文件拷贝到 " C:\Windows " 目录下 ,将所有的 " .dll " 动态库拷贝到 " C:\Windows\Sy…

java集合框架中的Map和Set的使用方式

目录 一、Map的使用方法说明 put&#xff08;&#xff09;&#xff1a; GetOrDefault()&#xff1a; containsKey()与containsVal&#xff08;&#xff09;&#xff1a; keySet()与m.values()&#xff1a; 二、Set的使用方法说明 add(): iterator()---->迭代器 一、M…

19 做好微服务间依赖的治理和分布式事务

在前两讲里&#xff0c;分别从微服务的对外接口、消息消费以及微服务自身的相关编码规范上阐述了“防备上游、做好自己”这两个准则如何落地。 在本讲里&#xff0c;将会讲解为什么要“怀疑下游”&#xff0c;以及有哪些手段可以落地此条准则。此外&#xff0c;还会介绍在进行…

每日OJ题_DFS爆搜深搜回溯剪枝②_力扣526. 优美的排列

目录 力扣526. 优美的排列 解析代码 力扣526. 优美的排列 526. 优美的排列 难度 中等 假设有从 1 到 n 的 n 个整数。用这些整数构造一个数组 perm&#xff08;下标从 1 开始&#xff09;&#xff0c;只要满足下述条件 之一 &#xff0c;该数组就是一个 优美的排列 &#…

Content type ‘application/json;charset=UTF-8‘ not supported异常的解决过程

1.首先说明开发场景 *就是对该json格式数据传输到后台 后台实体类 import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; import org.sp…

Linux搭建靶场

提前准备&#xff1a; 文章中所使用到的Linux系统&#xff1a;Ubantu20.4sqlilabs靶场下载地址&#xff1a;GitHub - Audi-1/sqli-labs: SQLI labs to test error based, Blind boolean based, Time based. 一. 安装phpstudy phpstudy安装命令&#xff1a;wget -O install.sh h…

《MySQL对库的基本操作》

文章目录 一、查看数据库列表查看数据库中的所有表想知道当前处于哪个数据库里 二、创建一个数据库三、删除一个数据库知道两个集1.字符集2.校验集修改数据库的字符集和编码集 不同的校验码对数据库的影响四、数据库的备份与恢复注意事项&#xff1a;备份数据库中的表 总结 一、…

Lan仿朋友圈系统源码,用于表白墙等微商相册,商品图册等

这是一套基于PHP开发的Lan仿朋友圈系统开源&#xff0c;适用于表白墙、微商相册、商品图册等场景。 下 载 地 址 &#xff1a; runruncode.com/php/19750.html 主要功能包括&#xff1a; - 支持前端用户注册和消息提示。 - 用户注册时可设置必须验证邮箱账号&#xff0c;以…

【C++】学习笔记——类和对象_5

文章目录 二、类和对象14. 日期类的实现15. const成员16. 取地址重载17. 再谈构造函数初始化列表 18. explicit关键字19. static成员 未完待续 二、类和对象 14. 日期类的实现 上一篇我们已经大致将日期类的重要功能都给实现了&#xff0c;这节将会对日期类进行完善&#xff…

Linux 端口复用:SO_REUSEPORT

文章目录 前言一、BSD socket1.1 简介1.2 SO_REUSEADDR1.2.1 3-way or 4-way handshake1.2.2 SO_LINGER 1.3 SO_REUSEPORT 二、Connect() Returning EADDRINUSE三、Multicast Addresses四、Linux4.1 Linux < 3.94.2 Linux > 3.9 五、Linux SO_REUSEPORT socket option六、…

python安卓自动化pyaibote实践------学习通自动刷课

前言 欢迎来到我的博客 个人主页:北岭敲键盘的荒漠猫-CSDN博客 本文是一个完成一个自动播放课程&#xff0c;避免人为频繁点击脚本的构思与源码。 加油&#xff01;为实现全部电脑自动化办公而奋斗&#xff01; 为实现摆烂躺平的人生而奋斗&#xff01;&#xff01;&#xff…

【吊打面试官系列】Java高并发篇 - 为什么 wait 和 notify 方法要在同步块中调用?

大家好&#xff0c;我是锋哥。今天分享关于 【为什么 wait 和 notify 方法要在同步块中调用&#xff1f;】面试题&#xff0c;希望对大家有帮助&#xff1b; 为什么 wait 和 notify 方法要在同步块中调用&#xff1f; Java API 强制要求这样做&#xff0c;如果你不这么做&#…

论文精读-基于FPGA的卷积神经网络和视觉Transformer通用加速器

论文精读-基于FPGA的卷积神经网络和视觉Transformer通用加速器 优势&#xff1a; 1.针对CNN和Transformer提出了通用的计算映射&#xff08;共用计算单元&#xff0c;通过不同的映射指令&#xff0c;指导数据通路和并行计算&#xff09; 2.非线性与归一化加速单元&#xff0…