C++ smart pointer 之速度之討論(二)
在系列的上一篇中,提到了如何使用 shared_ptr
才能增加程式中「函數間互相傳資料」的速度。然而首先,C++ 的 shared_ptr
是 thread-safe 的,有時候我的程式並不需要 thread-safety,但為了維持這個性質可能會產生不少 overhead。其次,前文中使用 shared_ptr
時每次放資料都要自己 push/pop,寫起來也不是很乾淨。有沒有可能自己寫一個簡單的 shared_ptr
來增加效率以及增加使用時的方便性呢?
快速結論
- 在我的測試中,即使
shared_ptr
有作到 thread-safe,但並不太會因此影響效能。即使是一個很簡化的實做,也不太會比 gcc 的快,因此本文不放效能分析的內容。 - 若要同時兼顧方便性以及效能,不需要自己實做記憶體管理,僅須在
shared_ptr
包裝一層就好了,也就是把前一篇的 new/delete 程式碼包裝在 constructor 跟 deconstructor 裡面。
自幹 my_shared_ptr
概念說明
上方段落中解釋了我的目的,接著就是把目的轉換為設計目標:
- 簡單:盡量用越少的程式碼來實做。
- 方便性:要有記憶體管理功能,使用上不需要自己從 memory pool 拉 object,或是把用過的 object 推回 memory pool。
- 要有 template 功能:目標支援物件是固定大小的 object,就是
my_shared_ptr<T>
。
Wikipedia 說,shared_ptr
就是 reference count 的 C++ 的實做,所以要先理解 reference count 是什麼。
顧名思義,reference count 就是數一個 pointer T*
現在被幾個使用者 share 到,像是這張圖:
圖中綠色那個框框就是使用者會實際用到的 my_shared_ptr
。藍色那個框框則是存了真正的 data pointer 跟 counter,我們把他叫做 my_shared_data
好了。
當多一個 my_shared_ptr
指向 my_shared_data
的時候,就把計數 +1 即可。
當少一個 my_shared_ptr
指向的時候,就把計數 -1 即可。
如果 counter 歸零,一般是把資料 free 掉,但是要實做 memory pool 的話,可以把「整個 my_shared_data
」放回 memory pool 裡面。同理,新建一個 my_shared_ptr
要從 memory pool 拿一個 object 起來,並把 counter 設成 1。整體來說,狀態的轉換並不複雜,如下所示:
程式撰寫
從上面的說明我們可以知道 my_shared_data
就是 pointer 跟 count,而 my_shared_ptr
的話,既然都化成箭頭了,應該可以簡單用一個 pointer 做出來。
1template<typename T>
2struct my_shared_data {
3 std::unique_ptr<T> ptr;
4 int counter;
5};
6template<typename T>
7class my_shared_ptr {
8 my_shared_data<T>* shared_;
9};
為了自己管理記憶體,我們必須給每個 class 他一些 static 的資料。第一個 vector private_
是儲存真正的資料,也就是上面圖中的藍色方塊,當資料 allocate 好、放進去這個 private_
之後就不會被 free 掉了,直到整個程式的生命週期結束。
第二個 free_ptrs_
存的是 private_
中的這些藍色方塊的記憶體位置,當一個資料在剛創建好放在 private_
裡面時,他是乾淨的(沒有被 my_shared_ptr
指到),所以該資料的 pointer 會被放在 free_ptrs_
。此外,在 deconstructor 被回收、導致沒有被 my_shared_ptr
指到的資料也會變回乾淨的狀態,而放回 free_ptrs_
。
(註:可能有人想問,這邊 private_
為什麼是 vector of pointer vector<unique_ptr<my_shared_data<T>>>
,難道不能用一個 vector vector<my_shared_data<T>>
就好嗎?答案是不行,因為 free_ptrs_
指向的位置在程式的生命週期中都不能改變,而如果用 vector 的話,該位置會在 vector resize 的時候改變,非常高的機率會產生記憶體錯誤。)
1template<typename T>
2class my_shared_ptr {
3private:
4 my_shared_data<T>* shared_;
5 static std::vector<std::unique_ptr<my_shared_data<T>>> private_;
6 static std::vector<my_shared_data<T>*> free_ptrs_;
7};
當我們要新建立一個 my_shared_ptr
的時候,會先從 free_ptrs_
找看看有沒有剩下乾淨的 pointer,沒有的話就新建立一個。接著從 free_ptrs_
拿出一個乾淨的 pointer,並把 counter 設成 1,像下面這樣:
1my_shared_ptr() {
2 if (free_ptrs_.empty()) {
3 private_.emplace_back(
4 new my_shared_data<T>{std::unique_ptr<T>(new T), 0}
5 );
6 free_ptrs_.emplace_back(private_.back().get());
7 }
8 shared_ = free_ptrs_.back();
9 shared_->counter = 1;
10 free_ptrs_.pop_back();
11}
這樣我們就完成了自製的、自帶記憶體管理的簡單 shared_ptr
了,可惜的是這樣作起來跟 shared_ptr
慢了 0~10%,實際上沒有得到好處。
Rule of five
根據 C++ 的 rule of five,當我們定義了 constructor 的時候,我們也必須定義剩下的 constructor, assignment operator, deconstructor,實做如下所示。
1void incref() {
2 if (shared_ != nullptr) {
3 ++(shared_->counter);
4 }
5}
6void decref() {
7 if (shared_ != nullptr) {
8 if (--(shared_->counter) == 0) {
9 free_ptrs_.push_back(shared_);
10 }
11 shared_ = nullptr;
12 }
13}
14my_shared_ptr(const my_shared_ptr& rhs) {
15 shared_ = rhs.shared_;
16 incref();
17}
18my_shared_ptr(my_shared_ptr&& rhs) {
19 shared_ = rhs.shared_;
20 rhs.shared_ = nullptr;
21}
22my_shared_ptr& operator=(const my_shared_ptr& rhs) {
23 decref();
24 shared_ = rhs.shared_;
25 incref();
26 return *this;
27}
28my_shared_ptr& operator=(my_shared_ptr&& rhs) {
29 decref();
30 shared_ = rhs.shared_;
31 rhs.shared_ = nullptr;
32 return *this;
33}
34~my_shared_ptr() {
35 decref();
36}
系列文連結
-
C++ smart pointer 之速度之討論(一)
-
C++ smart pointer 之速度之討論(二)
- C++ smart pointer 之速度之討論(一)
- C++ smart pointer 之速度之討論(二)