C++的优化

文章目录

  • 前言
  • 一、编译器帮我们做了什么?
    • 1、 如果一个类什么都不写编译器会创建如下函数
    • 2、编译器隐藏的实现
    • 3、函数参数的传递
    • 4、比较ab(a+b)与ab=a+b
  • 二、问题的引出
  • 三、返回值的优化(NRVO)
      • 1.编译器如何优化
      • 2. 示例代码
      • 3.优化的限制条件
        • 异常
        • 不同名称的返回值对象
  • 总结


前言

在C++编程时,需要知道编译器给你做了哪些工作,这样对程序的优化有帮助。很多年前看的《C++语言99个常用编程错误》里面关于返回值优化,在什么情况下不优化,记忆有点模糊,翻遍了整个有道笔记,没有找到相应的记录。所以这里重新温习了这一部分知识,并以文章的形式记录下来。

提示:以下是本篇文章正文内容,下面案例可供参考

一、编译器帮我们做了什么?

为了更好的说明问题,先简单复习一下C++编译器帮我们做了什么

1、 如果一个类什么都不写编译器会创建如下函数

1.默认的构造函数
2.拷贝构造函数
3.赋值函数
4.移动构造函数
5.移动赋值函数
6.析构函数

看到这可能会问怎么这么多?c++教材里明明只有4种,为什么这里有6种,是不是弄错了?
这里要说明一下的是:
c++98/03的标准是只有4种

1.默认的构造函数
2.拷贝构造函数
3.赋值函数
4.析构函数

而 C++11,有6种,分别为最开始提到的6种

class Thing {
public:
    Thing();                        // 默认构造
    Thing(const Thing&);            // 拷贝构造
    Thing& operator=(const Thing&); // 赋值
    ~Thing();                       // 构造
    // C++11:
    Thing(Thing&&);                 // 移动构造
    Thing& operator=(Thing&&);      // 移动赋值
};

2、编译器隐藏的实现

class Y
{
	public:
		Y(int);
		~Y();
}

Y a(66);  //(1)
Y b = Y(66); //(2)
Y c = 66; //(3)

对于a是直接初始化,直接调用构造函数。对于b 先生成一个Y类型的匿名临时对象,以66初始化,然后该匿名对象被用作Y类型的拷贝构造函数来完成对b的初始化。最后临时对象调用构造函数。
编译器产生类似下面的代码:

Y temp(66);
Y b(temp);
temp.~Y();

c的初始化与b完全相同,只不过产生匿名临时对象的要求不那么明显。

3、函数参数的传递

如下所示

class Y
{
	public:
	Y(int);
	~Y();
	void func(Y arg);
	private:
		Y(const Y&);
}

对func的调用func(1234) 这里编译是通不过的。为什么?
因为C++中的标准规定,参数传递是拷贝构造函数实现的。

4、比较ab(a+b)与ab=a+b

对于class对象而言,以函数的返回值为初始化物,要比以函数的返回值对其赋值来的高效

String ab(a+b) ; 	//高效
ab = a+b; 		    //未必高效

在ab的声明语句中,编译器可以自由的直接将a+b的计算结果复制到ab(所占用的内存区块)中去,不过,对于赋值语句来说这是不可能的。因为赋值运算符是一个成员函数。
String & String::operator=(const String&ths);
为了初始化String类型的成员运算符函数rhs,编译器被迫将a+b的结果复制到一个临时对象中,用该临时对象来初始化rhs,然后再operator=返回时析构该临时对象。(为了效率起见,初化是要优于赋值的,所以先初始化rhs。

二、问题的引出

有一个类class A
如下函数

A  getA1() //函数1
{
	A a;
	a.attr = 12;
	//xxxx;
	return a;
}
void  getA2(A&a) //函数2
{
	a.attr = 12;
	//xxxx;
}

请问上述二个函数,哪个效率高一些?
你可能会回答:函数2的效率会高一些。因为没有临时对象的创建。
但正确回答远非上述这么简单,这里引出 Named Return Value Optimization (NRVO) 返回值的优化。

三、返回值的优化(NRVO)

关于返回值的优化 《C++语言99个常用编程错误》 书中常见错误58 “无视返回值优化”有讲。这里不重复书本上的内容。主要延伸一下NRVO的知识。

1.编译器如何优化

如下例所示

A MyMethod (B &var)
{
   A retVal;
   retVal.member = var.value + bar(var);
   return retVal;
}
valA = MyMethod(valB);

编译器会如何优化了?对于这个问题我们首先要了解如果编译器对于valA = MyMethod(valB)不优化, 这行代码将产生哪些中间环节.
这里有一个知识点: 关于返回值,编译器的实现手法是把返回值的目的数据区作为函数的一个隐藏的额外参数。如下面的_hiddenArg

A MyMethod (A &_hiddenArg, B &var)
{
   A retVal;
   retVal.A::A(); 	// 构造retVal
   retVal.member = var.value + bar(var);
   _hiddenArg.A::A(retVal);  // A的拷贝构造函数
   return;
retVal.A::~A();  	// 析构 retVal

}

MyMethod 函数的返回的值是通过使用隐藏参数_hiddenArg在 valA 指向的内存空间中创建的。
对于上述代码,有存在优化的空间,**基本思想是消除基于堆栈的临时变量 (retVal) 并使用隐藏参数(_hiddenArg)。因此,这将消除基于堆栈的值的复制构造函数和析构函数。**这就是基于 NRVO 的代码优化。

A MyMethod(A &_hiddenArg, B &var)
{
   _hiddenArg.A::A();
   _hiddenArg.member = var.value + bar(var);
   Return
}

2. 示例代码

#include 
class RVO
{
public:
       
            RVO(){printf("I am in constructor\n");}
            RVO (const RVO& c_RVO) {printf ("I am in copy constructor\n");}
            ~RVO(){printf ("I am in destructor\n");}
            int mem_var;       
};
RVO MyMethod (int i)
{
            RVO rvo;
            rvo.mem_var = i;
            return (rvo);
}
int main()
{
            RVO rvo;
            rvo=MyMethod(5);
}

没有优化的输出

I am in constructor			//main的构造
I am in constructor 		//MyMethod 内的rvo构造
I am in copy constructor	//类似前面讲的 _hiddenArg.A::A(retVal);  // A的拷贝构造函数
I am in destructor			//MyMethod 内的rvo析构
I am in destructor			//函数中的隐藏函数_hiddenArg 析构
I am in destructor			//main中的析构

优化后的输出

I am in constructor			//main的构造
I am in constructor			//_hiddenArg.A::A();
I am in destructor			//函数中的隐藏函数_hiddenArg 析构
I am in destructor          //main中的析构

按照《C++ primer 》第四版第214页所说:“如果返回类型不是引用,在调用函数的地方会将函数返回值复制给临时对象”,这里的_hiddenArg 可以理解成”临时对象“。

3.优化的限制条件

异常

下面情况不会开启NRVO优化

#include 
RVO MyMethod (int i)
{
            RVO rvo;
            rvo.mem_var = i;
            throw "I am throwing an exception!";
            return (rvo);
}
int main()
{
            RVO rvo;
            try 
            {
                        rvo=MyMethod(5);
            }
            catch (char* str)
            {
                        printf ("I caught the exception\n");
            }
}
不同名称的返回值对象

下面情况下不会开启优化,所以为了保证开启NRVO优化,返回时尽量名称一致。

#include 
class RVO
{
public:
       
            RVO(){printf("I am in constructor\n");}
            RVO (const RVO& c_RVO) {printf ("I am in copy constructor\n");}
            int mem_var;       
};
RVO MyMethod (int i)
{
            RVO rvo;
            rvo.mem_var = i;
      if (rvo.mem_var == 10)
         return (RVO());   //改为return rvo 就会开启优化
            return (rvo); 
}
int main()
{
            RVO rvo;
            rvo=MyMethod(5);
}

注意: 这里并不是返回名称一致就一定会有NRVO优化。这取决于编译器。如VS2005在,如果有自定的析构函数,拥有多个返回路径并引入析构函数会在函数中创建 EH 状态。由于编译器跟踪哪些对象需要销毁的复杂性,它避免了返回值优化。(可能后续VS编译会改进此问题)。


总结

**优化的前提条件是所有的返回表达式都一样,并指向同一局部变量。为了增加优化的可能,最好是只有一个返回语句,返回单独一个局部变量。**

回到最开始的提的问题

#include 
#include 
#include 
class RVO
{
public:

            RVO(QString _name = "raw")
                :name(_name)
            {
                qDebug()<<"I am in constructor"<<name;
            }
            RVO (const RVO& c_RVO)
            {
                qDebug()<<"I am in copy constructor"<<name<<c_RVO.name;
            }
            ~RVO()
            {
                qDebug()<<"I am in destructor "<<name;
            }
            int mem_var;
            QString name;
};
RVO MyMethod (int i)
{
    RVO rvo("rvo");
    rvo.mem_var = i;
    return (rvo);
}

void MyMethod2 (RVO&rvo,int i)
{
    rvo.mem_var = i;
}

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    
    {
        RVO rvo;
        qDebug()<<"---";
        rvo=MyMethod(5);
        qDebug()<<"---";
    }

    qDebug()<<"1 ==========="<<endl;

    {
        qDebug()<<"---";
        RVO rvo=MyMethod(5);
        qDebug()<<"---";
    }

    qDebug()<<"2 ==========="<<endl;

    {
        qDebug()<<"---";
        RVO rvo;
        MyMethod2(rvo,5);
        qDebug()<<"---";
    }
    //return a.exec();
}

release 下的输出结果

I am in constructor "raw"
---
I am in constructor "rvo"
I am in destructor  "rvo"
---
I am in destructor  "rvo"
1 ===========

---
I am in constructor "rvo"
---
I am in destructor  "rvo"
2 ===========

---
I am in constructor "raw"
---
I am in destructor  "raw"

RVO rvo=MyMethod(5); 的使用方法与MyMethod2(rvo,5);方法效率一样。
但rvo=MyMethod(5);的写法多了一次构造与析构。怎么选择取决于应用场景。

你可能感兴趣的:(C++,c++)