美文网首页
C++ 提高性能手段 —— 临时对象的产生与避免

C++ 提高性能手段 —— 临时对象的产生与避免

作者: 从不中二的忧伤 | 来源:发表于2020-07-09 22:43 被阅读0次
一、临时对象的概念
二、临时对象的产生与避免

一、临时对象的概念

临时对象是 在源码中不可见的,是上的、没有名字的对象。与函数内定义的临时对象有根本差别。

  • 临时对象在源码中不可见,函数内定义的临时对象并不是这里讨论的临时对象:
    非临时对象情况:
int func()
{
    int tmp = 1;    // 这里的 tmp 并不是临时对象,其生命周期在退出函数后结束 
    return tmp; 
}

产生临时对象的情况:

int main()
{
    int i = 1;
    int j = i++;    // 这里的 i++ 会产生临时对象,这里的临时对象是在系统中产生,代码中看不见的
                    // 首先将 i 的值赋给临时对象,再把临时对象的值作为返回结果赋给 j,再对 i 进行自增操作。 
    
    return 0;   
} 
  • 临时对象存放在 栈 上,产生与销毁都不需要在代码中操作。

  • 临时对象的产生与销毁会产生额外的系统开销,所以在代码书写时,应该尽量避免临时对象的产生。


二、临时对象的产生与避免

(以下程序在 C++ 11 环境下编译,并且关闭了编译器的构造优化 -fno-elide-constructors,以分析临时对象的产生)

C++标准允许一种(编译器)实现省略创建一个只是为了初始化另一个同类型对象的临时对象。指定这个参数(-fno-elide-constructors)将关闭这种优化,强制G++在所有情况下调用拷贝构造函数。

并且从C++ 11 起,C++ 14 和 C++ 17 都有不同程度上的复制优化,具体可参考:
https://zh.cppreference.com/w/cpp/language/copy_elision

1、类型转换生成临时对象

例一:

class A
{
public:
    int val;

public:
    A(int v = 0) : val(v)
    {
        cout << "A()\t" << val << endl;
    }
    
    ~A()
    {
        cout << "~A()\t" << val << endl;
    }
    
    A(const A& t) : val(t.val)
    {
        cout << "A(const A& t)" << val << endl;
    }
    
    A& operator=(const A& t)
    {
        val = t.val;
        cout << "A& operator=(const A& t)" << endl;
    } 
    
}; 

int main()
{   
    A a;
    a = 10; // 1. 将 10为参数调用了 A 的构造函数创建了一个 A 类型的临时对象
            // 2. 通过拷贝赋值运算符,将临时对象的成员值赋给了 a
            // 3. 临时对象被销毁,调用 A 的析构函数 
    
    return 0;
}

执行结果:

A()     0   // A a 调用构造函数 
A()     10  // 临时对象调用构造函数,val = 10 
A& operator=(const A& t)    // 调用 拷贝赋值运算符,将临时对象的 tmp.val 赋值给 a.val 
~A()    10  // 销毁临时对象 
~A()    10  // 销毁 a 



优化方法:将 A 的定义与初始化在一行代码中完成:

int main()
{
    A a = 10;
    return 0;
}

C++ 11 环境下执行结果:

A()     10      // 临时对象调用构造函数,val = 10 
A(const A& t)10 // 调用拷贝构造函数,构造对象 a 
~A()    10      // 销毁临时对象 
~A()    10      // 销毁对象 a 

在 C++ 11 的环境下,可以看到,系统通过将对象 a 通过临时对象调用拷贝构造函数,后置构造,省略了一步拷贝赋值的过程。



C++ 17 环境下执行结果:

A()     10      // 系统为 a 预留了空间,直接在预留空间中用 10 构造了对象 a 
~A()    10      // 销毁对象 a 



例二:
隐式类型转换时,产生临时对象

void PrintStr(const string& src)
{
    cout << src << endl;
}

int main()
{
    char mystr[100] = "hello world";
    
    PrintStr(mystr);    // char[] 类型隐式转换成 string 类型,产生临时对象 
                        // src 绑定到了临时对象上 
    
    return 0;
}

将 char[] 类型强制转换成 string 类型,产生 string 类型的临时对象,并通过 mystr 进行赋值。此时调用 PrintStr,传入的实际上是这个 临时对象。也就是说,src 绑定到了临时对象上。

临时对象是右值,可以通过 const 左值引用绑定,但是不允许修改。

如果将 PrintStr 修改成:

void PrintStr(string& src)
{
    cout << src << endl;
}

那么将会编译报错,因为左值引用是不能够绑定右值的。

如果将 PrintStr 修改成:

void PrintStr(string&& src)
{
    cout << src << endl;
}

那么对于本 case,是可以编译通过的,因为可以通过右值引用绑定右值。但是如果此时未经过隐式类型转换,直接传入 string 类型左值,就会编译报错,因为不能通过右值引用绑定左值。

这里要避免产生临时对象,实际上将传入的实参和函数的形参类型保持一致即可:

void PrintStr(const string& src)
{
    cout << src << endl;
}

int main()
{
    string mystr("hello world") ;
    PrintStr(mystr);
    
    return 0;
}
2、类型转换生成临时对象

例一:

A Double(A& src)
{
    A tmp(src.val * 2);
    return tmp;
}

int main()
{
    A a1(10);
    
    A a2 = Double(a1);
    
    return 0;
}

执行结果:

A()     10      // 产生对象 a1,调用构造函数 
A()     20      // 产生对象 tmp,调用构造函数 
A(const A& t)20 // 产生临时对象,调用拷贝构造函数,拷贝对象 tmp 
~A()    20      // 对象 tmp 被销毁 
A(const A& t)20 // 产生对象 a2,调用 拷贝构造函数,拷贝临时对象 
~A()    20      // 临时对象被销毁 
~A()    20      // 对象 a2 被销毁 
~A()    10      // 对象 a1 被销毁 

通过执行结果,可以看出,函数返回对象,会导致临时对象的产生,多了一次拷贝构造函数的产生和一次析构函数的产生。
同时,在此编译环境下,会产生 “从临时对象到对象 a2 的拷贝”,我看了其他人的操作,都没有多出来的这一步,可能是C++版本不同导致(本环境为 Windows C++ 11),待更多尝试验证。

例二:
通过右值引用绑定函数返回的临时对象:

A Double(A& src)
{
    A tmp(src.val * 2);
    return tmp;
}

int main()
{
    A a1(10);
    
    A&& a2 = Double(a1);
    
    return 0;
}

执行结果:

A()     10 
A()     20
A(const A& t)20
~A()    20
~A()    20
~A()    10

通过右值引用绑定 函数返回对象 产生的 临时对象,不会产生从 临时对象到 a2 的拷贝。因为此时是通过 引用 去绑定,不会产生新的对象。

例三:
函数返回对象的引用:

A& Double(A& src)
{
    A tmp(src.val * 2);
    A& tmplr = tmp; 
    return tmplr;
}

int main()
{
    A a1(10);
    
    A& a2 = Double(a1);
    
    cout << "a2.val\t" << a2.val << endl;
    
    A& a3 = Double(a1);
    
    cout << "a3.val\t" << a3.val << endl;
    cout << "a3.val\t" << a3.val << endl;
    cout << "a2.val\t" << a2.val << endl;
    
    return 0;
}

执行结果:

A()     10
A()     20
~A()    20
a2.val  20
A()     20
~A()    20
&a2     0x71fdd0
&a3     0x71fdd0
a3.val  632089152
a3.val  632089152
a2.val  632089152
~A()    10

在这种情况下,函数返回对象的引用,在 Double 函数中,tmplr 所绑定的 tmp 退出函数时已被销毁,但是引用仍然保持可访问的状态。这种引用被称为 悬垂引用
从执行结果可以看出, 引用 a2 和 a3 的地址相同,都是 Double(a1) 返回的引用。但是当访问这个引用的数据时,会发现返回的数据是 不稳定 的,因为实际上此时它们所绑定的对象已经被销毁。
这种 悬垂引用 在代码的书写中,应该尽量避免。

优化方法:
不建议使用 【引用】来减少 构造和析构的开销。建议使用 返回值优化 (RVO)

A Double(A& src)
{
    return A(src.val * 2);
}

int main()
{
    A a1(10);
    
    A a2 = Double(a1);
    
    return 0;
}

理想执行结果:

A()     10
A()     20
~A()    20
~A()    10

但是。。 可能因为编译环境的问题…… 本人实操时的执行结果却是:

A()     10
A()     20
A(const A& t)20
~A()    20
A(const A& t)20
~A()    20
~A()    20
~A()    10

待验证……待验证……

不过幸运的是,现在编译器已经对 消除复制 有了很好的优化,当编译时去掉参数 -fno-elide-constructors,就能够得到最优的优化效果:

A()     10
A()     20
~A()    20
~A()    10

相关文章

  • C++ 提高性能手段 —— 临时对象的产生与避免

    一、临时对象的概念 二、临时对象的产生与避免 一、临时对象的概念 临时对象是 在源码中不可见的,是栈上的、没有名字...

  • C++类---临时对象深入探讨、解析,提高性能手段

    一、临时对象的概念 二、产生临时对象的情况和解决:三种情况和解决方案 CTempValue类的结构 2.1以传值的...

  • C++系列 --- 临时对象深入探讨、解析,提高性能手段

    一、临时对象的概念 二、产生临时对象的情况和解决:三种情况和解决方案 CTempValue类的结构 2.1以传值的...

  • CPU缓存和内存屏障

    CPU性能优化手段 缓存 运行时指令重排 缓存 为了提高程序运行的性能,处理器大多会利用缓存来提高性能,而避免访问...

  • Unity中Lua使用的几个原则

    性能相关: lua中尽量少长期保存C#的对象,如果不可避免建议尽量采用保存lua迭代对象,在传送给C#时再临时构造...

  • C++11之move语义

    要理解c++11的move语义,就需要理解C++中的左值和右值和临时对象的概念。 左值与右值和临时对象的简单介绍:...

  • 结构型设计模式.享元模式

    <主要用于减少创建对象的数量,以减少内存占用和提高性能> 概念理解 定义:采用一个共享来避免大量拥有相同内容对象的...

  • 反射的性能开销都在哪

    1.反射调用过程中会产生大量的临时对象,这些对象会占用内存,可能会导致频繁 gc,从而影响性能。2.反射调用方法时...

  • Android优化

    代码优化 1.避免产生不必要的对象 对象的创建从来都不是免费的. 一个使用线程分配池的通用垃圾回收器可以让临时对象...

  • Avoid async lambda in Parallel

    并行与异步是提升程序性能的常用手段。但是应避免在Parallel.For或者Parallel.ForEach中使用...

网友评论

      本文标题:C++ 提高性能手段 —— 临时对象的产生与避免

      本文链接:https://www.haomeiwen.com/subject/rgcccktx.html