最佳化 C++/程式碼最佳化/記憶體訪問
當應用程式訪問主記憶體時,它會隱式使用各種處理器快取和作業系統虛擬記憶體管理器進行的磁碟交換機制。
處理器快取和虛擬記憶體管理器都以塊為單位處理資料,因此如果少數記憶體塊包含單個命令使用的程式碼和資料,則軟體執行速度更快。一個命令處理的資料和程式碼應該駐留在記憶體中的相鄰區域的原則稱為 引用區域性性。
在多核系統上的多執行緒應用程式中,此原則對於效能變得更加重要,因為如果在不同核心上執行的多個執行緒訪問同一個快取塊,則爭用會導致效能下降。
在本節中,提出了一些技術來最佳化處理器快取和虛擬記憶體的使用,方法是增加程式碼和資料的引用區域性性。
在同一個編譯單元中將屬於同一個瓶頸的所有函式定義放在附近。
這樣,編譯這些函式生成的機器程式碼將具有相鄰的地址,從而提高程式碼的引用區域性性。
另一個積極的結果是,這些函式宣告和使用的區域性靜態資料將具有相鄰的地址,從而提高資料的引用區域性性。
但是,不同執行緒使用的資料應該位於不同的快取行,以避免錯誤共享(即快取行反彈)。Gerber/Bik/Smith/Tian 在《軟體最佳化手冊》(第 262 頁)中提出的建議是將此類資料間隔至少 128 位元組(這大於任何 L1/2/3 快取行)。
在中型或大型陣列或集合中,使用聯合體。
聯合體允許在可變型別結構中節省記憶體空間,從而使它們更加緊湊。
但是,不要將它們用於小型或微型物件,因為沒有明顯的空間增益,並且對於某些編譯器,放入聯合體中的物件不會保留在處理器暫存器中。
如果中型或大型物件包含幾個範圍很小的整數,則將它們轉換為位域。
位域減小了物件大小。
例如,代替以下結構
struct {
bool b;
unsigned short ui1, ui2, ui3; // range: [0, 1000]
};
佔用 8 個位元組,您可以定義以下結構
struct {
unsigned b: 1;
unsigned ui1: 10, ui2: 10, ui3: 10; // range: [0, 1000]
};
僅佔用(1 + 10 + 10 + 10 = 31 位,31 <= 32)4 個位元組。
另一個例子,代替以下陣列
unsigned char a[5]; // range: [-20, +20]
佔用 5 個位元組,您可以定義以下結構
struct {
signed a1: 6, a2: 6, a3: 6, a4: 6, a5: 6; // range: [-20, +20]
};
僅佔用(6 + 6 + 6 + 6 + 6 = 30 位,30 <= 32)4 個位元組。
但是,打包和解包欄位會產生效能損失。此外,在最後一個示例中,欄位不再可以透過索引訪問。
需要注意的是,位域的行為沒有很好地定義,因此不同的編譯器會給出不同的結果。位域結構成員不是執行緒安全的,因為記憶體粒度問題,因此在多處理器平臺上併發訪問時具有未定義的行為,這會導致難以檢測的錯誤。因此,許多編譯器會嘗試完全忽略它們。
這種最佳化最適合微控制器。由於這些裝置通常是單處理器裝置,因此併發訪問沒有問題。它們還擁有非常有限的記憶體,因此無法承受浪費的位。此外,許多微控制器具有專門的指令,旨在提高位操作的效能,例如測試位元組中某個偏移量處單個位是否設定的指令。
如果在類模板中,一個非平凡的成員函式不依賴於任何模板引數,則定義一個具有相同主體但不是成員的函式,並將原始函式主體替換為對新函式的呼叫。
假設您編寫了以下程式碼
template <typename T>
class C {
public:
C(): x_(0) { }
int f(int i) { body(); return i; }
private:
T x_;
};
嘗試將上面的程式碼替換為以下程式碼
template <typename T>
class C {
public:
C(): x_(0) { }
int f(int i) { return f_(i); }
private:
T x_;
};
int f_(int i) { body(); return i; }
對於使用該類模板的函式的每個類模板例項化,函式的整個程式碼都被例項化。如果該類模板中的函式不依賴於任何模板引數,則在每次例項化時都會複製函式機器程式碼。這種程式碼複製會使程式膨脹。
在一個類模板或函式模板中,一個大型函式可能有一大部分不依賴於任何模板引數。在這種情況下,首先將該程式碼部分作為單獨的函式分離出來,然後應用此指南。