C++:编写高效率代码

概述:

C++相比其他高级语言效率高的多,也有许多程序使用C++作为内核以提高程序的性能瓶颈,一个太大太慢的程序他们的优点无论有多么引人注目都不会为人们所接受,尽管有一些程序的确是为了复杂的运算才占用更多的时间和空间,但是更多的程序只能归咎于糟糕的设计和马虎的编程。想用C++写出高效的代码之前,必须认识到C++本身绝对与你所遇到的任何性能上的问题无关。如果想写出一个高效的C++程序,你必须首先写出一个高效的算法。太多的开发人员都忽略了这样一个简单的道理。例如:循环能够被手工展开,移位操作能够代替乘法,传递临时变量时候使用std::move,异常抛出时候对效率的影响,不断的产生临时变量调用构造函数析构函数。但是如果你使用的高层算法内在效率很低, 这些微调就不会有任何作用。

80-20准则:

80-20准则说的是大约20%的代码使用了80%的程序资源 ;大约20%的代码耗用了大约80%的运行时间;大约20%的代码使用了80%的内存;大约20%的代码执行80%的磁盘访问,大约80%的的维护投入大约20%的代码上;通过无数台机器、操作系统和应用程序上的实验这条准则已经被再三验证过。

当程序员力争最大化的提升软件性能时,80-20准则技能简化了你的工作又使你的工作变得复杂。一方面80-20准则标识大多是时间你能够编写性能一般的代码,因为80%的时间里这些代码的效率不会影响到整个系统的性能,这回减少一些你的工作压力。而另一方面这条准则也表示如果你的软件出现了性能问题,你将面临一个困难的工作,因为你不进必须找到导致问题的那一小块代码的位置,还必须寻找到方法提高它们的性能。这些任务中最困难的一般是找到系统瓶颈,基本有两个不同的方法来寻找:大多数人用的方法和正确的方法。

大多数人寻找瓶颈的方法就是猜。通过经验,直觉。一个又一个程序员一本正经的宣称程序性能的瓶颈已经被找到,因为网络的延迟,不正确的内存分配,编译器没有进行足够的优化或者一些拒绝在关键循环里使用汇编语句。这些评估和预言都是错误的。大多数程序员在他们的性能特征上的直觉都是错误的,因为程序性能特征往往不能靠直觉来确定。觉过为提高程序各部分的效率而倾注了大量的精力,但是对程序的整体性能没有显著的影响。例如在程序里使用能够最小化计算量的奇特算法和数据结构,但是如果程序的性能主要限制在IO上,那么就丝毫起不到作用。采用IO性能抢劲的程序库代替编译器本身附加的程序库,如果程序的性能瓶颈主要在CPU上,这种方法也不起什么作用。

正确的方法是用profiler程序识别出程序的20%部分。不是所有工作都让profiler去做。你想让它去直接地测量你感兴趣的资源。例如如果程序太慢,你想让profiler告诉你程序的各个部分都耗费了多少时间。然后你关注局部效率能够被极大提高的地方,这也会将很大地提高整体的效率。

使用懒惰计算法:

(1)引用计数:

1 class String {...};
2 
3 String s1 = "HelloWorld";
4 String s2 = s1;  // 会调用String的构造拷贝函数

通常String的拷贝构造函数让s2被s1初始化以后,s1和s2都有自己对字符串的一份拷贝,这种拷贝会引起较大的开销(这属于热情计算)。

懒惰就是少做工作,不应该让给s2一个s1的拷贝,而是让s2与s1共享一个值。我们只需要记录一下便知道是谁在共享什么,就能够剩掉new和strcpy的开销。这种方法只有在修改某个String时才会有差别,会造成所有共享String的变量值都被修改,为了解决这样的问题,可以在被修改时进行拷贝值,然后再新建一份拷贝的共享。

(2)区别对待读取和写入:

1 String s= "HelloWorld";
2 // ...
4 std::cout << s[3];
5 s[3] = 'x';

有人可能想了,我们可以利用常量性对operator[]重载,使得区分读写动作,使用代码来呈现的话,大致是这样的

1 class String {
2 public:
3     const char &operator[](int idx) const;     // 针对读操作
4     char &operator[](int idx);                   // 针对写操作
5 };

但是在现实中,这是没有用的。编译器在const和no-const之间的选择,只以“调用该函数的对象是否是const为基准”,并不考虑它们在什么情况下被调用。因此

1 String s1, s2;
2 // ...
3 std::cout << s1[5];    // 调用no-const operator[],因为s1不是const
4 
5 s2[5] = 'x';        // 也调用no-const operator[],因为s2不是const
6 s1[3] = s2[8];       // 左右都是调用no-const operator[]

事实证明,重载operator[]不能区分读写操作

我们的想法基于一个事实:虽然或许不知道operator[]是在左值还是右值情境下被调用,我们还是可以区分读写操作-只要我们将动作延缓,直至知道operator[]的返回结果将是如何被使用的。我们需要知道的,就是如何延缓我们的决定(决定对象是被读或者写),直到operator[]返回。

Proxy class(代理类)可以让我们实现想要的效果,示例代码如下:

 1 class CharProxy {
 2 public:
 3     CharProxy(String &str, int idx);
 4     CharProxy &operator=(const CharProxy &rhs); //左值运用
 5     CharProxy &operator](char c); //左值运用
 6     operator char() const;     //右值运用
 7 private:
 8     String &str_;
 9     int idx_;
10 };
11 
12 // 我们将String的operator[]重载以后返回这个代理类
13 // 就可以有效的实现区分读取和写操作
14 
15 String s1, s2;
16 std::cout << s1[5]; //调用CharProxy::operator char() const;
17 s2[5] = 'x';     //调用CharProxy &operator](char c);

使用Proxy class很适合区分operator[]是左值还是右值运用,但是这项技术并非没有缺点,我们希望Proxy class能够无间隙地取代她所代表的对象,但是这样的想法很难实现,因为除了赋值操作,对象极有可能在其他情景被使用,而在那种情况下,Proxy class的表现与真实对象可能会有出入。

1 // 考虑一下代码
2 String s1 = "Hello";
3 char *p = &s1[1]; //错误
4 // 或许我们可以重载&操作符
5 // 可以思考一下还能如何解决

 

(3)懒惰提取:

假如程序使用了一些包含许多大字段的大型对象。这些对象的生存期超越了程序运行期,所以他们必须被储存到数据库。每一个对都有一个唯一的标识符,用来从数据库中重新获得对象

 1 class LargeObject {
 2   public:
 3     LargeObject(ObjectID id);  // 从磁盘中恢复对象
 4     const string &field1();  // 字段的值
 5     // ...
 6 };
 7 
 8 // 现在考虑一下从磁盘中回复LargeObject的开销
 9 void restoreAndProcessObject(ObjectID id) {
10     LargeObject obj(id);  // 恢复对象
11 }
12 
13 // 因为LargeObject对象实例很大,为这样的对象获取所有数据,数据库的操作开销非常大,特别是远程数据库中获取内存,而在这种情况下不需要去读所有数据,例如
14 void restoreAndProcessObject(ObjectID id) {
15     LargeObject obj(id);
16     if(obj.field1() == "x") {
17         std::cout << "xxxx" << std::endl;
18     }
19 }
20 // 这里只需要filed1的值,为其获取其他字段而付出的努力都是白费的
21 // 当LargeObject对象被建立时,不需要从磁盘读取所有数据,这样懒惰法解决了这个问题,不过对象建立时候仅仅是一个壳,而当需要某个数据时,这个数据才被从数据库中取回。

(4)懒惰表达式计算:

1 // 考虑这样的代码
2 template<class T>
3 class Matrix { ... };
4 Matrix<int> m1(1000, 1000);  // 一个1000*1000的矩阵
5 Matrix<int> m2(1000, 1000);
6 // ...
7 Matrix<int> m3 = m1 + m2;
8 // operator+函数计算m1与m2的和。这个计算量相当大
9 // 懒惰表达式计算方法说这样工作太多,所以还是不要去做了。而是应该建立一个数据结构来标识m3的值是m1与m2的和,在用一个enum标识他们之间是加法操作。很明显,建立这个数据结构比m1与m2相加要快许多

(5)分期摊还期望的计算

待写

posted @ 2018-12-28 22:10  wind飘雪  阅读(1920)  评论(0编辑  收藏  举报