提高C++效能的程式設計技術筆記:建構函式和解構函式+測試程式碼
物件的建立和銷燬往往會造成效能的損失。在繼承層次中,物件的建立將引起其先輩的建立。物件的銷燬也是如此。其次,物件相關的開銷與物件本身的派生鏈的長度和複雜性相關。所建立的物件(以及其後銷燬的物件)的數量與派生的複雜度成正比。
並不是說繼承根本上就是程式碼效能的絆腳石。我們必須區分全部計算開銷、必須開銷和計算損失(computional penalty). 全部計算開銷是一次計算中所執行的全部指令的總和。必須開銷是全部指令的子集,它的結果是必要的。這部分計算是必需的,其餘部分即為計算損失。計算損失是可以通過別的設計和實現來消除的那部分計算。
我們不能斷言採用了複雜的繼承的設計一定是壞的,也不能斷定它們總是帶來效能損失。我們只能說總的開銷會隨著派生樹規模的增長而增加。如果所有的計算都是有價值的,那麼它們都是必須的開銷。實際上,繼承層次不見得是完善的,在這種情況下,它們很可能會導致計算損失。
物件的複合與繼承一樣,都引入了與物件建立和銷燬有關的類似效能問題。在物件被建立(或銷燬)時,必須同時建立(或銷燬)它所包含的成員物件。
建立和銷燬被包含物件是另一個值得注意的問題:在建立(或銷燬)被包含物件時無法阻止子物件的建立(或銷燬),因為這是編譯器自動強加的步驟。
效能優化經常需要犧牲一些其它軟體目標,諸如靈活性、可維護性、成本和重用之類的重要目標經常必須為效能讓步。
在C++中,不自覺地在程式開始處預先定義所有物件的做法是一種浪費。因為這樣可能會建立一些直到最後都沒有用到的物件。在C++中,把變數的建立延遲到第一次使用前。
建構函式和解構函式可以像手工編寫的C程式碼一樣有效。然而在實踐中,它們經常包含冗餘計算。
物件的建立(或銷燬)觸發對父物件和成員物件的遞迴建立(或銷燬)。
要確保所編寫的程式碼實際使用了所有建立的物件和這些物件所執行的計算。
物件的生命週期不是無償的。至少物件的建立和銷燬會消耗CPU週期。不要隨意建立一個物件,除非你打算使用它。通常情況下,要等到需要使用物件的地方再建立它。
編譯器必須初始化被包含的成員物件之後再執行建構函式體。你必須在初始化階段完成成員物件的建立。這可以降低隨後在建構函式部分呼叫賦值操作符的開銷。在某些情況下,這樣也可以避免臨時物件的產生。
以下是測試程式碼(constructors_and_destructors.cpp):
#include "constructors_and_destructors.hpp"
#include <iostream>
#include <string>
#include <mutex>
#include <chrono>
namespace constructors_destructors_ {
// reference: 《提高C++效能的程式設計技術》:第二章:建構函式和解構函式
class SimpleMutex { // 單獨的鎖類
public:
SimpleMutex(std::mutex& mtx) : mymtx(mtx) { acquire(); }
~SimpleMutex() { release(); }
private:
void acquire() { mymtx.lock(); }
void release() { mymtx.unlock(); }
std::mutex& mymtx;
};
class BaseMutex { // 基類
public:
BaseMutex(std::mutex& mtx) {}
virtual ~BaseMutex() {}
};
class DerivedMutex : public BaseMutex {
public:
DerivedMutex(std::mutex& mtx) : BaseMutex(mtx), mymtx(mtx) { acquire(); }
~DerivedMutex() { release(); }
private:
void acquire() { mymtx.lock(); }
void release() { mymtx.unlock(); }
std::mutex& mymtx;
};
class Person1 {
public:
Person1(const char* s) { name = s; } // 隱式初始化和顯示賦值
private:
std::string name;
};
class Person2 {
public:
Person2(const char* s) : name(s) {} // 顯示初始化
private:
std::string name;
};
int test_constructors_destructors_1()
{
// 測試三種互斥鎖的實現
// Note:與書中實驗結果有差異,在這裡繼承物件並不會佔用較多的執行時間,在這裡這三種所佔用時間基本差不多
using namespace std::chrono;
high_resolution_clock::time_point time_start, time_end;
const int cycle_number {100000000};
int shared_counter {0};
{ // 1.直接呼叫mutex
std::mutex mtx;
shared_counter = 0;
time_start = high_resolution_clock::now();
for (int i = 0; i < cycle_number; ++i) {
mtx.lock();
++shared_counter;
mtx.unlock();
}
time_end = high_resolution_clock::now();
std::cout<< "time spen1: "<<(duration_cast<duration<double>>(time_end - time_start)).count()<< " seconds\n";
}
{ // 2.不從基類繼承的獨立互斥物件
std::mutex mtx;
shared_counter = 0;
time_start = high_resolution_clock::now();
for (int i = 0; i < cycle_number; ++i) {
SimpleMutex m(mtx);
++shared_counter;
}
time_end = high_resolution_clock::now();
std::cout<< "time spen2: "<<(duration_cast<duration<double>>(time_end - time_start)).count()<<" seconds\n";
}
{ // 3.從基類派生的互斥物件
std::mutex mtx;
shared_counter = 0;
time_start = high_resolution_clock::now();
for (int i = 0; i < cycle_number; ++i) {
DerivedMutex m(mtx);
++shared_counter;
}
time_end = high_resolution_clock::now();
std::cout<< "time spen3: "<<(duration_cast<duration<double>>(time_end - time_start)).count()<<" seconds\n";
}
// 隱式初始化和顯示賦值與顯示初始化效能對比:使用顯示初始化操作要優於使用隱式初始化和顯示賦值操作
{ // 1.隱式初始化和顯示賦值操作
time_start = high_resolution_clock::now();
for (int i = 0; i < cycle_number; ++i) {
Person1 p("Pele");
}
time_end = high_resolution_clock::now();
std::cout<< "隱式初始化, time spen: "<<(duration_cast<duration<double>>(time_end - time_start)).count()<<" seconds\n";
}
{ // 2.顯示初始化操作
time_start = high_resolution_clock::now();
for (int i = 0; i < cycle_number; ++i) {
Person2 p("Pele");
}
time_end = high_resolution_clock::now();
std::cout<<"顯示初始化, time spen: "<<(duration_cast<duration<double>>(time_end - time_start)).count()<<" seconds\n";
}
return 0;
}
} // namespace constructors_destructors_
執行結果如下: