[英]C++ weak_ptr creation performance
我已經讀過創建或復制std :: shared_ptr涉及一些開銷(引用計數器的原子增量等)。
但是如何從它創建一個std :: weak_ptr:
Obj * obj = new Obj();
// fast
Obj * o = obj;
// slow
std::shared_ptr<Obj> a(o);
// slow
std::shared_ptr<Obj> b(a);
// slow ?
std::weak_ptr<Obj> c(b);
我希望在一些更快的性能,但我知道共享指針仍然必須遞增弱引用計數器..所以這仍然像將shared_ptr復制到另一個慢?
這是我與游戲引擎的日子
故事如下:
我們需要一個快速的共享指針實現,不會破壞緩存(緩存現在更聰明btw)
正常指針:
XXXXXXXXXXXX....
^--pointer to data
我們的共享指針:
iiiiXXXXXXXXXXXXXXXXX...
^ ^---pointer stored in shared pointer
|
+---the start of the allocation, the allocation is sizeof(unsigned int)+sizeof(T)
用於計數的unsigned int*
位於((unsigned int*)ptr)-1
這樣一個“共享指針”是指針大小,它包含的數據是指向實際數據的指針。 所以(因為template =>
inline而且任何編譯器都會內聯一個運算符返回一個數據成員)它與普通指針的訪問權限相同。
創建指針需要比正常情況多3個CPU指令(訪問位置-4正在運行,添加1和寫入位置-4)
現在我們在調試時只使用弱指針(因此我們使用DEBUG定義(宏定義)進行編譯)因為那時我們希望看到所有的分配和最新情況等等。 這很有用。
弱指針必須知道他們指向的東西何時消失,不要保持他們指向的東西(在我的情況下,如果弱指針保持分配活着,引擎永遠不會回收或釋放任何記憶,那么它基本上是無論如何共享指針)
所以每個弱指針都有一個bool, alive
或者什么東西,並且是shared_pointer
的朋友
調試我們的分配時看起來像這樣:
vvvvvvvviiiiXXXXXXXXXXXXX.....
^ ^ ^ the pointer we stored (to the data)
| +that pointer -4 bytes = ref counter
+Initial allocation now
sizeof(linked_list<weak_pointer<T>*>)+sizeof(unsigned int)+sizeof(T)
您使用的鏈表結構取決於您關注的內容,我們希望保持盡可能接近sizeof(T)(我們使用伙伴算法管理內存),因此我們存儲了指向weak_pointer的指針並使用了xor技巧。 ... 美好時光。
無論如何:指向shared_pointers指向的東西的弱指針放在一個列表中,以某種方式存儲在上面的“v”中。
當引用計數達到零時,您將瀏覽該列表(這是指向實際weak_pointers的指針列表,當它們顯然被刪除時它們將自行刪除)並且您將alive = false(或其他內容)設置為每個weak_pointer。
weak_pointers現在知道他們指向的東西不再存在(所以在被引用時扔掉了)
在這個例子中
沒有開銷(系統的對齊是4個字節.64位系統往往喜歡8個字節的對齊....在那里將ref-counter與int [2]結合在一起以填充它。記住這個涉及到新聞(沒有人投票,因為我提到它們:P)等等。你需要確保你對分配施加的struct
與你分配和制作的struct
相匹配。編譯器可以自己對齊東西(因此int [2]不是int, INT)。
您可以完全取消引用shared_pointer,而不需要任何開銷。
正在制作的新共享指針根本不會破壞緩存並且需要3個CPU指令,它們不是很容易管道但是編譯器總是會內聯getter和setter(如果不是總是:P)那么'將成為可以填充管道的呼叫站點周圍的東西。
共享指針的析構函數也很少(遞減,就是這樣),所以很棒!
高性能筆記
如果你有這樣的情況:
f() {
shared_pointer<T> ptr;
g(ptr);
}
無法保證優化器不敢通過“按值”將shared_pointer傳遞給g來進行加法和減法。
這是您使用普通引用(實現為指針)的地方
所以你要做g(ptr.extract_reference());
相反 - 編譯器將再次內聯簡單的getter。
現在你有一個T&,因為ptr的范圍完全包圍g(假設g沒有副作用等等),該引用在g的持續時間內有效。
刪除引用是非常難看的,你可能不會偶然做到(我們依賴這個事實)。
事后來看
我本應該創建一個名為“extracted_pointer”的類型,或者很難為類成員輸入錯誤的類型。
stdlib ++使用的弱/共享指針
http://gcc.gnu.org/onlinedocs/libstdc++/manual/shared_ptr.html
不是那么快......
但是不要擔心奇怪的緩存未命中,除非你制作一個游戲引擎沒有運行不錯的工作量> 120fps:P仍然比Java更好。
stdlib方式更好。 每個對象都有自己的分配和工作。 使用我們的shared_pointer
這是一個真實的例子,“相信我的工作,不要擔心如何”(不是很難),因為代碼看起來非常混亂。
如果你解除了......他們對他們實現中的變量名稱做了什么,它會更容易閱讀。 參見Boost的實現,正如文檔中所述。
除了變量名之外,GCC stdlib實現很可愛。 你可以很容易地閱讀它,它可以正常工作(遵循OO原則)但速度稍慢,並且可能會在最近蹩腳的芯片上破壞緩存。
UBER高性能筆記
您可能在想,為什么不擁有XXXX...XXXXiiii
(最后的引用計數)然后您將得到最好的分配器對齊!
回答:
因為必須做pointer+sizeof(T)
可能不是一個CPU指令! (減去4或8是CPU可以輕松完成的事情,因為它有意義,它會做很多事情)
除了Alec對他之前項目中使用的shared / weak_ptr系統的非常有趣的描述之外,我還想詳細介紹一下典型的std::shared_ptr/weak_ptr
實現可能發生的事情:
// slow
std::shared_ptr<Obj> a(o);
上述結構的主要費用是分配一塊內存來保存兩個引用計數。 這里不需要進行原子操作(除了在operator new
下執行可能會或可能不會執行的operator new
)。
// slow
std::shared_ptr<Obj> b(a);
復制構造中的主要開銷通常是單個原子增量。
// slow ?
std::weak_ptr<Obj> c(b);
這個weak_ptr
構造函數的主要開銷通常是單個原子增量。 我希望這個構造函數的性能幾乎與shared_ptr
復制構造函數的性能相同。
另外兩個要注意的重要構造函數是:
std::shared_ptr<Obj> d(std::move(a)); // shared_ptr(shared_ptr&&);
std::weak_ptr<Obj> e(std::move( c )); // weak_ptr(weak_ptr&&);
(以及匹配的移動賦值運算符)
移動構造函數根本不需要任何原子操作。 他們只是將引用計數從rhs復制到lhs,並使rhs == nullptr。
僅當賦值之前的lhs!= nullptr時,移動賦值運算符才需要原子遞減。 大部分時間(例如在vector<shared_ptr<T>>
)移動賦值之前的lhs == nullptr,因此根本沒有原子操作。
后者( weak_ptr
移動成員)實際上不是C ++ 11,而是由LWG 2315處理。 但是我希望它已經被大多數實現實現(我知道它已經在libc ++中實現)。
當在容器中搜索智能指針時,例如在vector<shared_ptr<T>>::insert/erase
,將使用這些移動成員,並且與使用智能指針復制成員相比,可以產生可測量的積極影響。
我指出它,以便你知道如果你有機會移動而不是復制一個shared_ptr/weak_ptr
,那么輸入一些額外的字符是值得的。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.