More Effective C++ (1)

Posted on 2010-01-18 14:16 rikisand 阅读(324) 评论(0)  编辑 收藏 引用 所属分类: 工作记录~~everydayC/C++

第四章  效率

······条款16 记住80-20准则

大约20%的代码使用了80%的资源,程序的整体性能是由该程序的一小部分代码所决定的~

可行的办法是使用程序分析器(profiler)来找到导致性能瓶颈的拿20%的程序~

而且要针对造成瓶颈的资源来使用相应的分析器~

 

······条款17  考虑使用延迟计算

延迟计算: 也就是说知道程序要求给出结果的时候才进行运算~ 很好理解,和操作系统中的cow copy on write 一个原理~

四个使用场景:

~1~ 引用计数 :

  class String{…};

String s1 = “hello”;

String s2 = s1 ;      //call string Copy ctor

通常情况下,s2赋值后会有一个hello的拷贝,者通常需要使用new操作符分配内存,之后strcpys1

的数据给他,但如果下面的操作如下的话:

cout << s1 ;

cout << s1 + s2;

这种情况下如果只增加s1的引用计数,而s2只是共享s1的值就可以了。只有在需要对s2进行修改或者s1进行修改时,才需要真正拷贝给s2一个副本,引用计数的实现在29条款

~2~区分读写操作

如: String s = “homer’s all”;

cout<< s[3];

s[3] = ‘x’;

在进行读操作时,使用引用计数是开销很小的,然而写操作必须生成新的拷贝。通过条款30的代理类我们可以把判断读写操作推迟到我们能够决定哪个是正确操作的时候

~3~延迟读取

假设程序使用了包含许多数据成员的大对象,这些对象必须在每次程序运行的时候保留下来,因此存进了数据库。某些时候从database中取出所有数据是没有必要的,比如他只访问该对象中的一个数据成员。此时,应该对对象进行处理,只有对象内部某一个特定的数据成员被访问的时候才把他取出来。类似于os中的按需换页~

class LargeObject{

    LargeObject(ObjectID id);

const string& field1() const;

int field2() const;

double field3() const;

const string& field4() const;

private:

ObjectID id;

mutable string* field1value;

mutable int   * fieldValue;

};

LargeObject::LargeObject(ObjectID id):oid(id),fieldValue(0),…{}

const string& LargeObject::field1()const{

   if(fieldValue == 0){

       //read the data for field 1 from database and make field1 point to it

   }

   return  *field1Value;

}

实施lazy fetching 任何成员函数都需要初始化空指针以指向有效数据。但是const成员函数中,试图修改数据编译器会报错。所以声明字段指针为 mutable ,表示任何函数都可以修改,即便在const成员函数中也可以~ 条款28中的智能指针可以让这一方法更灵活

~3~延迟表达式求值

数值计算领域,也在使用延迟计算。例如

matrix<int> m1(1000,1000);

matrix<int> m2(1000,1000);

matrix<int> m3 = m1 + m2;

如果此时计算出m3的话运算量非常之大~

但是如果此时程序为:

m3 = m4*m1;

那么刚才的计算就没必要了

如果cout<< m3[4];

我们只需要计算m3[4]就可以了,其他的值等到确实需要他们的时候才予以计算~如果运气够好的话永远不需要计算~

总结,延迟计算只有当软件在某种程度下可以被避免时候才有意义~只有延迟计算得到的好处大于设计它与实现它花费的精力时才有意义~

·······条款18: 分期摊还预期的计算开销

提前计算~ over-eager evaluation 在系统要求你做某些事情之前就做了他~

例如:大量数据的集合

template<class NumericalType>

class DataCollection}{

public:

  NumericalType min() const;

  NumericalType max() const;

  NumericalType avg() const;

};

使用提前计算,我们随时跟踪目前集合的最大最小平均值,这样 min max avg被调用时候,我们可以不用计算立刻返回正确的数值~~

提前计算的思想便是:如果预计某个计算会被频繁调用,你可以通过设计你的数据结构以更高效的办法处理请求,这样可以降低每次请求的平均开销~

最简单的做法为 缓存已经计算过并且很可能不需要重新计算的那些值~

例如在数据库中存有很多办公室的电话号码,程序在每次查询电话时先查询本地的缓存如果没找到再去访问数据库,并且更新缓存,这样使用缓存平均访问时间要大大减小。

预处理也是一种策略。

例如设计动态数组的时候,当索引下标大于已有最大范围时候,需要new出新的空间,如果申请两倍于索引的大小的话就可以避免频繁的申请操作~~~

 

········条款 19 : 了解临时对象的来源

如果一个对象被创建,不是在堆上,没有名字,那么这个对象就是临时对象。

通常产生于: 为了使函数调用能够成功而进行的隐式转换,或者函数返回对象是进行的隐式转换。由于构造和析构他们带来的开销可以给你的程序带来显著的影响,因此有必要了解他们~

~1首先考虑为了函数调用能通过产生的临时对象的情况

传给某个函数的对象的类型和这个函数所绑定的参数类型不一致的情况下会出现这种情况。

例如:

size_t count(const string& str,char ch);

函数定义为计算str中ch的数量

char buffer[100];

cout<<count(buffer,‘c’);

传入的是一个char数组,此时编译器会调用str的构造函数,利用buffer来创建一个临时对象。

在调用完countChar语句后这个临时对象就被自动销毁了~

仅当传值或者const引用的时候才会发生这样的类型转换~当传递一个非常量引用的时候,不会发生。

void uppercasify(string& str); //change all chars in str to upper case;

在这个例子中使用char数组就不会成功~

因为程序作者声明非常量引用也就是想让对引用的修改反映在他引用的对象身上,但是如果此时生成了临时对象,那么这些修改只是作用在临时对象身上,也就不是作者的本意了。所以c++禁止非常量引用产生临时对象。

~2 函数返回对象时候会产生临时对象

例如: const Number operator + ( const Number& lhs,const Number& rhs);

这个函数返回一个临时对象,因为他没有名字,只是函数的返回值。

条款20中 ,会介绍让编译器对已经超出生存周期的临时对象进行优化

 

········条款20: 协助编译器实现返回值优化

返回值优化:返回带有参数的构造函数。

cosnt Rational operator * (cosnt Rational& lhs,const Rational& rhs){

    return Rational(lhs.numerator()*rhs.numerator(),lhs.denomiator()*rhs.denominator()};

c++允许编译器针对超出生命周期的临时对象进行优化。因此如果调用Rational c=a*b;

c++允许编译器消除operator*内部的临时变量以及operator*返回的临时变量,编译器可以把return表达式所定义的返回对象构造在分配给c的内存上。如果这样做的话那么调用operator*所产生的临时对象所带来的开销就是0~ 我们可以把operator 声明为内联函数而去除调用构造函数带来的开销~

#include <iostream>
#include <string>
#include "time.h"
using namespace std;
char buffer[100];
class number{
public:
    const friend  number operator * (const number& rhs,const number lhs);
    number(){}
    number(int b):a(b){}
    number(const number& rhs){
        a = rhs.a;
    }
      int a;
};
const number operator*(const number& rhs,const number lhs){
    number res;
    res.a = rhs.a * lhs.a;
        return res;
    /*return number(rhs.a*lhs.a);*/
}
//CLOCKS_PER_SEC
int main()
{
    clock_t start = clock();
    number A(5);number B(6);
    for(int i=0;i<100000000;i++)
        number C = A*B;

    clock_t end = clock();
    cout<<double(end-start)/CLOCKS_PER_SEC<<endl;
}

通过上面的程序运行 如果没有返回值优化 运行时间 15.9s 优化后是 10.1s

还是很显著的么 快了33% ,如果这种情况出现在程序的热点处~效果就很好了

 

·········条款21 : 通过函数重载避免隐式类型转换

例子:

class upint{

public:

upint();

upint(int value);

};

cosnt upint operator+(const upint&lhs,const upint&rhs);

upint up1,up2;

upint up3 = up1+up2;

upi3 = up1 +10;

upi4 = 10+ upi2;

这些语句也可以通过,因为创建了临时对象,通过带有int的构造函数产生了临时的upint对象,如果我们不愿意为这些临时对象的产生与析构付出代价,我们需要做什么:

我们声明 cosnt upint operator+(cosnt upint&lhs,int rhs);

cosnt upint operator+(int lhs,const upint& rhs);

就可以去除临时对象产生了~

但是如果我们写了 const upint operator+(int lhs,int rhs); // 错了~

c++规定,每一个被重载的运算符必须至少有一个参数属于用户自定义类型,int并不是自定义类型所以上面的不对的

同样的如果希望string char* 作为参数的函数,都有理由进行重载而避免隐形类型转换(仅仅在有必要的时候,也就是说他们可以对程序效率起到很大帮助的时候~)

··········条款: 考虑使用 op = 来取代 单独的 op运算符

class Rational{

public:

   Rational& operator+=(const Rational& rhs);

   Rational& operator-=(const Rational& rhs);

}

const Rational operator+(cosnt Rational& lhs,const Rational & rhs){

    return Rational(lhs)+=rhs;

}

利用+= -=来实现+ -可以保证运算符的赋值形式与单独使用运算符之间存在正常的关系。

Rational a,b,c,d,result;

result = a+ b+c+d; // 可能要用到3个临时对象

result +=a;result+=b;result+=c; //没有临时对象

前者书写维护都更容易,而且一般来说效率不存在问题,但是特殊情况下后者效率更高更可取

注意:

如果+的实现是这样的:

const T operator+ (constT& lhs,const T&rhs){

     T result(lhs);

     return result += rhs;

}

这个模版中包含有名字对象result,这个对象有名字意味着返回值优化不可用~~~~~~~~·

 

·······条款23 : 考虑使用其他等价的程序库

主旨:

提供类似功能的程序库通常在性能问题上采取不同的权衡措施,比如iostream和stdio,所以通过分析程序找到软件瓶颈之后,可以考虑是否通过替换程序库来消除瓶颈~~~~

 

 

······条款24 : 理解虚函数,多重继承,虚基类以及 RTTI 带来的开销

虚函数表:vtabs 指向虚函数表的指针 vptrs

程序中每个声明了或者继承了的虚函数的类都具有自己的虚函数表。表中的各个项就是指向虚函数具体实现的指针。

class c1{

   c1();

   virtual ~c1();

   virtual void f1();

   virtual int f2(char c)const;

   virtual void f3(const string& s);

};

c1 的虚函数表包括: c1::~c1 c1::f1 c1::f2 c1::f3

class c2:public c1{

   c2();

   virtual ~c2();

   virtual void f1();

   virtual void f5(char *str);

};

它的虚函数表入口指向的是那些由c1声明但是c2没有重定义的虚函数指针:

c2::~c2  c2::f1 c1::f2 c1::f3 c2::f5

所以开销上: 必须为包含虚函数的类腾出额外的空间来存放虚函数表。一个类的虚函数表的大小取决于它的虚函数的个数,虽然每一个类只要有一个虚函数表,但是如果有很多类或者每个类具有很多个虚函数,虚函数表也会占据很大的空间,这也是mfc没有采用虚函数实现消息机制的一个原因。

由于每一个类只需要一个vtbl的拷贝,把它放在哪里是一个问题:

一种:为每一个需要vtbl的目标文件生成拷贝,然后连接时取出重复拷贝

或者:更常见的是采用试探性算法决定哪一个目标文件应该包含类的vtbl。试探:一个类的vtbl通常产生在包含该类第一个非内联,非纯虚函数定义的目标文件里。所以上面c1类的vtbl将放在c1::~c1 定义的目标文件里。如果所有虚函数都声明为内联,试探性算法就会失败,在每一个目标文件就会有vtbl。所以一般忽略虚函数的inline指令。

如果一个类具有虚函数,那么这个类的每一个对象都会具有指向这个虚函数表的指针,这是一个隐藏数据成员vptr~被编译器加在某一个位置。

此处第二个开销:你必须在每一个对象中存放一个额外的指针~

如果对象很小这个开销就十分显著~~因为比例大~

此时 void makeCall(c1* pc1){

   pc1->f1();

}

翻译为 (*pc1->vptr[i])(pc1);

根据vptr找到vtbl 这很简单,

在vtbl找到调用函数对应的函数指针,这个步骤也很简单,因为编译器为虚函数表里的每一个函数设置了唯一的索引

然后调用指针所指向的函数~

这样看来,调用虚函数与普通函数调用的效率相差无几,只多出几个指令。

虚函数真正的开销与内联函数有关~:在实际应用中,虚函数不应该被内联,因为内联意味着在编译时刻用被调用函数的函数体来代替被调用函数。但是虚函数意味着运行时刻决定调用哪个一函数,so~~~虚函数付出的第三个代价啊:~不能内联(通过对象调用虚函数的时候,这些虚函数可以内联,但是大多数虚函数通过指针或者以用来调用的)。

~多重继承的情况

多重继承一般要求虚基类。没有虚基类,如果一个派生类具有多个通向基类的继承路径,基类的数据成员会被复制到每一个继承类对象里,继承类与基类间的每一条路径都有一个拷贝。

有了虚基类,通常使用指向虚基类的指针作为避免重复的手段,这样需要在对象内部嵌入一个或者多个指针~也带来了一定的开销~

例如菱形继承 :

class A{};

class B:virtual public A{};

class C:virtual public A{};

class D:public B,public C{};

这里A是一个虚基类,因为B和C虚拟继承了他。

对象 D 的布局:

B data

vptr

pointer to virtual base class

C data

vptr

pointer to virtual base class

D data members

A data members

vptr

上面四个类,只有三个vptr,因为B和D可以共享一个vptr  (为啥?)

现在我们已经看到虚函数如何使对象变得更大,以及为何不能把它内联了~

下面我们看看RTTI的开销 runtime type identifycation 所需要的开销

通过rtti我们可以知道对象和类的有关信息,所以肯定在某个地方存储了这些供我们查询的信息,这些信息被存储在type_info 类型的对象里,你可以通过typeid运算符访问一个类的type_info对象。

每个类仅仅需要一个RTTI的拷贝,规范上只保证提供哪些至少有一个虚函数的对象的准确的动态类型信息~

why?和虚函数有啥关系~ 因为rtti设计在vtbl里

vtbl的下标0包含指向type_info对象的指针。所以使用这种实现方法,消费的空间是vtbl中占用一个额外的单元再加上存储type_info对象所需要的空间。

 

------------------------罪恶的结束线 OVER~------------------------------------------


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   博问   Chat2DB   管理