在 cpu 處理資料時,happy flow 的順序會先從 memory=>L3(假如有的話)=>L2=>L1=>register=>cpu,把資料從 memory 一路往內搬,然後讓 cpu 來進行處理。而 false sharing 則是指當多個 cpu 明明是處理不同的資料(變數),但卻因為 cpu 處理資料最小單位是 cache line,而這些資料又剛好是在同一個 cache line 裡面時,就會發生所謂的 false sharing 問題。
再簡單點來說,就是各個 cpu 會有自己的 register, L1 甚至 L2,所以同一份 cache line 的資料會被各個 cpu 搬到自己的 cache 裡面。這時候如果其中一個 cpu 修改了 cache line 裡面的資料,目前多核心 cpu 的架構,就會讓其他 cpu 的 cache line 失效,導致這個倒楣的 cpu 需要重新從 memory 或 L3, L2 有共享的地方重新再把資料往內搬,這就是造成慢的原因。
直接來看程式比較容易理解
依我的機器,這段範例程式跑起來大概需要 10 秒鐘左右。但這段程式說白了,也就只是傻瓜版的加總動作,跑到快 10 秒實在是有點慢。
範例程式
int n1 = 0;
int n2 = 0;
var t1 = Task.Run(() =>
{
for (int i = 0; i < 1000000000; i++)
{
n1++;
}
});
var t2 = Task.Run(() =>
{
for (int i = 0; i < 1000000000; i++)
{
n2++;
}
});
Task.WaitAll(t1, t2);
Console.WriteLine($"n1:{n1}, n2:{n2}");
定義一個 struct 把型,然後把裡面型別的 bytes 數位移成 cache line 64 bytes 的倍數。
[StructLayout(LayoutKind.Explicit, Size = 128)]
struct S1
{
[FieldOffset(0)]
public int N1;
[FieldOffset(64)]
public int N2;
}
void Main(string[] args)
{
S1 num = new S1();
var t1 = Task.Run(() =>
{
for (int i = 0; i < 1000000000; i++)
{
num.N1++;
}
});
var t2 = Task.Run(() =>
{
for (int i = 0; i < 1000000000; i++)
{
num.N2++;
}
});
Task.WaitAll(t1, t2);
}
假設是 int 型別,1 個 int 是 4 bytes,而 1 個 cache line 是 64 bytes,這時候故意宣告成陣列,也就是讓 n1 和 n2 在 heap 裡被分配記憶體時隔的夠遠,這樣也可以避免 false sharing 的問題。
int[] n1 = new int[16];
int[] n2 = new int[16];
var t1 = Task.Run(() =>
{
for (int i = 0; i < 1000000000; i++)
{
n1[0]++;
}
});
var t2 = Task.Run(() =>
{
for (int i = 0; i < 1000000000; i++)
{
n2[0]++;
}
});
Task.WaitAll(t1, t2);
Console.WriteLine($"n1:{n1[0]}, n2:{n2[0]}");
因為我用的問題範例程式是用加總來突顯 false shareing 的問題,所以另一個解法也可以在各個 task 裡先自己加總完,最後再把結果放回 n1 or n2 的變數裡。但這就可以 by case 看是不是平行執行的工作要的結果都可以這樣處理。
我自己認為正常的商用軟體或公司內部的系統,這種所謂的 false sharing 應該都不會是問題才對。只有在那種講求極致 or 低層的系統才比較需要去講究。
這個傻瓜版的範例加總程式只是一個示意,只是為了要突顯 false sharing 遇到的原因,其實是資料處理的單位會是 cache line。所以在同一個 cache line 裡面就有可能會同時存放二個 cpu 都同時需要使用到的資料。
在真實世界裡就算真的遇到,除非量大到拖垮效能,不然也不會有人特別注意到。
但越理解它的實際原理,就能更了解 cpu 在拿資料的處理流程。也就更有機會寫出那種講求極致效能的程式。
現代系統的設計如果要求快,思維其實不是單純的把硬體一直往上加快就好(是的話,現在 cpu 的時脈處理速度早就破表了),另一個角度是要從資料的層面去思考。
你的程式 or 架構可以多快把需要的資料送到 cpu 附近,你的處理速度就可以讓你多有感的提升。
一樣的概念,放在各種不同的地方,像 memory locality, 或像套件 redis, mongoDb 等等,他們可以是快取、也可以用來當資料儲存。但對於**client(用的人)**來說,有一個大原則不變,就是越快讓我拿到資料越好。
因為這些快取存放,對 client 來說這些地方已經是這些資料離他最近的地方了。拿到之後,趕快往本機的 memory 放,然後就一路 L3=>L2=>L1=>register 趕快送到 cpu 身邊讓他可以使用跟處理。
所以稍微理解 flase sharing,就可以多了解 cache line 的概念,也就可以再一路往外延伸一些架構的設計靈感~~