💡 過早優化是萬惡之源。 ——Tony Hoare
作為軟件開發人員的一句名言,相信絕大多數小夥伴都有聽聞過這句名言,而我在最近閲讀netty源碼的時候就見識了這麼一個有趣的例子。
Netty是一個用於構建高性能、可伸縮的網絡應用程序的異步事件驅動框架。它主要關注在網絡通信、協議處理和高性能的特性上,是一個基於Java的開源框架。Netty的設計目標是提供簡單而強大的 API,使得開發者能夠輕鬆地構建各種網絡應用,包括但不限於服務器和客户端。
可能很多開發同學沒有直接使用過netty,但它作為一個優秀的通信框架你很有可能在實際項目中已經在使用了,比如國內流行的rpc框架dubbo底層默認就使用netty作為通信層,此外還有Elasticsearch、RocketMQ、Camel等等中間和框架。
如此優秀的框架非常值得一讀,而我也在閲讀FastThreadLocal中發現了一個有趣的優化。
在 Netty 中,FastThreadLocal 是一種優化過的線程本地存儲(ThreadLocal)實現,用於提供更高性能的線程本地變量訪問。它的設計目標是減小線程本地存儲的性能開銷,特別適用於高併發的網絡應用場景,如 Netty 所涉及的網絡通信框架。
作為其核心原理的InternalThreadLocalMap內有這樣一行
// Cache line padding (must be public)
// With CompressedOops enabled, an instance of this class should occupy at least 128 bytes.
public long rp1, rp2, rp3, rp4, rp5, rp6, rp7, rp8, rp9;
故事今天就是圍繞這一行展開的,眾所周知基於時間局部性和空間局部性原理所以當處理器需要加載內存中的數據時,它會加載整個緩存行,而不僅僅是請求的特定數據,緩存行的大小通常是2的冪,例如64字節。當一個線程修改緩存行中的數據時,整個緩存行都會被標記為"髒",這會導致其他線程中緩存行的數據無效,需要重新加載,這被稱為偽共享。
為了避免偽共享和優化多線程程序的性能,可以使用以下方法:
-
使用
@Contended註解(僅在Java 8及更新版本中可用):該註解可用於類或字段上,可以告訴JVM在生成字節碼時添加填充以避免偽共享,原理是在使用此註解的對象或字段的前後各增加128字節大小的padding,使用2倍於大多數硬件緩存行的大小來避免相鄰扇區預取導致的偽共享衝突,具體可以參考RFR (S): JEP-142: Reduce Cache Contention on Specified Fields
- 緩存行填充(Cache Line Padding):通過在數據結構的末尾添加一些無關的變量,使得不同線程操作的數據不在同一個緩存行上。這樣可以減少偽共享的影響。
而很明顯上面的代碼就屬於第二種,試圖通過行填充解決偽共享,看上去好像沒什麼問題,netty使用各種小技巧的地方也非常多,但這一行在https://github.com/raidyue/netty/commit/ef540815a98dac50769e38b39e5107dc5a313b47 中被改為了
/** @deprecated These padding fields will be removed in the future. */
public long rp1, rp2, rp3, rp4, rp5, rp6, rp7, rp8, rp9;
哎哎哎,剛才還説這一行多麼老道,這會怎麼就要刪除了,提交的log大概是這麼説的
我沒有看到填充有任何明顯的優勢。
唯一受保護的其他字段是對 BitSet 的很少更改的對象引用。
填充也使用“long”,這不一定會阻止 JVM 將上述對象引用放入對齊間隙中。
好嘛,原來只是一個拍腦袋的優化,也並沒有基準測試的數據。
但代碼已經在了刪除的它的風險就很高,所以只是寫了一行註釋要刪除卻沒有真正的刪除它。
甚至於還有後續,在https://github.com/netty/netty/pull/12309 中它被改成了這樣
/** @deprecated These padding fields will be removed in the future. */
public long rp1, rp2, rp3, rp4, rp5, rp6, rp7, rp8;
細心的小夥伴發現少了一個rp9,因為在之前的某次功能合併中InternalThreadLocalMap增加了一個字段
private ArrayList<Object> arrayList;
導致緩存行填充從128字節破壞為136字節。。。
為此又做了一個修復去掉了一個long field,黑魔法用的多確實容易被反噬。,這就是一個活生生的例子。
甚至於最近還有人提到這個事情https://github.com/netty/netty/issues/12312 有開發者認為如果是為了避免偽共享,那麼隨着jdk內存佈局的調整我們應該使用字節字段進行填充而不是long去做填充。netty的作者估計也是不想在這個事情上多做糾纏,明確在主幹分支上已經把這個玩意幹掉了,4.x版本後續也會逐步移除它,沒有更嚴格的基準測試之前我們將不會再做填充。
看完這個系列希望各位同學們慎用黑魔法,放下腦子一熱的優化,多去借鑑借鑑他人優秀的架構與設計 😎寫出更優雅的代碼。
原文地址:https://pebble-skateboard-d46.notion.site/FastThreadLocal-Cac...