第四章 效率
······條款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~------------------------------------------