C++11在时空性能方面的改进

C++11在时空性能方面的改进

这篇我们聊聊C++11在时间和空间上的改进点;
主要包括以下方面:
新增的高效容器:array、forward_list以及unordered containers;
以及常量表达式、静态断言和move语义;

大小固定容器 array

std::array是一个支持随机访问且大小(size)固定的容器,它是c++11中新增的容器。它有如下特点:

  • 不预留多余空间,只分配必须空间(译注:size() == capacity())。
  • 可以使用初始化表(initializer list)的方式进行初始化。
  • 保存了自己的size信息。
  • 不支持隐式指针类型转换。

可以认为它是一个很不错的内建数组类型。示例:

array<int,6> a = { 1, 2, 3 };
a[3]=4;
int x = a[5];    // array的默认数据元素为0,所以x的值变成0
int* p1 = a; // 错误: std::array不能隐式地转换为指针
int* p2 = a.data(); // 正确,data()得到指向第一个元素的指针

可以认为array是一个紧缩版的vector,它比vector高效(没有自动空间分配),但缺少了push_back这样的神器,使得它的使用场景一般是用来替换c++内建的数组类型,而不是vector;

前向列表 forward_list

c++11新增的容器:前向列表 forward_list

前向列表是一个能够在任意位置快速插入和删除的容器(列表都这特性,前向列表当然也具有这特性),但不支持快速随机存取。

它是用单向链表实现的,相比较于它的C实现而言没有什么额外开销。相较于std::list而言,此容器耗费的空间更少,因为它是单向的,不是双向的。

std::forward_list<int> mylist (3,5);   // 3 ints with value 5
for (int& x : mylist ) std::cout << " " << x;

哈希表[无序容器] unordered containers

hash容器在很多之前的编译器中就包含进来了;比如gcc 较早的版本中,它存在于tr1命名空间中;

以unordered_map为例,unordered_map基于散列表实现,元素之间无序存储;

而map基于红黑树实现,元素之间有序(通过operator< 进行比较);

hash版本的查找时间复杂度为O(1),在数据量很大时,比红黑树的版本效率高很多;

对比在C++11中和之前使用上的区别:

// c++0x中:
#include <tr1/unordered_map>
std::tr1:: unordered_map< char,int >  map1;
map1.insert(std::pair<char,int>(‘a‘,100) );

// C++11中:
#include <unordered_map>
std::unordered_map< char,int >  map1;
map1.insert(std::pair<char,int>(‘a‘,100) );

常量表达式 constexpr

编译期计算(Compile-time evaluation):常量表达式

在程序中,有些计算是与运行时无关的;每次执行都是相同的结果;

常量表达式允许让这些计算发生在编译时,而不是在运行时;

这样利用编译时的计算能力,将显著提升程序执行时的效果;

eg:对函数申明为constexpr

constexpr int multiply (int x, int y)
{
    return x * y;
}

// 将在编译时计算
const int val = multiply( 10, 10 );
cin >> x;
// 由于输入参数x只有在运行时确定,所以以下这个不会在编译时计算,但执行没问题
const int val2 = mutliply(x,x);

静态断言 static_assert

static_assert提供一个编译时的断言检查。如果断言为真,什么也不会发生。如果断言为假,编译器会打印一个特殊的错误信息。由于是在编译期间执行的,所以它不会影响运行时的性能;

expression在编译期进行求值,当结果为false(即:断言失败)时,将string作为错误消息输出。例如:

static_assert(sizeof(long) >= 8,
   “64-bit code generation required for this library.”);
struct S { X m1; Y m2; };
static_assert(sizeof(S)==sizeof(X)+sizeof(Y), ”unexpected padding in S”);

static_assert在判断代码的编译环境方面十分有用,比如判断当前编译环境是否64位。但需要注意的是,由于static_assert在编译期进行求值,它不能对那些依赖于运行期计算的值的进行检验。例如:

int f(int* p, int n)
{
      //错误:表达式“p == 0”不是一个常量表达式
      static_assert(p == 0,
          “p is not null”);
}

正确的做法是在运行期进行判断,假如条件不成立则抛出异常;

下面这段代码原本期望只做用于整数类型。

template <typename T1, typename T2>
auto add(T1 t1, T2 t2) -> decltype(t1 + t2)
{
return t1 + t2;
}

但是如果有人写出如下代码,编译器并不会报错

std::cout << add(1, 3.14) << std::endl;
std::cout << add("one", 2) << std::endl;

程序会打印出4.14和”e”。但是如果我们加上编译时断言,那么以上两行将产生编译错误。

template <typename T1, typename T2>
auto add(T1 t1, T2 t2) -> decltype(t1 + t2)
{
   static_assert(std::is_integral<T1>::value, "Type T1 must be integral");
   static_assert(std::is_integral<T2>::value, "Type T2 must be integral");

   return t1 + t2;
}

error C2338: Type T2 must be integral
see reference to function template instantiation ‘T2 add<int,double>(T1,T2)‘ being compiled
   with
   [
      T2=double,
      T1=int
   ]
error C2338: Type T1 must be integral
see reference to function template instantiation ‘T1 add<const char*,int>(T1,T2)‘ being compiled
   with
   [
      T1=const char *,
      T2=int
   ]

move语义和右值引用

move语义和右值介绍

左值就是一个有名字的对象,而右值则是一个无名对象(临时对象)。move语义允许修改右值(以前右值被看作是不可修改的,等同于const T&类型)。

void incr(int& a) { ++a; }
int i = 0;
incr(i);    // i变为1
//错误:0不是一个左值
incr(0);
// 0不是左值,无法直接绑定到非const引用:int&。
// 假如可行,那么在调用时,将会产生一个值为0的临时变量,
// 用于绑定到int&中,但这个临时变量将在函数返回时被销毁,
// 因而,对于它的任何更改都是没有意义的,
// 所以编译器拒绝将临时变量绑定到非const引用,但对于const的引用,
// 则是可行的
”&&”表示“右值引用”。右值引用可以绑定到右值(但不能绑定到左值):

X a;
X f();
X& r1 = a;        // 将r1绑定到a(一个左值)
X& r2 = f();    // 错误:f()的返回值是右值,无法绑定
X&& rr1 = f();    // OK:将rr1绑定到临时变量
X&& rr2 = a;    // 错误:不能将右值引用rr2绑定到左值a

考虑如下函数:

template<class T> swap(T& a, T& b) // 老式的swap函数
    {
        T tmp(a);// 现在有两份"a"
        a = b;      // 现在有两份"b"
        b = tmp;    // 现在有两份tmp(值同a)
    }

如果T是一个拷贝代价相当高昂的类型,例如string和vector,那么上述swap()操作也将煞费气力;我们的初衷其实并不是为了把这些变量拷来拷去,我是仅仅想将变量a,b,tmp的值做一个“移动”(即通过tmp来交换a,b的值)。

移动赋值操作背后的思想是,“赋值”不一定要通过“拷贝”来做,还可以通过把源对象简单地“偷换”给目标对象来实现。例如对于表达式s1=s2,我们可以不从s2逐字拷贝,而是直接让s1“侵占”s2内部的数据存储;

我们可以通过move()操作符来实现源对象的“移动”:

template <class T>
void swap(T& a, T& b)  //“完美swap”(大多数情况下)
{
      T tmp = move(a);  // 变量a现在失效(译注:内部数据被move到tmp中了)
      a = move(b);      // 变量b现在失效(译注:内部数据被move到a中了,变量a现在“满血复活”了)
      b = move(tmp);    // 变量tmp现在失效(译注:内部数据被move到b中了,变量b现在“满血复活”了)
}

move(x) 意味着“你可以把x当做一个右值”,把move()改名为rval()或许会更好,但是事到如今,move()已经使用很多年了。在C++11中,move()模板函数以及右值引用被正式引入。

将拷贝改进成移动操作,减少创建不必要的对象,节省了对象的空间分配消耗和构造析构的调用;

move对算法中的改进

基于move的std::sort()和std::set::insert()要比基于copy的对应版本快15倍以上。不过它对标准库中已有操作的性能改善不多,因为它们的实现中已经使用了类似的方法进行优化了(例如string,vector使用了调优过的swap操作来代替copy了)。当然如果你自己的代码中包含了move操作的话,就能自动从新标准库中获益了。

move对容器的改进

在C++11的标准库中,所有的容器都提供了移动构造函数和移动赋值操作符,那些插入新元素的操作,如insert()和push_back(), 也都有了可以接受右值引用的版本。最终的结果是,在没有用户干预的情况下,标准容器和算法的性能都提升了,而这些都应归功于拷贝操作的减少。

以vector为例,定义“移动构造函数(move constructors)”和“移动赋值操作符(move assignments”来“移动”而非复制它们的参数:

template<class T> class vector {
        // …
        vector(const vector&);  // 拷贝构造函数
        vector(vector&&);   // 移动构造函数
        vector& operator= (const vector&); // 拷贝赋值函数
        vector& operator =(vector&&);  // 移动赋值函数
}; //注意:移动构造函数和移动赋值操作符接受
// 非const的右值引用参数,而且通常会对传入的右值引用参数作修改

容器新增了move版的构造和赋值函数后,它最重要的内涵就是允许我们高效的从函数中返回一个容器:

vector<int> make_random(int n)
        {
            vector<int> ref(n);
            // 产生0-255之间的随机数
                for(auto x& : ref) x = rand_int(0,255);

                return ref;
        }

        vector<int> v = make_random(10000);
        for (auto x : make_random(1000000)) cout << x << ‘\n‘;

上边代码的关键点是vector没有被拷贝操作(vector ref的内存空间不会在函数返回时被stack自动回收了,move assignment通过右值引用精巧的搞定了这个问题)。对比我们现在的两种惯用法:在自由存储区来分配vector的空间,我们得负担上内存管理的问题了;通过参数传进已经分配好空间的vector,我们得要写不太美观的代码了。

原地安置操作 Emplace operations

在大多数情况下,push_back()使用移动构造函数(而不是拷贝构造函数)来保证它更有效率,不过在极端情况下我们可以走的更远。为何一定要进行拷贝/移动操作?为什么不能在vector中分配好空间,然后直接在这个空间上构造我们需要的对象呢?做这种事儿的操作被叫做”原地安置”(emplace,含义是:putting in place)。

举一个emplace_back()的例子:

vector<pair<string,int>> vp;
string s;
int i;
while(cin>>s>>i) vp.emplace_back(s,i);

emplace_back()接受了可变参数模板变量并通过它来构造所需类型。至于emplace_back()是否比push_back()更有效率,取决于它和可变参数模板的具体实现。如果你认为这是一个重要的问题,那就实际测试一下。否则,就从美感上来选择它们吧。

参考

http://www.stroustrup.com/C++11FAQ.html

https://www.chenlq.net/books/cpp11-faq

Posted by: 大CC | 07SEP,2015

博客:blog.me115.com [订阅]

Github:大CC

时间: 2024-10-02 20:42:36

C++11在时空性能方面的改进的相关文章

提高SDN控制器的拓扑发现性能(通过改进逻辑)

最近做实验,发现ryu的拓扑发现性能不高,因此阅读了ryu拓扑发现源码,查阅了相关论文,改进了ryu的拓扑发现性能,改写了代码,并且重新做了实验,发现改进的拓扑发现模块能力有所增强,但是还是有局限性,现在先对前面的工作做个总结,以后有时间了继续改进. 一.LLDP拓扑发现原理 传统网络中的链路发现协议为LLDP(Link Layer Discovery Protocol),LLDP允许局域网中的结点告诉其他结点他自己的capabilities和neighbours.在传统以太网交换机中,交换机从

《C++Primer》12、13章

第12章 动态内存 12.1 智能指针 shared_ptr<T> make_shared<T>(args) 直接初始化 new int(10); 默认初始化 new int; 值初始化 new int(); 由内置指针(而不是智能指针)管理的动态内存在被显示释放前一直都会存在. 最好坚持只使用智能指针: delete之后重置指针值为nullptr: unique_ptr u = nullptr 释放u指向的对象,将u置为空 u.release()  u放弃对指针的控制权,返回指针

802.11协议精读12:初探协议性能

序言 在初始的802.11协议版本之后,陆续更新的802.11e,以及802.11n以及更新的802.11技术,其都是基于改善当前802.11协议的缺陷不断进行改进的.为了理解这些改进,我们首先要理解802.11存在的一些问题,其中一个主要的问题就是性能问题. 本文我们先简单介绍802.11中一个常见问题,即路由器的宣称速率不等于实际速率的问题,然后我们具体分析一下这个宣称速率(即物理层速率)的计算方法.在后面一篇文章中,我们会介绍用数学方法对该吞吐量具体进行估计的方法,即Bianchi模型.

802.11协议精读13:协议理论性能(Bianchi模型)

序言 为了更好理解一些802.11的后续设计,我们需要深入了解一下802.11的协议性能.我们之前简单描述了下协议性能的部分,这一段我们讨论下具体数学模型下的802.11性能(Bianchi模型). Bianchi模型出自于论文<Performance Analysis of the IEEE 802.11 Distributed Coordination Function>,该论文在2000年的时候发表在JSAC(IEEE Journal on selected areas in commu

GitHub的DGit改进了平台的可靠性、性能以及可用性

GitHub最近悄悄地发布了DGit,全称为“分布式Git”.这是一种基于Git创建的分布式存储系统,其目标是改进使用GitHub时的可靠性.可用性以及性能. DGit是一个应用层面的协议,它利用了Git分布式的特性,将每个仓库在三台不同的.独立选择的服务器上保留三个备份.按GitHub所说,这个简单的架构在可靠性.可用性和性能方面带来了许多直接的好处. 考虑到托管某个仓库的三台服务器是互相独立的,那么他们同时变得不可用的可能性非常低. 用户的请求可在这三台服务器之间进行负载均衡处理,由于这些请

C++11多线程教学(一)

转载自:http://www.cnblogs.com/lidabo/p/3908705.html 本篇教学代码可在GitHub获得:https://github.com/sol-prog/threads. 在之前的教学中,我展示了一些最新进的C++11语言内容: 1. 正则表达式(http://solarianprogrammer.com/2011/10/12/cpp-11-regex-tutorial/) 2. raw string(http://solarianprogrammer.com/

提升网页性能

之前做pc端的时候基本不是特别会特别去关注页面尤其是js的性能,即使做也是主要针对图片,最近做手机端的一个座位图页,大概有1000个左右作为,要根据触摸滑动来实时改变位置,大小,颜色,等等.在谷歌浏览器那叫一个流畅~当在安卓机上实验时,简直卡爆了,于是..优化,花了将近1个星期时间,主要是针对js的优化,因为数据是ajax实时取来生成的.本来打算就这次优化做一次简单的总结,无意中看到一篇好文章,把我用到的没用到的基本都总结了,于是特此分享出来. 当然其实关于js有几点特别需要注意的是,如果是to

[转] 优化反射性能的总结(上)

反射是一种很重要的技术,然而它与直接调用相比性能要慢很多,因此如何优化反射性能也就成为一个不得不面对的问题. 目前最常见的优化反射性能的方法就是采用委托:用委托的方式调用需要反射调用的方法(或者属性.字段). 那么如何得到委托呢? 目前最常见也就是二种方法:Emit, ExpressionTree .其中ExpressionTree可认为是Emit方法的简化版本, 所以Emit是最根本的方法,它采用在运行时动态构造一段IL代码来包装需要反射调用的代码, 这段动态生成的代码满足某个委托的签名,因此

[转] Python 代码性能优化技巧

选择了脚本语言就要忍受其速度,这句话在某种程度上说明了 python 作为脚本的一个不足之处,那就是执行效率和性能不够理想,特别是在 performance 较差的机器上,因此有必要进行一定的代码优化来提高程序的执行效率.如何进行 Python 性能优化,是本文探讨的主要问题.本文会涉及常见的代码优化方法,性能优化工具的使用以及如何诊断代码的性能瓶颈等内容,希望可以给 Python 开发人员一定的参考. Python 代码优化常见技巧 代码优化能够让程序运行更快,它是在不改变程序运行结果的情况下