C++ smart pointer 之速度之討論(二)

Share on:

本文內容採用創用 CC 姓名標示-非商業性-相同方式分享 3.0 台灣 授權條款授權.

在系列的上一篇中,提到了如何使用 shared_ptr 才能增加程式中「函數間互相傳資料」的速度。然而首先,C++ 的 shared_ptr 是 thread-safe 的,有時候我的程式並不需要 thread-safety,但為了維持這個性質可能會產生不少 overhead。其次,前文中使用 shared_ptr 時每次放資料都要自己 push/pop,寫起來也不是很乾淨。有沒有可能自己寫一個簡單的 shared_ptr 來增加效率以及增加使用時的方便性呢?

快速結論

  1. 在我的測試中,即使 shared_ptr 有作到 thread-safe,但並不太會因此影響效能。即使是一個很簡化的實做,也不太會比 gcc 的快,因此本文不放效能分析的內容。
  2. 若要同時兼顧方便性以及效能,不需要自己實做記憶體管理,僅須在 shared_ptr 包裝一層就好了,也就是把前一篇的 new/delete 程式碼包裝在 constructor 跟 deconstructor 裡面。

自幹 my_shared_ptr

概念說明

上方段落中解釋了我的目的,接著就是把目的轉換為設計目標:

  1. 簡單:盡量用越少的程式碼來實做。
  2. 方便性:要有記憶體管理功能,使用上不需要自己從 memory pool 拉 object,或是把用過的 object 推回 memory pool。
  3. 要有 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}

  1. C++ smart pointer 之速度之討論(一)
  2. C++ smart pointer 之速度之討論(二)