讀書筆記:提高C++性能的編程技術

pdf下載地址:http://download.csdn.net/download/u012662731/10121312

讀書筆記:提高C++性能的編程技術

2014-02-12 20:57 by 付哲, 1083 閱讀, 0 評論, 收藏編輯

Efficient C++ Performance Programming Techniques

第1章 跟蹤範例

1.1 關注點

本章引入的實際問題爲:定義一個簡單的Trace類,將當前函數名輸出到日誌文件中。Trace對象會帶來一定的開銷,因此在默認情況下不會開啓Trace功能。問題是:怎麼設計Trace類,使得在不開啓Trace功能時引入的開銷最小。

1.2 使用狀態變量開關功能

用宏來開關Trace功能很簡單,在不開啓時開銷完全沒有:

#ifdef TRACE
Trace trace("aaa");
#endif

缺點是每次開關都需要重新編譯。

使用狀態變量的話有一定的運行時開銷,但能保證靈活性,是一種比較合理的選擇:

複製代碼
class Trace {
public:
    ...
    static bool isTraceEnabled;
    void Debug() {
        if (isTraceEnabled) {
            ...
        }
    }
}
複製代碼

 

1.3 延遲創建

原本的Trace類中內置string成員,這樣在不開啓Trace時也要承擔構造和析構的開銷。可以將其改爲string*,並在真正需要開啓時再創建該成員。

如果Trace的開啓時間遠小於總時間,則此方法很有效,否則當動態創建的開銷大於固定的1次構造和析構的開銷時,原方法更好一些。

第2章 構造函數和析構函數

2.1 關注點

繼承和合成會導致構造函數和析構函數的開銷超過你的預期。如何在代碼重用性與運行性能間權衡值得關注。

2.2 去冗餘對象

去掉不必要的對象使用,不管是類中的成員變量,還是函數的參數,都會帶來不必要的構造和析構開銷。

2.3 去冗餘基類

有些基類沒有成員變量,也沒有提供接口的作用,這種基類就屬於無意義的基類,在繼承層次中去掉這樣的基類可以減少以下幾項開銷:

  1. 構造和析構的開銷:每個子類的相應過程中都減少了一次函數調用。
  2. 虛表的開銷:減小了每個子類對象的體積,同時允許編譯器將它們放入寄存器中進行優化。
  3. 虛函數的開銷:詳細的見下章,主要是虛函數不能內聯,以及需要額外的跳轉的開銷。

2.4 嵌入對象與嵌入指針的權衡

如果直接嵌入對象的話,對象A的構造會導致對象B和C的構造,會導致對象B1 B2 C1 C2的構造……會形成一個構造樹,層次較多時這個的開銷會很巨大。

嵌入指針的話屬於延遲創建,在第一次使用時才構造,但會帶來new的開銷。若經常使用則直接嵌入對象會好一些。

2.5 延遲創建

這裏寫的是將對象的定義和創建儘量延後,直到所有條件都具備了再進行創建。

2.6 去掉多餘的構造開銷

主要是指不必要的複製:

string s;
s = "a"; // 有一次多餘的賦值開銷,還可能有一次將"a"轉換爲string臨時對象的構造和析構開銷

 

以及不在初始化列表中進行的初始化:

複製代碼
class A {
public:
    A(const string &s)  {
        name = s; //有一次多餘的賦值開銷
    }
private:
    string name;
};
複製代碼

 

第3章 虛函數

3.1 關注點

利用好虛函數的優點:動態綁定,以及節省代碼。儘量避免虛函數帶來的開銷。

3.2 虛函數的開銷

可分爲三種:

  1. 必須在構造函數內初始化vptr:這個相當於是在不使用虛函數的類中內置一個type變量的開銷,是值得的,不討論。
  2. 需要使用指針間接跳轉:相當於在switch中通過type來調用相應版本函數的開銷,不討論。
  3. 虛函數不能內聯:這個是關注點。

3.3 方案1:不繼承

不繼承的話就是將各子類獨立出來,缺點是在代碼中會充斥大量的switch,非常沒有靈活性,排除。

3.4 方案2:繼承

繼承的缺點如3.2.1所述,成員函數無法內聯,尤其是非常短小使用頻繁的函數,會增加大量開銷。

3.5 方案3:模板

使用模板來實現隱式接口:

複製代碼
template <typename LockType>
void func(LockType &lock) {
    lock.Lock();
    ...
    lock.Unlock();
}
複製代碼

 

實現了一個需要有Lock和Unlock的隱式接口。因爲模板是在編譯時確定的,因此生成的函數可以內聯,同時還省去了指針間接跳轉的開銷。

缺點是模板導致的編譯錯誤非常難以調試,同時C++不支持這種隱式接口,開發時經常會弄錯模板的接口要求。

第4章 返回值優化

4.1 關注點

任何時候只要路過了對象的創建和清除,就會獲得性能上的收益。編譯器會在可能時去掉一些臨時對象的創建和清除,這種優化被稱作返回值優化(RVO)。

4.2 編譯器可對匿名對象進行RVO

函數結尾直接返回一個匿名對象往往可以進行RVO:

複製代碼
string Func() {
    string a = "a";
    return a;
}
string FuncRVO() {
    return string("a");
}
複製代碼

 

FuncRVO相比於Func更容易進行RVO,編譯器會去掉返回的對象,而將其值直接賦給接收返回值的對象中。

4.3 主動進行RVO

如果類A有“+”操作如下:

const A operator +(const A &x, const A &y) {
    A z(x);
    z += y;
    return z;
}

 

那麼在A中增加一個構造函數:

class A {
public:
    A(const A &x, const A &y); // A = x + y;
};

 

則將“+”改爲以下形式可獲得RVO收益:

1
2
3
const operator +(const A &x, const A &y) {
    return A(x, y);
}

  

優點是減少了一個臨時對象的構造和析構成本,缺點是要爲所有需要進行RVO的操作分別新增一個類似的構造函數,靈活性太差。如果對性能要求特別高,可以考慮這種優化方法。

第5章 臨時對象

5.1 關注點

如何避免產生不必要的臨時對象。

5.2 類型不匹配

在不同類型間的賦值容易無意中導致臨時對象的創建。可以通過在單參數構造函數前加explicit來避免這種隱式的轉換產生。

5.3 避免重複創建相同的臨時對象

如下循環中:

Complex a;
for (int i = 0; i < 10; ++i) {
    a += 1.0;
}

 

其中每次循環都會創建一個值爲1.0的Complex對象。可以在循環外創建一個值爲1.0的Complex對象,來減少這種開銷:

Complex one(1.0);
for (int i = 0; i < 10; ++i) {
    a += one;
}

 

第6章 單線程內存池

6.1 關注點

默認的通用內存管理器的性能在特定場景下會造成一定的性能瓶頸。本章討論的是在單線程環境下,每次分配固定大小和不固定大小的內存時,實現比通用new/delete性能更好的內存池管理器。

6.2 測試代碼

複製代碼
Rational *array[1000];
for (int j = 0; j < 500; ++j) {
    for (int i = 0; i < 1000; ++i) {
        array[i] = new Rational(i);
    }
    for (int i = 0; i < 1000; ++i) {
        delete array[i];
    }
}
複製代碼

 

6.3 Rational專用內存池

每次分配Rational大小的內存塊,用一個空閒鏈表維護已分配的空閒內存,在釋放時重新將此內存塊放回到鏈表中:

複製代碼
class Rational {
    ...
    static list<char *> freeList;
    void *operator new(size_t size) {
        if (freeList.empty()) {
            return new char[sizeof(Rational)];
        } else {
            void *buf = freeList.back();
            freeList.pop_back();
            return buf;
        }
    }
    void operator delete(void *ptr, size_t size) {
        freeList.push_back(ptr);
    }
};
複製代碼

 

此版本的內存池從不收縮,如果需要釋放內存,則需要新增一個接口。

此版本的內存池與通用內存管理器相比,收益在於:

  1. 不用處理併發情況,沒有臨界區。
  2. 每次分配的大小爲固定值,不用在空閒列表中進行大量的查找(直接返回末端指針)。

6.4 固定大小內存池

不只針對Rational,而是擴展爲支持任意固定大小的類:

複製代碼
template <typename T>
class FixedSizeMemoryPool {
public:
    FixedSizeMemoryPool(): size_(sizeof(T))
    {}
    ~FixedSizeMemoryPool() {
        for(char *&p: freeList_) {
            delete[] p;
        }
    }
    void *Alloc() {
        if (freeList_.empty()) {
            return new char[size_];
        } else {
            void *buf = freeList_.back();
            freeList_.pop_back();
            return buf;
        }
    }
    void Free(void *buf) {
        freeList_.push_back(buf);
    }
private:
    list<char *> freeList_;
    const size_t size_;
};
複製代碼

 

Rational則需要改爲:

複製代碼
class Rational {
public:
    void *operator new(size_t size) {
        return pool.Alloc();
    }
    void operator delete(void *ptr, size_t size) {
        pool.Free(ptr);
    }
private:
    static FixedSizeMemoryPool<Rational> pool;
};
複製代碼

 

6.5 不定大小內存池

不定大小的內存池的管理方法與上面的版本不同,因爲沒有辦法直接從鏈表中返回一個內存塊(大小不同)。這裏我們在需要時分配一個大的固定大小的內存塊,每次分配單個對象的內存時就從這個內存塊上分配,空間不夠時就分配大的內存塊。

隨着通用性的增加,性能也在逐漸下降。因此,在非常需要性能時,犧牲一些靈活性通用性也許會有很好的效果。

第7章 多線程內存池

7.1 關注點

在單線程的內存池中加入互斥鎖,來實現多線程環境下可工作的分配器。在初始化分配器時可以傳入鎖的參數。

7.2 pthread_mutex版的內存池

在上一章MutableSizeMemoryPool中增加一個新的模板參數:typename LockType,允許傳入一個LockType*,並在Alloc和Free時加鎖,其它保持不變。

這一版的分配器的性能並不好,原因是pthread_mutex的性能超出了我們的需要,我們可能只需要一個功能很簡單的鎖。如果能傳入一個更原始版本的互斥鎖的話,會有更好的性能。

7.3 增加內存池的可伸縮性

目前版本的內存池在高併發環境下性能不好,因爲對內存塊的訪問(Alloc和Free)必須要串行化。可以增加多個內存塊的列表,併爲每個列表單獨加鎖,這樣可以把多個請求分散到不同的列表中同時進行處理。

第8章 內聯基礎

8.1 關注點

內聯可能會提高性能,但也可能會降低性能。如何避免負面影響,同時利用好正面收益,是本章的關注點。

8.2 收益:去除函數調用

去除了函數調用的開銷。一般的函數調用包括:

  1. 保存某些寄存器的值到棧上。
  2. 計算參數值並賦給調用函數的對應棧位置上。
  3. 跳轉到指定函數。
  4. 保存棧幀指針。
  5. 執行代碼。
  6. 複製返回值。
  7. 跳轉回原位置。
  8. 恢復寄存器。

另外,還避免了進行跳轉帶來的處理器空轉損失。

8.3 收益:跨函數優化

使得編譯器可以進行跨函數的變量優化:

複製代碼
int Inc(int x) {
    return x + 1;
}
void Func() {
    int y = Inc(1);
}
複製代碼

 

上面的Inc如果內聯的話,Func就相當於:

void Func() {
    int x = 1;
    int y = x + 1;
}

 

編譯器甚至可以進一步優化爲:

void Func() {
    int y = 2;
}

 

通過內聯,編譯器可以重排大量的方法,從中省略掉大量不必要的語句,甚至包括對象的創建和清除。

8.4 收益:縮短關鍵路徑

通過內聯關鍵路徑上的函數,可以在完全不改變程序邏輯的情況下縮短關鍵路徑,從而大幅提高性能。

8.5 損失:編譯後體積

內聯後,函數代碼會展開在每個調用點,如果函數代碼量和調用點都比較多,則會導致代碼體積膨脹。一方面會導致代碼載入速度變慢,另一方面會導致更頻繁的缺頁發生。

但如果函數長度特別短,比整個調用過程還短,那麼內聯後反倒會減小代碼體積,這種函數是一定要內聯的。

8.6 損失:修改代碼後必須重編譯

內聯的函數如果修改了代碼,所有用到它的地方必須重新進行編譯,因爲該函數的代碼已經在各個調用點展開了。因此大型工程往往在收尾時才進行內聯。

第9章 內聯——性能方面的考慮

9.1 關注點

本章主要關注內聯的第2項收益:在內聯函數的代碼展開後,編譯器針對其進行的各項性能優化。

9.2 調用間優化

內聯後,調用函數的代碼與調用處代碼混合,這允許編譯器進行很多高級的優化,如同將代碼重新組織了一樣。尤其是針對直接量進行的優化:

複製代碼
int Choice(int x) {
    switch (x) {
    case 1: return 5; break;
    case 2: ...
    ...
    case 100: return 301; break;
    default: return 0; break;
    }
}
int x = Choice(100);
複製代碼

 

在內聯優化後,上面的代碼可能只剩下:

int x = 301;

 

9.3 爲何不使用內聯

內聯的主要缺點就是可能會增大代碼體積。尤其是當相對龐大的方法被多層內聯時會出現體積指數級膨脹的問題。

缺點2是每次修改需要全部重新編譯。

缺點3是很難對內聯函數進行調試,因爲實際的函數已經沒有了,無法追蹤到函數的入口和出口。

9.4 基於配置的內聯

在內聯前應該統計各個函數的編譯後體積和調用次數、調用點等信息,通過這些配置信息來決定對哪些函數進行內聯。

可以將函數的尺寸分爲:

  1. 靜態尺寸:編譯後的函數指令數*調用點數。
  2. 動態尺寸:運行過程中函數總的指令數(包括每次的調用指令和函數本身的指令)*調用次數。

對靜態尺寸較小而動態尺寸較大的函數,內聯會有很大的收益。對於只有一個調用點的函數,如循環內的調用,內聯幾乎總是對的。而對於調用點和調用次數都很多的函數,最好重寫以展示出其快速路徑,再進行內聯。

某函數如下:

複製代碼
void FuncX {
    if (/* error handle code */) {
        ... // 30 lines
    }
    ... // real work (5 lines)
}
複製代碼

 

FuncX有大約40行代碼,表面上看不適於內聯,但如果將它的錯誤處理代碼拆成一個單獨的函數:

複製代碼
void FuncX {
    if (...)
        FuncY();
    ... // real work (5 lines)
}
void FuncY {
    ... // error handle code (30 lines)
}
複製代碼

 

此時FuncX只有7行代碼,很適合內聯了。這也相當於將靜態尺寸大而動態尺寸小的代碼段拆出去,從而讓剩餘的靜態尺寸小動態尺寸大的代碼可以進行內聯。

9.5 非常適合內聯的函數

  1. 唯一函數:只有一個調用點的函數。
  2. 微小函數:語句少於5行的函數。

第10章 內聯技巧

10.1 關注點

一些可幫助你更好的內聯的技巧。

10.2 條件內聯

如果想用一個預編譯選項來控制某些函數何時內聯,何時關閉內聯,可以使用條件內聯的技巧。

將內聯函數的定義放到.inl中,其它函數的定義放到.cpp中,然後在.h中加入:

#ifdef INLINE
#include "*.inl"
#endif

 

在.inl中加入:

#ifndef INLINE
#define inline  // let inline be void
#endif
inline FuncX(...){}

 

在.cpp中加入:

#ifndef INLINE
#include "*.inl"
#endif

 

10.3 選擇性內聯

可以將某函數在一些調用點處內聯,而在其它調用點處不內聯。具體內容不是很喜歡,略過。

10.4 遞歸內聯

尾遞歸的函數可以改成迭代函數,再尋找內聯方法。

非尾遞歸的函數如果非常在意性能,可以將函數進行一定的展開:

複製代碼
void RecursiveInline() {
    ...
    Recursive();
    ...
}
void Recursive() {
    ...
    RecursiveInline();
    ...
}
複製代碼

 

將前一個函數內聯,這樣會加快運行速度,但也會明顯增加編譯後體積。

也可以手動展開,或是用宏來維護,但很容易出問題。

10.5 特殊體系結構

有些體系結構下(如SPARC),函數調用的開銷會在調用層次較少時非常的低,此時再內聯那些非微小的函數的收益就很不明顯了。因此,任何對非微小函數的內聯都要建立在瞭解配置信息的基礎上。

第11章 標準模板庫

11.1 關注點

  1. STL與不同容器和算法在漸近複雜度方面的性能保證捆綁在一起,是怎麼回事?
  2. STL由許多容器構成,面對一個給定的任務,應該使用哪個?
  3. STL的性能如何?如果自己開發,是否可以做得更好?

11.2 比STL更好

要比STL更好的話,往往要犧牲一定的通用性和靈活性,從一些特定的環境因素着手進行優化。

第12章 引用計數

12.1 關注點

C++使用了引用計數來解決垃圾回收問題,基本思想是把對象清除的責任從客戶端代碼轉移給對象本身。

引用計數可以減少內存使用、避免內存泄漏,但在執行速度方面卻可能會有壞處,尤其是在多線程環境中。

12.2 引用計數的實現

實現A:類內置引用計數。類RefCountBase封裝和引用計數相關的操作,需要實現引用計數的類繼承它:

複製代碼
class RefCountBase {
public:
    Attach() {
        ++refCount_;
    }
    Detach() {
        if (--refCount_ == 0) {
            delete this;
        }
    }
protected:
    RefCountBase(): refCount_(0) {}
    RefCountBase(const RefCountBase &rc): refCount_(0) {}
    RefCountBase &operator=(const RefCountBase &rc) {
        return *this;
    }
    virtual ~RefCountBase() {}
    
    size_t refCount_;
};
複製代碼

 

如類A繼承自RefCountBase,爲了實現引用計數,還需要一個代理類SmartPtr充當A的智能指針:

複製代碼
template <typename T>
class SmartPtr {
public:
    SmartPtr(T *ptr = nullptr): ptr_(ptr) {}
    SmartPtr(const SmartPtr &sptr): ptr_(sptr.ptr_) {
        if (ptr_) {
            ptr_->Attach();
        }
    }
    SmartPtr &operator=(const SmartPtr &sptr) {
        if (sptr.ptr_) {
            sptr.ptr_->Attach();
        }
        if (ptr_)
            ptr_->Detach();
        ptr_ = sptr.ptr_;
        return *this;
    }
    T *operator->() { return ptr_; }
    T &operator*() ( return *ptr_; )
private:
    T *ptr_;
};
複製代碼

 

實現B:將計數功能放入SmartPtr中。去掉RefCountBase,而是在SmartPtr中增加一個size_t *count_,對ptr_的Attach操作變爲++*count_,而Detach操作則變爲--*count_。其它相同。

12.3 併發引用計數

SmartPtr中需要同時對count_和ptr_進行操作,在併發環境下這就意味着需要在操作前後加鎖,來保證對兩個對象的原子操作。

12.4 引用計數的性能

實現A中需要對原類進行修改,如果不能進行這種修改,則只能使用實現B。實現B中因爲需要操作兩個堆上的成員(count_和ptr_),創建和清除性能會比實現A差一些。

引用計數的收益是:

  1. 防止內存泄露。
  2. 高效的賦值操作。尤其是作爲寫時複製(COW)的重要環節,如果賦值後很少有修改操作的話,相比於深複製,引用計數的收益非常明顯。
  3. 節省內存空間。尤其是體積非常大的對象。
  4. 可以方便的實現RAII。將引用計數的Detach操作變爲某種關閉操作,則可很方便地實現RAII。

引用計數的壞處:

  1. COW中如果修改較多,那麼性能相比深複製不一定有提升。
  2. 在併發環境下對它的操作還有鎖的開銷,可能會影響性能比較多。

下列條件會增加引用計數的收益:

  1. 目標對象消耗大量資源。
  2. 資源的分配和釋放很昂貴。
  3. 目標對象高度共享。
  4. 引用的創建和清除很廉價。

第13章 代碼優化

13.1 關注點

應用程序編碼階段會引入很多的性能問題,這類問題通常是小範圍的問題,解決它們不需要看太多的代碼,也不需要改變深層次的設計,但有可能會帶來比較明顯的性能提升。

13.2 緩存

記住頻繁計算和計算代價高的計算結果。比較典型的是將在循環中需要反覆計算的固定結果保存在循環外的一個變量中,並在循環中使用這個變量。

13.3 預先計算

可以將一些在關鍵路徑上需要頻繁用到的計算結果提前進行計算,將結果保存起來,這樣真正使用時只需要簡單的查找就可以了。

13.4 降低靈活性

如果目標代碼使用的範圍很固定,那麼就不需要在代碼中考慮太多的通用情況,而是可以針對目前已知的一些特定情況進行大膽的假設,從而加快運行速度。

13.5 提高常用路徑的速度。

80%的時間消耗在20%的函數調用上,因此儘量降低這20%的函數需要的時間就能大大提高整個系統的性能。

相似的例子出現在if (and1 && and2)以及if (or1 || or2)中,如果兩個條件沒有依賴關係,那麼就將更有可能決定整個關係式值的條件放在前面,即如果and1比and2更容易爲false,那就將and1放在前面,而如果or1比or2更容易爲false,就將or2產在前面。

除了條件值的可能性外,還可以將每個操作的指令數也考慮進去,則可以令整個條件式指令數最小的條件放在前面。

而如果所有外部參數中有5%是特殊的,其它95%是類似的,那麼我們可以單獨爲這5%的特殊參數設計一個路徑,從而加快95%的常見情況的處理速度。

13.6 緩式計算

將計算延遲到真正需要的時候,從而避免昂貴的計算結果最後沒被使用。這節沒什麼新東西。

13.7 無用計算

這節沒什麼新東西。

13.8 體系結構

在設計對象佈局時考慮到體系結構的影響,主要是系統緩存帶來的影響。如矩陣的行長度如果恰好和緩存行長度相等,那麼在進行矩陣轉置時會出現頻繁的緩存未命中。而在設計經常需要一起訪問的兩個成員時,最好讓它們可以處於同一緩存行中,這也需要讓先被訪問的成員放在前面。

13.9 內存管理

性能是一種交易。沒什麼新東西。

13.10 庫和系統調用

很多性能細節都隱藏在庫和系統調用的背後,因此在設計時要詳細瞭解這些細節,並在多個可用的工具中選擇功能剛剛好夠用的那個,它的性能往往也要比那些功能更加完善強大的版本好一些。

13.11 編譯器優化

在release時開啓編譯器優化,可能會有很大的性能提高。

第14章 設計優化

14.1 關注點

設計上的優化是全局的,依賴於其它組件和代碼。

14.2 設計的靈活性

在軟件開發的早期,如果不瞭解程序的熱點,那麼就全面使用STL好了。當對程序的運行有一定了解後,可以用一些靈活性去換取性能。

14.3 緩存:時間戳

web服務中每次請求都需要寫入日誌,並帶有一個時間戳。如果單次請求需要多次寫入,那麼可以將計算出來的時間戳緩存起來供所有這些日誌寫使用。

14.4 緩存:數據擴展

如果對象經常需返回某個操作的值,那麼可以將這個值內嵌在對象中,如各種容器的size等。

14.5 緩存:公用代碼陷阱

如果某段代碼每次都需要判斷請求的類型來決定運行路徑,那麼可以將它拆成兩段代碼分別做單一的操作。這是虛函數很擅長的領域。

14.6 高效的數據結構

沒什麼新東西。

14.7 緩式計算、無用計算和失效代碼

沒什麼新東西。

第15章 可伸縮性

15.1 關注點

並行或併發環境下的性能問題。

15.2 SMP體系

SMP體系的一個性能瓶頸是多個處理器需要共享與內存間的總線。

解決方案是每個處理器配一個大的緩存,但帶來的主要問題是緩存一致性問題。

以上兩個問題導致了實際的並行性能提升難以達到核心數量提升的倍數。

15.3 Amdahl法則

順序計算是通往可伸縮性道路上的主要障礙。單獨加速某一段帶來的提升不會大於這段所佔的總開銷比例。

15.4 分解任務

把單一的任務分解爲多個併發子任務可以提高以下指標:

  1. 請求的響應時間。
  2. 服務器的吞吐量。
  3. CPU使用率。

I/O密集型任務更適合併發執行。

15.5 緩存共享數據

例子:某線程服務於某請求,在線程生命期內,線程的許多操作都要作用於該請求之上。一種思路是在每個操作處調用pthread_getspecific,但這會帶來嚴重的鎖開銷。另一種思路就是在線程開始時獲得一次指針,並傳給隨後的所有函數。

15.6 無共享

上例中,更好的方法是直接將線程相關的東西放到與線程關聯的結構中,這樣可以完全地去掉需要串行化的部分。

15.7 部分共享

在不知道請求數量的時候,可以用固定大小的線程池來進行服務,這樣有着很好的伸縮性。

15.8 鎖的粒度

通常,把多個無關的資源融合到單個鎖的保護之下不是個好主意。例外是滿足以下兩個條件的情況:

  1. 所有的共享資源總是一起被操作。
  2. 其中沒有消耗大量CPU時間的操作。

鎖的粒度太粗會導致並行性下降,而粒度太細又會導致鎖的開銷增加、以及單個任務的處理時間增長。

15.9 僞共享

SMP系統上,兩個鎖如果處於同一緩存行中,那麼p1對m1的鎖操作會導致整個緩存行在p2上失效,從而導致p2訪問m2時要重新讀內存。避免這個問題的方法是手動在m2和m2間插入一定的空白。

15.10 驚鳥

如果用多個線程accept,比如100個,那麼在來連接請求時,100個線程都會醒來,但只有1個線程能獲得請求,其它99個線程轉而繼續睡眠。這種CPU衝擊會導致服務器萎縮並嚴重損害吞吐量。當吞吐量下降時,系統可能會增加更多的線程,從而導致問題更加嚴重。

解決問題的方法是隻用一個線程accept再將請求分發給其它線程。

15.11 讀寫鎖

沒什麼新東西。

第16章 系統體系結構相關性

16.1 關注點

本章討論的東西只簡單的羅列如下:

  1. 內存層次:從寄存器、L1到L2、內存、硬盤,訪問延遲成數量級的增加。其中寄存器的帶寬又比L1大很多。
  2. 正確地讓變量基於寄存器可以使某些編譯器產生的個別方法在性能上提高一個數量級。放在寄存器中的變量在調用方法時要進行保存,但省去了每次訪問時載入到寄存器中的開銷。
  3. 磁盤中的數據一般是用B+樹來保存的,爲的是減少尋道次數。
  4. 編寫代碼時多考慮數據的局部性,可以有效地減少缺頁中斷髮生的頻率,從而大幅提高性能。
  5. C++中同一namespace的代碼往往屬於同一編譯單元,因此相互的局部性更好。因此組織代碼時按照namespace而不用按照文件名來進行。
  6. 性能足夠的時候,永遠選擇更簡單更簡短的解決方案。複雜性是正確性和可維護性的敵人。
  7. 如果SMP中多個處理器都要讀或寫同一緩存行,P1寫後其它處理器都要等待緩存更新完畢才能進行讀,這種等待稱爲緩存顛簸。
  8. 帶有大量跳轉的短代碼序列要比不帶跳轉的長代碼序列更費時間去執行。
  9. 同上,簡單的計算勝過小的分支。
  10. 多線程的程序,如果各個線程間的獨立性不好,有着大量的共享資源,那麼因爲鎖的開銷+線程上下文切換的開銷,可能導致其性能還不如單線程程序。
  11. 上下文切換有三種主要代價:處理器上下文轉移、緩存和TLB丟失、調度開銷。其中緩存的影響可能最大。
  12. 同步操作的開銷往往比異步操作大得多,但異步操作往往需要用輪詢來獲取結果,根據需要再進行權衡。
  13. 對於要求響應延遲最小的程序來說,同步多線程方案要比異步輪詢方案好,因爲異步方案缺少通知手段。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章