高併發、低延遲之玩轉CPU快取記憶體(附C#示例)
寫在前面
好久沒有寫部落格了,一直在不斷地探索響應式DDD,又get到了很多新知識,解惑了很多老問題,最近讀了Martin Fowler大師一篇非常精彩的部落格The LMAX Architecture,裡面有一個術語Mechanical Sympathy,姑且翻譯成機器協同程式設計,很有感悟,說的是要把程式設計與底層硬體協同起來,這樣對於開發低延遲、高併發的系統特別地重要,為什麼呢,今天我們就來講講CPU的快取記憶體。
電腦的快取系統
電腦的快取系統分了很多層級,從外到內依次是主記憶體、三級快取記憶體、二級快取記憶體、一次快取記憶體,所以,在我們的腦海裡,覺點磁碟的讀寫速度是很慢的,而記憶體的讀寫速度確是快速的,的確如此,從上圖磁碟和記憶體距離CPU的遠近距離就看出來。這裡要說明一個概念,主記憶體被所有CPU共享,三級快取被同一個插槽內的CPU所共享,單個CPU獨享自己的一級、二級快取。CPU是真正處理事情的地方,它會首先從快取記憶體中去獲取所需的資料,如果找不到,再去三級快取中查詢,如果還是找不到最終就去會主記憶體查詢,如果每一次都這樣來來回回地取資料,那麼無疑是非常耗時。如果能夠把資料快取到快取記憶體中就好了,這樣CPU第一次就可以直接從快取記憶體中命中資料,但是這個地方對我們而言根本不透明,腫麼辦?
探索快取記憶體的構造
我們先來看一張使用魯大師檢測的處理器資訊截圖,如下:
從上圖可以看到,CPU快取記憶體(一、二級)的儲存單元為Line,大小為64 bytes,也就是說無論我們的資料大小是多少,快取記憶體都是以64 bytes為單位快取資料,比如一個8位的long型別陣列,即使只有第一位有資料,每次快取記憶體載入資料的時候,都會順帶把後面7位資料也一起載入,這是底層硬體運作的方式,所以我們要利用這個天然的優勢,讓資料獨佔整個快取行,這樣CPU命中的快取行中就一定有我們的資料。
示例
使用不同的執行緒數,對一個long型別的數值計數500億次。
備註:統計分析圖表和總結在最後。
1. 一般的實現方式
大多數程式設計師都會這樣子構造資料,老鐵沒毛病。
程式碼
///// <summary>
///// CPU偽共享快取記憶體行條目(偽共享)
///// </summary>
public class FalseSharingCacheLineEntry
{
public long Value = 0L;
}
單執行緒
平均響應時間 = 1508.56 毫秒。
雙執行緒
平均響應時間 = 4460.40 毫秒。
三執行緒
平均響應時間 = 7719.02 毫秒。
四執行緒
平均響應時間 = 10404.30 毫秒。
2. 獨佔快取行,直接命中快取記憶體。
2.1 直接填充
程式碼
/// <summary>
/// CPU快取記憶體行條目(直接填充)
/// </summary>
public class CacheLineEntry
{
protected long P1, P2, P3, P4, P5, P6, P7;
public long Value = 0L;
protected long P9, P10, P11, P12, P13, P14, P15;
}
為了保證快取記憶體行中一定有我們的資料,所以前後都填充7個long。
單執行緒
平均響應時間 = 1516.33 毫秒。
雙執行緒
平均響應時間 = 1529.97 毫秒。
三執行緒
平均響應時間 = 1563.65 毫秒。
四執行緒
平均響應時間 = 1616.12 毫秒。
2.2 記憶體佈局填充
作為一個C#程式設計師,必須寫出優雅的程式碼,可以使用StructLayout、FieldOffset來控制class、struct的記憶體佈局。
備註:就是上面直接填充的優雅實現方式而已。
程式碼
/// <summary>
/// CPU快取記憶體行條目(控制記憶體佈局)
/// </summary>
[StructLayout(LayoutKind.Explicit, Size = 120)]
public class CacheLineEntryOne
{
[FieldOffset(56)]
private long _value;
public long Value
{
get => _value;
set => _value = value;
}
}
單執行緒
平均響應時間 = 2008.12 毫秒。
雙執行緒
平均響應時間 = 2046.33 毫秒。
三執行緒
平均響應時間 = 2081.75 毫秒。
四執行緒
平均響應時間 = 2163.092 毫秒。
3. 統計分析
上面的圖表已經一目瞭然了吧,一般實現方式的持續時間隨執行緒數呈線性增長,多執行緒下表現的非常糟糕,而通過直接、記憶體佈局方式填充了資料後,響應時間與執行緒數的多少沒有無關,達到了真正的低延遲。其中直接填充資料的方式,效率最高,記憶體佈局方式填充次之,在四執行緒的情況下,一般實現方式持續時間為10.4秒多,直接填充資料的方式為1.6秒,記憶體佈局填充方式為2.2秒,延遲還是比較明顯,為什麼會有這麼大的差距呢?
刨根問底
在C#下,一個long型別佔8 byte,對於一般的實現方式,在多執行緒的情況下,隸屬於每個獨立執行緒的資料會共用同一個快取行,所以只要有一個執行緒更新了快取行的資料,那麼整個快取行就自動失效,這樣就導致CPU永遠無法直接從快取記憶體中命中資料,每次都要經過一、二、三級快取到主記憶體中重新獲取資料,時間就是被浪費在了這樣的來來回回中。而對資料進行填充後,隸屬於每個獨立執行緒的資料不僅被快取到了CPU的快取記憶體中,而且每個資料都獨佔整個快取行,其他的執行緒更新資料,並不會導致自己的快取行失效,所以每次CPU都可以直接命中,不管是單執行緒也好,還是多執行緒也好,只要執行緒數小於等於CPU的核數都和單執行緒一樣的快速,正如我們經常在一些效能測試軟體,都會看到的建議,執行緒數最好小於等於CPU核數,最多為CPU核數的兩倍,這樣壓測的結果才是比較準確的,現在明白了吧。
最後來看一下大師們總結的未命中快取的測試結果
從CPU到 | 大約需要的 CPU 週期 | 大約需要的時間 |
---|---|---|
主存 | 約60-80納秒 | |
QPI 匯流排傳輸 (between sockets, not drawn) | 約20ns | |
L3 cache | 約40-45 cycles | 約15ns |
L2 cache | 約10 cycles, | 約3ns |
L1 cache | 約3-4 cycles | 約1ns |
暫存器 | 暫存器 |
原始碼參考:https://github.com/justmine66/MDA/blob/master/tests/MDA.Test.Disruptor/FalseSharingTest.cs
寫在最後
如果有什麼疑問,歡迎評論區交流。
如果你覺得本篇文章對您有幫助的話,感謝您的【推薦】。
如果你對硬體協同程式設計方式感興趣的話可以關注我,我會定期的在部落格分享我的學習心得。
做一個有底蘊的軟體工程師
相關文章
- CPU快取記憶體快取記憶體
- 多核cpu、cpu快取記憶體、快取一致性協議、快取行、記憶體快取記憶體協議
- 談談CPU快取記憶體快取記憶體
- CPU快取和記憶體屏障快取記憶體
- iOS開發之記憶體與快取iOS記憶體快取
- Java高併發快取架構,快取雪崩、快取穿透之謎Java快取架構穿透
- Mybatis延遲載入、快取MyBatis快取
- Android記憶體優化之記憶體快取Android記憶體優化快取
- 聊聊高併發系統之HTTP快取HTTP快取
- mybatis延遲載入和快取MyBatis快取
- CPU、記憶體、快取的關係詳細解釋!記憶體快取
- Cgroups控制cpu,記憶體,io示例記憶體
- Nginx多程式高併發、低時延、高可靠機制在twemproxy快取代理中介軟體中的應用Nginx快取
- 高併發Web服務的演變:節約系統記憶體和CPUWeb記憶體
- Nginx多程式高併發、低時延、高可靠機制在滴滴快取代理中的應用Nginx快取
- Glide - 記憶體快取與磁碟快取IDE記憶體快取
- DDD 和 記憶體快取記憶體快取
- 記憶體快取選型記憶體快取
- 坑系列 —— 快取 + 雜湊 = 高併發?快取
- Golang併發之共享記憶體變數Golang記憶體變數
- Java 併發基礎之記憶體模型Java記憶體模型
- docker部署redis快取記憶體DockerRedis快取記憶體
- django 快取表格到記憶體Django快取記憶體
- 高吞吐低延遲Java應用的垃圾回收優化Java優化
- 高併發快取面臨的問題快取
- Java記憶體快取-通過Google Guava建立快取Java記憶體快取GoGuava
- C#獲取CPU佔用率、記憶體佔用、磁碟佔用、程式資訊C#記憶體
- 【高併發】高併發環境下如何防止Tomcat記憶體溢位?看完我懂了!!Tomcat記憶體溢位
- Perfdog 玩轉記憶體洩漏記憶體
- [玩轉MySQL之四]MySQL快取機制MySql快取
- 高效能記憶體快取 ristretto記憶體快取
- MRAM快取記憶體的組成快取記憶體
- C#之快取C#快取
- 併發程式設計與高併發解決方案學習(CPU多級快取-亂序執行優化)程式設計快取優化
- Java併發程式設計之Java記憶體模型Java程式設計記憶體模型
- 啃碎併發(11):記憶體模型之重排序記憶體模型排序
- ASP.NET Core - 快取之記憶體快取(上)ASP.NET快取記憶體
- ASP.NET Core - 快取之記憶體快取(下)ASP.NET快取記憶體