浅谈程序效率问题
程序效率是一件很重要的事情,随着处理业务深入,问题的多样化,人们对计算机程序要求越来越高,而用户操作需要越来越简化。这个矛盾体,需要处理的东西要求越多,又要能越来越简单,虽然硬件资源越来越便宜,但如果除去硬件条件情况下,程序效率必须考虑的问题。因此,程序设计和性能越来越考验程序员的功力。
程序如果太庞大太迟缓,不论它的功能有多么的强大,都难以被用户接受。虽然有些程序之所以变得更大,消耗更大的内存,是为了实现超大计算能力,但有太多的程序,其庞大的身驱和迟缓的脚步必须“归功”于懒散草率的编程习惯。
这几天感觉非常郁闷,在做公司一个小项目,前人留下的代码非常杂乱,无设计可言,代码到处重复,变量随意命名,本来很简单的功能,非得写得那么复杂代码,更令人恶心的事,整个工程项目一个注释都没有。全部推翻重写,是不可能的事情,毕竟公司花了钱上面,另外重写时间也不够,我的任务是在这上面加和修改一些功能。相当的无语,这种感觉好像“前人拉陀屎,我来帮他擦屁股”。所以遇到这种情况,只好无奈和自认倒霉......
扯远了点,还是回到效率问题吧。
高性能算法和数据结构虽然很棒,但是草率的实现过程会严重的降低其影响力,最严重的就是“产生和销毁过多的对象”这种情况经常被忽视,而且不容易被辨别出来。多余对象的构造动作和析构对象是程序性能大出血的地方。每一次有非必要的对象被产生和被销毁,便宜流失宝贵的CPU时间。
另外,程序变大变慢,并不只因为产生太多对象。高性能问题还包括程序库的选用及语言特性的施行。
那么,程序效率问题需要注意那些事情?
以下提几点原则
1、2-8原则
一个程序80%资源用于20%代码身上,80%的内存被20%代码占用,80%硬盘访问动用由20%代码执行,80%的维护力气花在20%代码上面。有时候甚至达到1-9法则。这里的数据并不是硬性指标,只说明存在这种事实,我们需要做的工作也是要集中精力,花80%的时间去解决优化20%的代码,解决了20%的代码性能问题,必然会带来整体上性能的提升。
比如,写一个程序来处理/传输或接收硬件数据,有时通过USB来实现,有时通过串口来实现,暂且举串口的例子的吧,这时处理串口数据代码并不是很多,而把硬件的数据转为我们需要的数据可能会写很多的代码,而影响整个程序的性能的地方可能就是串口读写操作部分,串口读写可能是中断模式或者轮询模式,而且受硬件传输速率(一般是波特率)影响,从PC到下位机或者下位机到PC并不是对等的,PC的速度远远快于普通硬件设备。这个时候我们并不能等待数据传输完了,再处理其他事情,否则将浪费很多CPU等待时间.集中把读写操作这块优化好,将整体提升整个程序的效率。如改用多线程操作,分批次队列实现等等。
2、延迟加载
为了实现一些功能,我们经常用声明很多类,类里面包含很多个方法和成员,有时实现过程并不需要全部用到,只需要用少数几个对象,那么这时如果全部加载所有对象,必然需要构造很多的对象,占用了很多的CPU时间和内存,程序必然显得非常缓慢。
上面提到我接手的那个项目就是这种情况,他在主窗口加载时声明所有子窗口对象,进行N次数据库连接操作,而且所有对象都是static,占用内存非常厉害。连续不断多次打开/关闭同样的子窗口内存总在不断增加。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
2 //已经不支持pr6c仪器
3 public static clsDYPR6C curDYPR6C;
4 private bool IsOpenDYPR6C = false;
5 private static string DYPR6CStat = "未连接";
6 private static string DYPR6CRunStat = string.Empty;
7 public static frmAutoTake formAutoTake1;
8 public static frmAutoTake formAutoTake2;
9 public static frmAutoTake formAutoTake3;
10 public static frmAutoTake formAutoTake4;
11 public static frmAutoTake formAutoTake5;
12 public static frmAutoTake formAutoTake6;
13 public static bool IsLoad1 = false;
14 public static bool IsLoad2 = false;
15 public static bool IsLoad3 = false;
16 public static bool IsLoad4 = false;
17 public static bool IsLoad5 = false;
18 public static bool IsLoad6 = false;
19 public static bool IsLoadDY3000I = false;
20 public static frmAutoTakeDY3000I formAutoTakeDY3000I;
21
22 public static bool IsLoadDY5000 = false;
23 public static bool IsLoadDY1000DY = false;
24 public static bool IsLoadDY2000DY = false;
25 public static bool IsLoadDY3000 = false;
26 public static bool IsLoadDY3000DY = false;
27 public static bool IsLoadCheckedComSel = false;
28 public static bool IsLoadCheckedDisplaySel = false;
29 public static bool IsLoadCheckedUpperComSel = false;
30 public static bool IsLoadSFY = false;
31 public static frmAutoTakeSFY formAutoTakeSFY;
32 public static frmMachineEdit formMachineEdit;
33 public static frmAutoTakeDY3000 formAutoTakeDY3000;
34 public static frmAutoTakeDY3000DY formAutoTakeDY3000DY;
35 public static frmAutoTakeDY2000DY formAutoTakeDY2000DY;
36 public static frmAutoTakeDY1000DY formAutoTakeDY1000DY;
37 public static frmAutoTakeDY5000 formAutoTakeDY5000;
38 public static frmAutoTakeDY3000 formAutoTakeDY5000LD;
39 public static frmCheckedComSelect formCheckedComSelect;
40 public static frmCheckedDisplaySelect formCheckedDisplaySelect;
41 public static frmCheckedComSelect formCheckedUpperComSelect;
41个static对象,其中20几个是子窗体对象,并在构造函数时候进行new 主窗体加载并不需要完成其他操作,只是操作相关菜单的时候才会用到。而且他到处这样public static XX 非常随意,严重的破坏了类的封装性,我不知道设计者是基于什么样的考虑。对此我是相当的无语。
我现在把修改为,点击菜单事件时才加进行 new操作,明显感觉改善一些,至少启动程序时不会点用很多内存。
延迟加载包括多方面措施:大对象局部读取、表达式延迟、引用计数
(1).大对象局部读取
程序中使用大型对象,其中包含许多字段。这些对象必须在程序每次执行时保持与前次执行的一致性与连贯性,所以它们经常存储于数据库或文件中(如XML)。每个对象一个独无二的对象识别码,可用来从数据库中取回对象。
示例 代码如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
{
public:
LargeObject(ObjectID id);
const string & field1() const;
int field2() const;
double field3() const;
const string& field4() const;
const string& field5() const;
...
};
考虑从磁盘中存储一个 LargeObject 对象所需要的成本:
void restoreAndProcessObject(ObjectID id)
{
LargeObject obj(id);//构造一个LargeObject对象 obj 用于存储
...
}
由于LargeObject的体积很大,取出此类对象的所有数据,数据库相关操作程序成本很高,特别是如果这些数据必须从远程数据库跨越网络而来。某些情况下,读取所有数据其实不是必须的。
看以下示例:
void restoreAndProcessObject(ObjectID id)
{
LargeObject obj(id);
if(obj.filed2()==0){
cout<<"Object"<<id<<": null field2";
}
}
这里只用到了field2,其他字段所花费的操作都是一种浪费。
那么延迟读取做法是,在产生一个LargeObject对象时,只产生该对象的“外壳”,不从磁盘读取任何字段数据,当对象内的某个字段需要了,程序才从数据库中取回相应的数据,下面做法可以实现这种行为:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
{
public:
LargeObject(ObjectID id);
const string & field1() const;
int field2() const;
double field3() const;
const string& field4() const;
const string& field5() const;
...
private:
ObjectID obj;
mutable string *field1Value;
mutable int *field2Value;
mutable double *field3Value;
mutalbe string *field4Value;
...
};
LargeObj::LargeObject(ObjectID id):obj(id),fieldValue(0),field2Value(0),field3Value(0),...
{
}
const string& LargeObject::field1()const
{
if(field1Value==0)
{
field1Value=getRecordValue("SELECT field1 FROM ....."); //从数据库读取field1字段,同时把field1Value指向它
}
return *field1Value;
}
对象内的每个字段都是指针,指向必要的数据,而LargeObject 构造函数负责将每个指针初始化为null。
如此的null指针表示字段数据未由数据库读入。LargeObject的每一个member function在访问字段指针所指的数据之前,都必须检查指针的状态。如果是null,表示对应的数据应该必须先从数据库读入,然后才能操作该数据。
注意:null指针可能会在任何成员函数内被赋值,以指向真正的数据。然而当你企业在const 成员函数内修改数据成员,编译器不会同意,所以必须显示告诉编译器你的意图,也就是将指针字段声明为mutable,意思是这样的字段可以在任何成员函数内被修改。这就是mutable关键字的作用。
旧式的编译器可能还不能支持mutable。如果不能支持那得有替代方案,且看下面示例:
产生一个 pointer-to-non-const 指向this所指的对象。当你需要修改某个数据成员时,就是通过this指针来修改。
View Code
{
public:
const string& field1()const;//与前面的一样
...
private:
string * fieldValue; //不再声明为mutable,
...
};
const string& LargeObject::field1()const
{
LargeObject* const myThis=const_cast<LargeObject* const>(this);
if(field1Value==0)
{
myThis->fieldValue=getRecordValue("SELECT field1 FROM .....");
}
return *field1Value;
}
声明一个myThis指针,指向this所指对象,并先将该对象的常量性(constness)转化掉。实现与mutable关键字同样的功能。
(2)表达式延迟
表达式缓存,多应用于数值运算当中,矩阵运算,线性计算等等。
示例:
template<class T>
class Matrix{...};//矩阵
Matrix<int> m1(1000,1000);//一个1000*1000矩阵
Matrix<int>m2(1000,1000);//同上
...
Matrix<int>m3=m1+m2;//将m1加上m2;
此例计算会返回m1+m2的和,这是一个大规模运算(1,000,000个加法),还有大量内存分配成本。
表达式缓存策略将设立一个数据结构m3中,用来表示m1和m2的总和,此数据结构可能只是两个指针和一个enum构成,前者向m1和m2,后者表示运算动作是“加法”。
假设之后,在m3被使用之前,程序执行如下动作:
Matrix<int>m4(1000,1000);
...
m3=m4*m1;
现在我们可以忘记m3是m1和m2的总和了,从现在开始可以记录:m3是m4和m1和乘积。不用说了,我们不会立刻执行这个乘法。
假设初始化了m3 为了m1和m2总和之后,这样使用m3
cout<<m3[4];//输出m3第四行
3.使用缓存技术
缓存经常用于处理多次重复使用某一个对象或者数据。其实缓存本质上是以空间换时间的作法。
如果程序常常会用到某个计算,可以降低每次的计算成本,办法就是设计一份数据结构以便能有效地处理需求,那么缓存是首选。
假设:写一个程序用来提供职员信息,而你的预期职员的房间号码在此程序中常常会被使用。更进一步假设,职员的相关信息存储在一个数据库中,为了避免这个特殊的应用程序重复不断地寻找职员房间号码造成对数据的不当压力。
写一个findNumber函数,将它的所找到的房号记录下来,后续再房号查询需求时,如果该号码已经取出了,就可借用高速缓存完成任务,不必再去查询数据库。
int findNumber(const string& employeeName)
{
typedef map<string ,int>CubicleMap;//用这个map作为局部cache
static CubicleMap cubes;
CubicleMap::iterator it=cubes.find(employeeName);
if(it==cubes.end())
{
int cubicle=getValueFromDb("select id from...");//从数据库里取出对应名称的数据值,这个函数另外定义
cubes[employeeName]=cubicle;//加入到cache中
return cubicle;
}
else
{
return (*it).second; //如果存在,直接从cache键对中返回对应的值即可
}
}
相对于数据库查询,cache显然要快得多。
除了caching之外,还有Prefetching(预先取出)。
Prefetching就像购买大量商品时的一个折扣。
没有写完待续。。。。