欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

《More Effective C++》学习笔记(四)

程序员文章站 2022-07-05 18:17:42
...

效率 Efficiency

条款 16:谨记80-20法则

    80-20法则就是:一个程序80%的资源用于20%的代码上;

    对于程序的性能特质和查找瓶颈,不能依靠“猜”,可行之道是完全根据观察或实验来识别出造成瓶颈的20%的代码,而辨识之道就是借助某个程序分析器(program profiler);

条款 17:考虑使用lazy evaluation(缓式评估)

    缓式评估有以下几种应用:

   Reference Counting(引用计数)

    String拷贝构造函数的常用做法是,一旦s2 以 s1为值,那么s1和s2都会有各自一份副本,通常会伴随着new operator 分配heap内存,并调用strcpy将s1的数据复制到s2所分配到的内存,这就是急式评估(eager evaluation);

String s1 = "Hello";
String s2 = s1;        //调用String的拷贝构造函数

    让s1分享s2的值,而不再给予s2一个“s1内容副本”,节省“调用new”以及“复制内容”的成本,数据共享引起的唯一危机时在其某个字符串被修改时发生的,不能再拖延了,必须将数据的内容做一个副本;“数据共享”的行动细节在条款29

   区分读和写

    第一个operator [ ]调用动作用来读取字符串的某一部分,而第二个调用动作这执行一个写入动作,而我们希望区分两者,因为对一个引用计数字符串做读取动作,不需要生成副本,但是对这样一个字符串做写入动作,可能需要字符串做一个副本,但是不能判断operator[  ]是在读或写的环境下被调用的,运用缓式评估和条款30描述的代理类,可以延缓决定“究竟是读还是写”;

String s = "Homer";        //假设s是个引用计数的字符串
...
cout<< s[3];               // 调用operator[] 以读取数据s[3]
s[3] = 'x';                // 调用operator[] 将数据写入s[3]

   Lazy Fetching(缓式取出)

    对于每次只是使用其中一部分的数据成员的大型对象,而且取出此类对象的数据,数据库相关操作成本极高,所以,某些情况下,读取所有数据其实不必要的,缓式(lazy)的做法是,在产生对象时,只产生该对象的“外壳”,当对象内某个字段被需要时,程序才从数据库取回相应的数据;
class LargeObject{            //大型对象类
public:
    LargeObject(ObjectID id);
    const string& field1() const;
    int field2() const;
    double field3() const;
    const string& field4() const;
    const string& field5() const;
private:
    ObjectID oid;
    mutable string *fieldValue;        //可变的数据成员(即使是常量成员函数)
    mutable int *field2Value;
    mutable double *field3Value;
    mutable string *field4Value;
};

LargeObject::LargeObject(ObjectID id)            //构造函数:构造时,数据成员都指向null指针,当前状态是个“空壳”
: oid(id), field1Value(0), field2Value(0), field3Value(0), field4Value(0){}

const string& LargeObject::fied1() const{        //当使用到某个成员函数时,才从数据库中对它进行读取操作,然后另指针指向取出的数据成员
    if(field1Value == 0){
        read the data for field 1 from the database and make field1Value point to it;
    }
    return *field1Value;
}

    实现lazy fetching时,null指针可能在任何成员函数(包括常量成员函数)内被赋值,以指向真正的数据,然而在企图在常量成员函数内修改数据成员,那将无法通过编译,所以数据成员应该声明为mutable。如果数据成员使用了智能指针,那么在构造时不需要手动初始化为指向null并且在使用前测试可用性,而且同时不需要声明为mutable了;

   Lazy Expression Evaluation(表达式缓式评估)

    operator+通常采用eager evalution(急式评估),会立即返回运算结果,但大规模运算发生时,会有大量内存分配和计算成本,缓式评估的策略就把表达式的计算延后了,把m3表达为m1 + m2(可能实现的方式是:在m3中设立一个数据结构,数据结构由两个指针和一个enum组成,两个指针分别指向表达式的两个参数(m1,m2),而enum用于指示运算动作是加法),在需要的时候才实际进行运算;
tempalte<class T>
class Martix{ ... };        //同质矩阵

Martix<int> m1(1000, 1000);
Martix<int> m2(1000, 1000);
...
Martix<int> m3 = m1 + m2;    // 将m1 + m2
Martix<int> m4(1000, 1000);
m3 = m4 * m1;                // m3并未使用就改变了,m3如果是使用了缓式评估的做法的话,那么程序的效率就得到了提升
    表达式缓式评估一般上的情况会是求部分的结果,所以缓式评估的做法是不计算m3第五行以外的结果,不过,如果m3所依持的矩阵之中有一个被修改了,那么必须为即将被修改的矩阵做复制动作,保存下即将被修改的变量的旧值;
cout << m3[4];    //输出m3[4];
m3 = m1 + m2;     //记录m3 等于 m1 + m2
m1 = m4;          //m1被改变了,那么m3现在等于m1的旧值加m2

条款 18:分期摊还贷计算成本

    令程序超前进度完成“要求以外”的更多工作,可称为超急评估(over-eager evaluation):在被要求前先把事情做下去;

template<class T>
class DataCollection{    //数据群类
public:
    T min() const;        //返回数据集群最小值
    T max() const;        //返回数据集群最大值
    T avg() const;        //返回数据集群平均值 
};

    这些函数实现的方式有三种,第一种使用eager evaluation,在函数max或avg被调用才检查所有的数据,然后返回检查结果,第二种是使用lazy evaluation,令函数返回某些数据结构,用于在“函数返回时的返回值真正被需要被派上用场”时,决定其适当数值,第三种使用over-eager evaluation,就是随时记录程序执行过程中数据集的最小值,最大值,和平均值,一旦函数被调用,我们可以立即返回正确的值,无须再计算,就能分期(逐次)摊还“随时记录数据群之最大,最小,平均值”的成本,而每次调用所付出的成本,远低于eager evaluation 或lazy evaluation;

    over-eager evaluation 的做法:

    Caching:将“已经计算好而有可能再被需要”的数值保留下来;

int findCubicleNumber(const string& employeeName){
    typedef map<string, int> CubicleMap;
    static CubicleMap cubes;                //用作局部缓存,存储取出来的值
    
    CubicleMap::iterator it = cubes.find(employeeName);
    if(it == cubes.end()){                  //如果并没有之前取出,那么直接从数据库取出,并保留下来
        int cubicle = the result of look up employeeName's cubicle number in the database;
        cubes[employeeName] = cubicle;
        return cubicle;
    }else{                                  //之前取出过了
        return (*it).second;                //直接读取
    }
}

    Prefetching:如果某次的数据被需要,通常邻近的数据也会被需要,这就是locality of reference现象(意指被取用的数据有“位置集中”的倾向),应用方面可以参考STL的Verctor的空间分配策略(容量扩增时总是超前分配多余的内存,以减少内存申请次数);

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

    C++的临时对象(编译器来说)通常是不可见的,只要产生一个non-heap object而没有为它命名,那么就会诞生一个临时对象,匿名对象通常发生于两种情况:一是当隐式转换被施行起来以便函数调用能够成功,二是当函数返回对象时;

    第一种发生在“为了让函数调用成功”而产生的临时对象,发生于“传递某对象一个函数,而其类型与它即将绑定上去的参数类型不同”,当“类型不吻合”时,编译器的做法是产生一个类型为调用函数需要的类型的临时对象;

size_t countChar(const string& str, char ch);
char buffer[MAX_STRING_LEN];
char c;
cout << countChar(buffer, c) << endl;    //buffer类型和函数声明的原型并不一致,以buffer作为自变量调用string 构造函数生成一个临时对象,str绑定到临时对象上

    但是,只有当对象以by value(传值)方式传递,或是当对象被传递给一个reference-to-const参数时,这类转换才会发生,如果对象被传递给reference-to-non-const参数时,并不会发生此类转换(因为假如允许一个临时对象绑定到一个reference-tp-non-const时,那么函数改变的其实是参数绑定的临时对象,而不是临时对象的复制范本);

void f(string& str);
char str[] = "Effective c++";
f(str);                                //错误

    第二种情况发生在当函数返回一个对象时;

条款 20:协助完成“返回值优化(RVO)”

    某些函数返回时会返回一个对象,这将会付出对象的构造和析构的成本;

class Rational{
public:
    Rational(int numerator = 0, int denominator = 0);
    ...
    int numerator() const;
    int denominator() const;
};

const Rational operator*(const Rational& lhs,
                         const Rational& rhs);

    如果返回指针的话,会引起一个问题,是否应该删除此函数返回的指针(通常是应该的),这其中就有资源泄漏的风险(如果忘记删除返回的指针的话);

const Rational* operator*(const Rational& lhs,
                          const Rational& rhs);

    或者可以返回引用, 不过那会导致一个问题,返回值指向一个不再存活的对象,因为在函数返回后,返回值所指向的对象已经被销毁了(栈内存对象);

const Raiton& operator*(const Rational& lhs, 
                        const Rational& rhs);
const Raitonal& Rational::opeartor*(const Rational& lhs, const Rational& rhs){
    Rational result(lhs.numerator() * rhs numerator(), 
                    lhs.denominator() * rhs.denominator());
    return result;
}

    可以用某种特殊的写法来撰写函数,使它在返回对象时,能够让编译器消除临时对象的成本(constructor arguments)。看起来好像是调用了Rational的构造函数,通过此表达式产生了一个Rational的临时对象当作返回值,以constructor arguments取代局部对象,当作返回值使得编译器得以消除“operator*”内的临时对象,及"被operator*返回的临时对象";

const Rational operator*(const Rational& lhs, const Raitonal rhs){
    return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}

条款21:利用重载技术(overload)避免隐式类型转换(implicit type conversions)

class UPInt{
public:
    UPInt();
    UPInt(int value);
    ...
};
const UPInt operator+ (const UPInt& lhs, const UPInt& rhs);
UPInt upi1, upi2;
...
UPInt upi3 = upi1 + upi2;    //调用 UPInt::operator+(const UPInt& lhs, const UPInt& rhs)
upi3 = 10 + upi1;            //发生隐式转换,产生了临时对象
upi3 = up2 + 10;

    upi3 = 10 + upi1 这类句子能被正常调用是因为其中发生了隐式转换,产生了一个临时对象,将整数10转换为UPInts;其中的隐式转换是可以被消除的(如果不希望发生隐式转换产生临时对象的话),而需要做的是声明数个函数,每个函数有不同的参数表:

const UPInt operator+(const UPInt& lhs,
                      const UPInt& rhs);    //将 UPInt 和 UPInt 相加
const UPInt operator+(const UPInt& lhs,
                      int rhs);             //将 UPInt 和 int 相加
const UPInt operator+(int lhs, 
                      const UPInt& rhs);    //将 int 和 UPInt 相加
const UPInt operator+(int lhs, 
                      int rhs);             //错误,每个重载运算符必须获得至少一个“用户定制类型”的自变量
...
UPInt upi1, upi2;
UPInt upi3 = upi1 + upi2;
upi3 = upi1 + 10;        // 不会产生临时对象了
upi3 = 10 + upi2;        // 不会产生临时对象了

    使用函数重载来消除隐式转换带来临时变量的方法不只是可以运用在操作符函数中,还能运用到一般的函数中,但是,如果这不是程序的速度瓶颈(条款16),这不见得是件好事;

条款 22:考虑以操作符复合形式(op =) 取代其独身形式(op)

    operator+= 和 opearator-= 都是具体实现的,而 operator+ 和 operator- 都是调用相应的复合操作符,采用这种设计的话,操作符中只有复合类型才需要维护,此外,如果操作符的类型是在public接口内的,那么就不需要让独身形式成为该类的friends(条款E19),而且复合形式效率比其对应的独身版本效率高(独身版本通常必须返回一个新对象,复合版本直接将结果写入其左端自变量中);

class Rational{
public:
    ...
    Rational& operaotr+=(const Rational& rhs);
    Rational& operator-=(const Rational& rhs);
};

cosnt Rational operator+(const Rational& lhs, const Rational& rhs){
    return Raional(lhs) += rhs;
}

const Rational operator-(const Rational& rhs, const Rational& rhs){
    return Rational(lhs) -= rhs;
}
    如果把所有独身形式操作符放在全局范围内,可以利用模板,完全消除独生形式的撰写的必要,有了模板之后,只要程序中针对类型T定义有一个复合操作符,对应的独生版本就会在编译器内生成,但是,要注意,在某些编译器会把模板内的T(lhs)调用构造函数的动作理解为强制转换,去除lhs的常量性,然后再调用operator+=操作符;
template<class T>
const T operator+(cosnst T& lhs, const T& rhs){
    return T(lhs) += rhs;
}

template<class T>
const T operaotr-(const T& lhs, const T& rhs){
    return T(lhs) -= rhs;
}
...

条款 23:考虑使用其他程序库

    可以使用性能评估软件(benchmark)对程序库进行评估,其结果可以作为程序库运行效率的参考标准;

    iostream 有类型安全(type-safe)特性,并且可扩充,而stdio效率更快,可执行文件通常更小;

    iostream在编译器就决定了操作数的类型,而stdio函数则是在运行期才解析其格式字符串;

条款24:了解virtual functions, multiple inheritance, vitrual base class, runtime type identification的成本

    虚函数实现的细节,大部分编译器是使用所谓的 vitrual tables (虚函数表)和 vitrual table pointers (虚函数表指针),简写为vtbls 和 vptrs,vtbl 通常是一个由“函数指针”架构而成的数组(某些编译器是链表),程序中每一个凡是声明或继承虚函数的类都会有一个自己的 vtabl ,而其中的每个条目(entries) 就是该类各个虚函数实现体的指针;

class C1{
public:
    c1();                    //构造函数
    virtual ~C1();           //析构函数
    virtual void f1();       //虚函数
    virtual int f2(char c) const;
    virtual void f3(const string& s);
    void f4() const;          //普通函数
    ...
};

《More Effective C++》学习笔记(四)

    非虚函数并不会在表格中(f4 和 构造函数),非虚函数包括必定是非虚函数的构造函数会像一般的C函数一样被实现;

    如果在单一继承体系中,子类重新定义了某些继承而来的虚函数,并加上新的虚函数;

class C2 : public C1{
public:
    C2();                          // 非虚函数
    virtual ~C2();                 // 重新定义的虚函数(注意是析构函数,重写发生时,名称和父类并不一致)
    virtual void f1();             // 重新定义的虚函数
    virtual void f5(char *str);    // 新的虚函数
    ...
};

《More Effective C++》学习笔记(四)

    子类的虚函数表和父类的虚函数表并不一致,而且是子类中重写了父类中对应的方法时,子类重写的方法会取代父类被重写的方法的位置;

    虚函数第一个成本:必须为每个拥有虚函数的类耗费一个vtabl空间,其大小视虚函数的个数(包括继承而来的)而定,每个类应该只有一个vtbl;

    虚函数第二个成本:凡声明有虚函数的类,其对象都含有一个隐藏的数据成员(data member),用来指向类的vtbl,这个数据成员就是vptr;

    《More Effective C++》学习笔记(四)

    虚函数第三个成本:虚函数不应该使用inlined,因为“inline”意味着“在编译期,将调用端的调用动作被调用函数的函数体本体取代”,而”virtual“意味着“等待,知道运行事情才知道哪个函数被调用”,所以,如果inling了,编译器就无法知道哪个函数被调用了,等于说,事实上放弃了inlining;

    假如存在多重菱形继承的情况的话;

class A{ ... };                      // 虚基类
class B: virtual public A{ ... };    // 继承虚基类
class C: virtual public A{ ... };    // 继承虚基类
class D: public B, public C{ ... };
《More Effective C++》学习笔记(四)
    如果派生类在其基类有多条继承路径,则此基类的数据成员会在每一个派生类的对象体内复制,每一个副本对应“派生类到基类之间的一条继承路径”,让基类成为虚基类就可以消除这种复制现象,而虚基类因为实现做法利用指针,指向“虚基类成分”,消除复制行为,而对象可能存在一个(或多个)这样的指针;

    运行时期类型识别(runtime type identification, RTTI)的成本,在vtabl数组中,索引为0的条目可能内含一个指针,指向“该vtbl所对应的类”的相应的type_info对象;