- 浏览: 25442 次
- 性别:
- 来自: 深圳
最新评论
所有的C、C++教科书都警告我们:不要通过函数来返回struct或 class对象,否则会造成内存复制以及复制构造函数的调用,降低性能。相信这句话已经成为了一个常识,大家都能牢记于心。然而,有时候我们不得不违反这个警告,例如,通过函数获取一个std::string对象(以个人的经验而言,这种情况是很常见的,我经常要通过函数创建一个新的对象)。不知道从什么时候起,当我面对这种情况的时候会通过引用来获取这个对象,像这样:
1
2
|
std::string GetString(); std::string& str = GetString(); |
这样子给我的感觉会好一点,让我觉得对象的复制次数少了。然而这只是一种凭空猜想,没有经过任何证实。为了弄清楚这样做究竟会不会带来性能的提升,我决定研究一下函数是如何返回struct或class对象的。最好的研究途径当然是反汇编编译器生成的机器码了。
我的实验环境是Visual Studio 2010,所有代码都是Debug版本的,因为这样生成的机器码是最原始的,没有经过任何优化,可以显示出真实的情况。而Release版本的机器码经过了优化,已经是“面目全非”,所以本文不考虑该版本。另外,对于struct来说,Visual Studio 2010 的C编译器和C++编译器生成的代码是一样的,所以本文所有代码都通过C++编译器来编译。注意,使用不同的编译器可能会有不同的结果!
如何返回struct对象
首先来看一下函数如何返回struct对象。分两种情况:第一种情况是struct的大小是1字节、2字节或4个字节,可以放到al、ax或eax寄存器中;第二种情况是struct的大小不是上面提到的三个值,不能放到寄存器中(包括3个字节的)。要注意,这里所说的“大小”是指在内存中经过对齐后的大小,而不是定义的大小。如果没有特别说明,下文提到的大小也是指经过对齐后的大小。
第一种情况:struct可以放到寄存器中
下面是第一种情况的典型例子,struct的大小是4个字节:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
struct S {
int Value;
}; S GetS( int value) {
S s;
s.Value = value;
return s;
} int wmain() {
S s = GetS(10);
} |
下面是GetS函数的部分汇编代码:
1
2
3
4
5
6
|
;s.Value = value; mov eax,dword ptr [value] mov dword ptr [s],eax ;return s; mov eax,dword ptr [s] |
可以看到,s是直接通过eax来返回的,因为它的大小恰好可以放进eax寄存器中。
下面是S s = GetS(10);的汇编代码:
1
2
3
4
5
6
|
push 0Ah ;参数10入栈 call GetS (8D1019h) ;调用GetS函数 add esp,4 ;释放参数空间 mov dword ptr [ebp-0D4h],eax ;将返回值保存到临时空间 mov eax,dword ptr [ebp-0D4h] ;从临时空间里取出返回值 mov dword ptr [s],eax ;将返回值保存到s中 |
这些代码都很好理解,唯一让人疑惑的地方是,返回值不是直接保存到s中,而是先放到一块临时空间里(ebp-0D4h),然后再从这块临时空间转移到s中。为什么编译器要如此多此一举呢?这是因为存在“不接收返回值”的函数调用,例如:GetS(10);,它返回的struct不会保存到局部变量里,而是只保存到那块临时空间中。
上面的汇编代码确实验证了那句警告,即使struct可以像一个普通的int那样通过eax返回,也会稍微降低性能,因为执行了两条“多余”的指令,但我认为这样的开销还是可以接受的。对于大小为1个字节或2个字节的struct来说,生成的汇编代码跟上面的几乎一样,只不过返回值是通过al或ax来返回的。
第二种情况:struct不能放到寄存器中
下面是第二种情况的典型例子,struct的大小为12字节:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
struct S {
int Value1;
int Value2;
int Value3;
}; S GetS( int value) {
S s;
s.Value1 = value;
s.Value2 = value * 2;
s.Value3 = value * 3;
return s;
} int wmain() {
S s = GetS(10);
} |
下面是GetS函数的部分汇编代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
;s.Value1 = value; mov eax,dword ptr [ebp+0Ch] mov dword ptr [ebp-14h],eax ;s.Value2 = value * 2; mov eax,dword ptr [ebp+0Ch] shl eax,1 mov dword ptr [ebp-10h],eax ;s.Value3 = value * 3; mov eax,dword ptr [ebp+0Ch] imul eax,eax,3 mov dword ptr [ebp-0Ch],eax ;return s; mov eax,dword ptr [ebp+8] ;取出第一个参数的值 mov ecx,dword ptr [ebp-14h] ;取出s.Value1 mov dword ptr [eax],ecx ;将s.Value1放到eax所指的内存中 mov edx,dword ptr [ebp-10h] ;取出s.Value2 mov dword ptr [eax+4],edx ;将s.Value2放到eax+4所指的内存中 mov ecx,dword ptr [ebp-0Ch] ;取出s.Value3 mov dword ptr [eax+8],ecx ;将s.Value3放到 eax+8所指的内存中 mov eax,dword ptr [ebp+8] ;将第一个参数作为返回值 |
重点看return s;这一句的汇编代码,它将局部变量s(ebp-14h)复制到了第一个参数(ebp+8)所指的内存中,然后将第一个参数作为返回值。等等,GetS不是只有一个参数吗?而且这个参数只是一个数值,而不是地址,这样做的话肯定会出错。再往上看看那几条赋值语句的汇编代码,或许就明白了:GetS的参数value实际上是ebp+0Ch,而不是ebp+8,也就是说,GetS实际上有两个参数!
再来看一下S s = GetS(10);这一句的汇编代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
push 0Ah ;参数10入栈 lea eax,[ebp-0E8h] ;取出临时空间的地址 push eax ;将临时空间的地址入栈 call GetS (51019h) ;调用GetS add esp,8 ;释放参数空间 ;接下来的6条指令是将返回的struct(ebp-0E8h)复制到另一块临时空间(ebp-0FCh)中 mov ecx,dword ptr [eax] mov dword ptr [ebp-0FCh],ecx mov edx,dword ptr [eax+4] mov dword ptr [ebp-0F8h],edx mov eax,dword ptr [eax+8] mov dword ptr [ebp-0F4h],eax ;接下里的6条指令将临时空间(ebp-0FCh)中的数据复制到局部变量s(ebp-14h)中 mov ecx,dword ptr [ebp-0FCh] mov dword ptr [ebp-14h],ecx mov edx,dword ptr [ebp-0F8h] mov dword ptr [ebp-10h],edx mov eax,dword ptr [ebp-0F4h] mov dword ptr [ebp-0Ch],eax |
可以看到,GetS除了value这个显式定义的参数之外,还有一个隐含的参数,该参数是一个指向一块临时空间(ebp-0E8h)的地址,在GetS内部将要返回的struct复制到了这块临时空间中,然后再通过eax返回这块临时空间的地址。这样,通过两方的协作,完成了struct的返回。
接下来的指令仍然是在做“多余”的事情:将返回值复制到另一块临时空间(ebp-0FCh)中,再从临时空间复制到局部变量s(ebp-14h)中。综上所述,为了从函数中返回一个struct,需要三块内存空间:一块用来接收返回值,一块“多余”的临时空间,一块是局部变量的空间。另外还需要进行三次内存复制:一次是被调用函数复制返回值,另外两次是“多余”的复制。由此看出,返回一个不能容纳于寄存器中的struct,不仅浪费时间,也浪费空间!
如何返回class对象
虽然在C++中struct和class本质上是一样的,但为了加以区别,在下文中规定,class泛指含有复制构造函数的struct或class,而struct 泛指没有复制构造函数的struct或class(希望不会给你带来混乱)。你会看到,有没有复制构造函数会造成很大的不同。
返回class对象的行为比返回struct的行为简单得多,不论class的大小如何,处理方式都是一样的。下面是例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
class C {
public :
C() { }
C( const C& rhs) {
Value1 = rhs.Value1;
Value2 = rhs.Value2;
Value3 = rhs.Value3;
}
int Value1;
int Value2;
int Value3;
}; C GetC( int value) {
C c;
c.Value1 = value;
c.Value2 = value * 2;
c.Value3 = value * 3;
return c;
} int wmain() {
C c = GetC(10);
} |
下面是C c = GetC(10);的汇编代码:
1
2
3
4
5
|
push 0Ah ;参数10入栈 lea eax,[c] ;取得局部变量c的地址 push eax ;将c的地址入栈 call GetC ;调用GetC add esp,8 ;释放参数空间 |
看上去清爽得多了。这里同样是将局部变量的地址作为隐含参数传递给被调用函数,但最后少了内存复制的操作。
下面是GetC的部分汇编代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
;C c; lea ecx,[c] call C::C ;调用默认构造函数 ;c.Value1 = value; mov eax,dword ptr [value] mov dword ptr [c],eax ;c.Value2 = value * 2; mov eax,dword ptr [value] shl eax,1 mov dword ptr [ebp-0Ch],eax ;c.Value3 = value * 3; mov eax,dword ptr [value] imul eax,eax,3 mov dword ptr [ebp-8],eax ;return c; lea eax,[c] push eax mov ecx,dword ptr [ebp+8] call C::C ;调用复制构造函数 mov eax,dword ptr [ebp+8] |
重点还是在return c;这条语句上,它的汇编代码非常简洁,仅仅是调用传递进来的C对象的复制构造函数!假如复制构造函数中只进行一次内存复制的话,那么从函数中返回一个class对象只需要进行一次内存复制,也只需要一块内存空间,即局部变量所需的空间。也就是说,返回一个class对象基本上只需要调用一次复制构造函数即可。
下面再来看一种特殊情况:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
class C {
public :
C( int value) {
Value1 = value;
Value2 = value;
Value3 = value;
}
C( const C& rhs) {
Value1 = rhs.Value1;
Value2 = rhs.Value2;
Value3 = rhs.Value3;
}
int Value1;
int Value2;
int Value3;
}; C GetC( int value) {
return C(value);
} int wmain() {
C c = GetC(10);
} |
在GetC函数中,直接在return语句中构造一个C对象并返回。可以猜想,这样的话只需要调用一次构造函数就可以返回class对象了。下面是GetC的部分汇编代码:
1
2
3
4
5
6
|
;return C(value); mov eax,dword ptr [value] push eax mov ecx,dword ptr [ebp+8] call C::C ;调用构造函数 mov eax,dword ptr [ebp+8] |
果然如此,这种做法的效率更高,跟创建一个新的对象几乎没有什么区别(当然,函数调用的开销还是存在的)。
由此可以看出,通过函数来返回一个class对象比返回一个struct对象开销要小得多,不需要多余的内存空间,也不需要多余的复制内存操作。
通过引用来获取对象真的高效率吗?
好了,上面通过对函数如何返回struct或class对象进行了比较全面研究,是时候来回答本文开头提到的问题了。下面分别是通过引用来获取struct和class的语句产生的汇编代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
;S& s = GetS(10); push 0Ah lea eax,[ebp-0F4h] push eax call GetS add esp,8 ;下面6条指令将返回值(ebp-0F4h)复制到第一块临时空间(ebp-108h) mov ecx,dword ptr [eax] mov dword ptr [ebp-108h],ecx mov edx,dword ptr [eax+4] mov dword ptr [ebp-104h],edx mov eax,dword ptr [eax+8] mov dword ptr [ebp-100h],eax ;下面6条指令将第一块临时空间(ebp-108h)的数据复制到第二块临时空间(ebp-20h) mov ecx,dword ptr [ebp-108h] mov dword ptr [ebp-20h],ecx mov edx,dword ptr [ebp-104h] mov dword ptr [ebp-1Ch],edx mov eax,dword ptr [ebp-100h] mov dword ptr [ebp-18h],eax ;将第二块临时空间(ebp-20h)的地址赋值给局部变量s(ebp-0Ch) lea ecx,[ebp-20h] mov dword ptr [ebp-0Ch],ecx ;C& c = GetC(10); push 0Ah lea eax,[ebp-1Ch] push eax call GetC add esp,8 ;将临时空间(ebp-1Ch)的地址赋值给变量c lea ecx,[ebp-1Ch] mov dword ptr [c],ecx |
通过与上文的汇编代码进行比较,发现使用引用后不仅没有减少指令,反而增加了两条指令,将临时空间的地址赋值给引用变量。所以得出结论,使用引用来获取对象的效率反而降低了!
总结
知道了函数如何返回struct或class对象,我得出下面的编程指导:
①对于大小为1字节、2字节或4字节的struct,可以通过函数来返回。
②对于大小不是1字节、2字节或4字节的struct,不要通过函数来返回。
③对于class,如果复制构造函数的工作量少,可以通过函数来返回;如果复制构造函数的工作量大,则不要通过函数返回。
④对于class,尽量通过在return语句中构造对象来返回。
⑤不要通过引用来获取函数返回的对象!
最后再说明一下,不同编译器的处理方式可能会不同,所以上面的指导不一定完全通用。另外,Release版本的代码会经过优化,可能会消除那些降低性能的代码。当然啦,我们不能依赖于编译器的优化,因为不是任何情况都适合优化的。
发表评论
-
网络编程——一些思考
2013-05-09 15:07 4901. 在学习网络编程的时候,我通过网上的了解,买了不少书, ... -
centos中编译log4cxx
2013-03-18 10:10 1534log4cxx-0.10.0日志中文乱码 log4cxx ... -
linux在用户程序中如何向操作系统发送按键事件
2013-01-23 19:09 2483转自:http://blog.csdn.net/xian ... -
为什么linux下多线程编程,每次执行结果都不一样
2013-01-03 21:41 1161#include <pthread.h> ... -
BlockingQueue C++实现
2012-11-18 21:05 1630// BlockingQueue.h: interfac ... -
27种设计模式C++实现——单例模式
2012-09-25 22:02 01. 单例模式 -
27种设计模式C++实现——原始模型模式
2012-09-25 22:01 6551. 克隆接口 2. 具体实现者类 -
27种设计模式C++实现——建造者模式
2012-09-25 21:59 10051. 指导者类 2. 抽象建造者类 3. 具体建造者类 ... -
27种设计模式C++实现——抽象工厂
2012-09-25 21:57 11271. 抽象产品类 2. 具体产品类 3. 抽象工厂 4. ... -
27种设计模式C++实现——工厂方法
2012-09-25 21:55 6121. 抽象产品类 2. 具体产品类 3. 工厂接口 4. ... -
27种设计模式C++实现——简单工厂
2012-09-25 21:54 624简单工厂 1. 抽象产品类 2. 具体产品类 3. ... -
面向对象编程<继承覆盖>之——C++
2012-09-23 21:39 665C++面向对象继承,虚方法,类似于指针..... ... -
windows进程同步
2012-09-21 15:40 8691. 进程同步的思想很简单 操作系统所有进程,都是内核 ... -
C内存对齐详解
2012-09-18 17:05 594一、什么是对齐,以及为什么要对齐: 1. 现代计算机中内存空 ... -
C++
2012-09-18 11:30 01. 学会数据分层,例如串口指令,与硬件业务分离 2. 学会 ... -
Java与C++内存回收浅析
2012-09-17 11:12 0java与C++内存回收浅析 内存分配结构 ... -
MFC Activex与JavaScript的接口交互
2012-06-18 15:06 1225在Activex的应用中与网页的JavaScript的交互必不 ...
相关推荐
本文详细分析了C#中struct和class的区别,对于C#初学者来说是有必要加以了解并掌握的。 简单来说,struct是值类型,创建一个struct类型的实例被分配在栈上。class是引用类型,创建一个class类型实例被分配在托管堆上...
hash_set c++总结(自定义类型stuct、class)。总结自定义struct、class三个案例。find函数测试,hash_set迭代器。
struct能包含成员函数吗? 能! struct能继承吗? 能!! struct能实现多态吗? 能!!! 本质的一个区别是默认的访问控制,体现在两个方面: 1)默认的继承访问权限。struct是public的,class是...
翻译自 Manju lata Yadav 2019年6月2日 的... 结构体不能有默认构造函数(无参构造函数)或析构函数,构造函数中必须给所有字段赋值。 public struct Coords { public double x; public double y; public Coords
C是一种过程化的语言,struct只是作为一种复杂数据类型定义,struct中只能定义成员变量,不能定义成员函数(在纯粹的C语言中,struct不能定义成员函数,只能定义变量)。例如下面的C代码片断: 代码如下: struct ...
struct能包含成员函数吗? 能!struct能继承吗? 能!!struct能实现多态吗? 能!!! 最本质的一个区别就是默认的访问控制,体现在两个方面:1)默认的继承访问权限。struct是public的,class是private的。 ...
class 获知对象类别或创建对象 clc 清除指令窗 clear 清除内存变量和函数 clf 清除图对象 clock 时钟 colorcube 三浓淡多彩交叉色图矩阵 colordef 设置色彩缺省值 colormap 色图 colspace 列空间的基 close...
2、C++中的 struct 和 class 有什么区别? 【参考答案】从语法上讲,class和struct做类型定义时只有两点区别: (一)默认继承权限。如果不明确指定,来自class的继承按照private继承处理,来自struct的继承按照...
策略性正确的struct(The Politically Correct Struct) 1.3 对象的差异(An Object Distinction) 指针的类型(The Type of a Pointer) 加上多态之后(Adding Polymorphism) 第2章 构造函数语意学(The Semantics...
策略性正确的struct(The Politically Correct Struct) 1.3 对象的差异(An Object Distinction) 指针的类型(The Type of a Pointer) 加上多态之后(Adding Polymorphism) 第2章 构造函数语意学(The Semantics...
template<class TT>struct ChangeClass,false> {typedef typename std::vector*> Type;}; 然后代码中的 typedef typename LK::Templates::UseT<LK::Templates::IsClassOrUnion<T>::value, std::vector, T>::type TP;...
1 介绍 LuaBind 是一个帮助你绑定C++和Lua的库....引用或常量引用作为函数的第一个参数.该函数的剩下的参数将在Lua侧可见,而对象指针将被赋值给第一个 参数.如果我们有如下的C++代码: struct A { int ...
在js中,可以利用构造函数来创建特定类型的对象,其中,有一些原生的构造函数,Object、Array、等等,所以,当type of Object时,返回的是function。此外,我们还可以创建自定义的构造函数,从而自定义对象的属性...
类不仅可以用关键字class来定义,也可以用struct或union来定义。 因为在C++中类和数据结构的概念太相似了,所以这两个关键字struct和class的作用几乎是一样的(也就是说在C++中struct定义的 类也可以有成员函数,而...
超结构 对Struct简单扩展,使其与Hash更兼容,而没有OpenStruct的性能损失 安装 将此行添加到应用程序的 Gemfile 中: gem 'super_struct' ...class Customer < SuperStruct xss=removed xss=removed> #<st
策略性正确的struct(The Politically Correct Struct) 1.3 对象的差异(An Object Distinction) 指针的类型(The Type of a Pointer) 加上多态之后(Adding Polymorphism) 第2章 构造函数语意学(The Semantics...
2.1 C++语言中类 class 和结构 struct 的主要区别是什么? 【解答】在 C++中,对结构体做了一个很重要的扩充,即允许结构体包含函数成 员。如此一来,我们可以使用结构体中的数据成员描述对象的属性,使用结构体 中...
友元函数与友元类、引用与指针那些事、深入浅出C++虚函数的vptr与vtable、宏那些事、范围解析运算符那些事、从初级到高级的enum那些事、纯虚函数和抽象类、volatile、virtual、using、union、this、struct_class、...
c++为了兼容c保留了struct类型,但是c++中的struct和c有明显的区别,c++中的struct可以继承,可以有成员函数,但是在c中却不行,在c++中struc和class更相似(还是有一些区别的,这里不再叙述),c中struct的内存分布...
对象数组:访问形式和struct 结构体一样。 初始化: point(){x=0;y=0;} point(float a){x=a;y=0} point(float a, float b){x=a,y=b;} 构造函数对其初始化 。 point array[3]={point(3,4),5} 第一个元素调用 point...