GC 的 STW 問題#
GC,垃圾回收器,本質(zhì)上是一種能夠自動(dòng)管理自己分配的內(nèi)存的生命周期的內(nèi)存分配器。這種方法被大多數(shù)流行編程語言采用,然而當(dāng)你使用垃圾回收器時(shí),你會(huì)失去對(duì)應(yīng)用程序如何管理內(nèi)存的控制。C# 允許在自動(dòng)控制內(nèi)存的基礎(chǔ)之上局部對(duì)內(nèi)存進(jìn)行手動(dòng)控制,但是自動(dòng)控制仍然是主要的場(chǎng)景。
然而 GC 總是需要暫停程序的運(yùn)行以遍歷和識(shí)別存活的對(duì)象,從而刪除無效對(duì)象以及進(jìn)行維護(hù)操作(例如通過移動(dòng)對(duì)象到更緊湊的內(nèi)存區(qū)域以減少內(nèi)存碎片,這個(gè)過程也叫做壓縮)。GC 暫停整個(gè)程序的行為也叫做 STW(Stop-The-World)。這個(gè)暫停時(shí)間越長(zhǎng),對(duì)應(yīng)用的影響越大。
長(zhǎng)期以來,.NET 的 GC 都一直在朝著優(yōu)化吞吐量性能和內(nèi)存占用的方向不斷優(yōu)化,這對(duì)于 Web 應(yīng)用以及跑在容器中的服務(wù)而言非常適合。而在客戶端、游戲和金融領(lǐng)域,開發(fā)人員一直都需要格外注意代碼中的分配問題,例如使用對(duì)象池、值類型以及非托管內(nèi)存等等,避免產(chǎn)生大量的垃圾和各種 GC 難以處理的反模式,以此來減少 GC 的單次暫停時(shí)間。例如在游戲中,要做到 60fps,留給每一幀的時(shí)間只有 16ms,這其中如果 GC 單次暫停時(shí)間過長(zhǎng),用戶就會(huì)觀察到明顯的掉幀。
Workstation GC?Server GC?DATAS GC?#
.NET 一直以來都有兩種 GC 模式 —— Workstation GC 和 Server GC。
Workstation GC 是 .NET 最古老的 GC 模式,其目標(biāo)之一是最小化內(nèi)存占用,以適配資源有限的場(chǎng)景。在 Workstation GC 中,它只會(huì)利用你一個(gè) CPU 核心,因此哪怕你有多核的計(jì)算資源,Workstation GC 也不會(huì)去使用它們來優(yōu)化分配性能,雖然 Workstation GC 同樣支持后臺(tái)回收,但即使開啟后臺(tái)回收,Workstation GC 也之多只會(huì)用一個(gè)后臺(tái)線程。這么一來其性能發(fā)揮就會(huì)受到不小的限制。面對(duì)大量分配和大量回收?qǐng)鼍皶r(shí) Workstation GC 則顯得力不從心。不過,當(dāng)你的應(yīng)用很輕量并且不怎么分配內(nèi)存的時(shí)候,Workstation GC 將是一個(gè)很適合的選擇。
而之后誕生的 Server GC 則可以有效的利用多核計(jì)算資源,根據(jù) CPU 核心數(shù)量來控制托管堆數(shù)量,大幅度提升了吞吐量。然而 Server GC 的缺點(diǎn)也很明顯——內(nèi)存占用大。另外,Server GC 雖然通過并發(fā) GC 等方式將一部分工作移動(dòng)到 STW 之外,從而使得 GC 和應(yīng)用程序可以同時(shí)運(yùn)行,讓 STW 得到了不小的改進(jìn),然而 Server GC 的暫停時(shí)間仍然稱不上優(yōu)秀,雖然在 Web 服務(wù)等應(yīng)用場(chǎng)景下表現(xiàn)得不錯(cuò),然而在一些極端情況下則可能需要暫停上百毫秒。
為了進(jìn)一步改善 Server GC 的綜合表現(xiàn),.NET 9 引入了新的 DATAS GC,試圖在優(yōu)化內(nèi)存占用的同時(shí)提升暫停時(shí)間表現(xiàn)。這個(gè) GC 通過引入各種啟發(fā)算法自適應(yīng)應(yīng)用場(chǎng)景來最小化內(nèi)存占用的同時(shí),也改善了暫停時(shí)間。測(cè)試表明 DATAS GC 相比 Server GC 雖然犧牲了個(gè)位數(shù)百分比的吞吐量性能,卻成功的減少了 70%~90% 的內(nèi)存占用的同時(shí),暫停時(shí)間也縮減到 Server GC 的 1/3。
然而,這仍然不能算是完美的解決方案。開發(fā)者們都是抱著既要又要還要的心理,需要的是一個(gè)既能做到大吞吐量,暫停時(shí)間又短,同時(shí)內(nèi)存占用還小的 GC。
因此,.NET 全新的 GC —— 在 .NET Runtime 核心成員幾年的努力下誕生了!這就是接下來我要講的 Satori GC。
Satori GC
為了讓 GC 能夠正確追蹤對(duì)象,在不少語言中,編譯器會(huì)給存儲(chǔ)操作插入一個(gè)寫屏障。在寫屏障中 GC 會(huì)更新對(duì)象的引用從而確保每一個(gè)對(duì)象都能夠正確被追蹤。這么做的好處很明顯,相比讀操作而言,寫操作更少,將屏障分擔(dān)到每次的寫操作里顯然是一個(gè)更有效率的方法。然而這么做的壞處也很明顯:當(dāng) GC 需要執(zhí)行壓縮操作時(shí)不得不暫停整個(gè)程序,避免代碼訪問到無效的內(nèi)存地址。
而 JVM 上的一些低延時(shí) GC 則放棄了寫屏障,轉(zhuǎn)而使用讀屏障,在每次讀取內(nèi)存地址的時(shí)候通過插入屏障來確保始終拿到的是最新的內(nèi)存地址,來避免無效地址訪問。然而讀操作在應(yīng)用中非常頻繁,這么做雖然能夠使得 GC 執(zhí)行壓縮操作時(shí)不再需要暫停整個(gè)程序,卻會(huì)不可避免地帶來性能的損失。
GC 執(zhí)行壓縮操作雖然開銷很大,但相對(duì)于釋放操作而言只是少數(shù),為了少數(shù)的操作能夠并發(fā)執(zhí)行拖慢所有的讀操作顯得有些得不償失。另外,在 .NET 上,由于 .NET 支持內(nèi)部指針和固定對(duì)象的內(nèi)存地址,因此讀屏障在 .NET 上實(shí)現(xiàn)較為困難,并且會(huì)帶來吞吐量的嚴(yán)重下降,在許多場(chǎng)景下難以接受。
.NET 的新低延時(shí)高吞吐自適應(yīng) GC —— Satori GC 仍然采用 Dijkstra 風(fēng)格的寫屏障設(shè)計(jì),因此吞吐量性能仍然能夠匹敵已有的 Server GC。
另外,Satori GC 采用了分代、增量并發(fā)回收設(shè)計(jì),所有與堆大小成比例的主要 GC 階段都會(huì)與應(yīng)用程序線程并發(fā)執(zhí)行,完全不需要暫停應(yīng)用程序,除了壓縮過程之外。不過,壓縮僅僅是 GC 可以執(zhí)行但不是必須執(zhí)行的一個(gè)可選項(xiàng)。例如 C++/Rust 的內(nèi)存分配器也不會(huì)進(jìn)行壓縮,但仍能正常運(yùn)行;Go 的 GC 也不會(huì)進(jìn)行壓縮。
除了標(biāo)準(zhǔn)模式之外,Satori GC 還提供了低延時(shí)模式。在這個(gè)模式下 Satori GC 直接關(guān)閉了壓縮功能,通過犧牲少量的內(nèi)存占用來獲取更低的延時(shí)。在某些情況下,因?yàn)槔厥瞻l(fā)生得更快,或者壓縮本身并沒有帶來太多實(shí)際收益,內(nèi)存占用反而會(huì)變得更小。例如在一些 Web 場(chǎng)景,大量對(duì)象只存活在單次請(qǐng)求期間,然后很快就會(huì)被清除。既然這些對(duì)象很快都會(huì)變成垃圾,那為什么要進(jìn)行壓縮呢?
與 Go 的徹底不進(jìn)行壓縮的 GC 不同,Satori GC 可以在運(yùn)行時(shí)動(dòng)態(tài)切換壓縮的開關(guān)狀態(tài),以適應(yīng)不同的應(yīng)用場(chǎng)景。想要開啟低延時(shí)模式,只需要執(zhí)行 GCSettings.LatencyMode = GCLatencyMode.LowLatency
即可。在需要極低延時(shí)的場(chǎng)景(例如高幀率游戲或金融實(shí)時(shí)交易系統(tǒng))中,這一設(shè)置可以有效減少 GC 暫停時(shí)間。
Satori GC 還允許開發(fā)者根據(jù)需要關(guān)閉 Gen 0:畢竟不是所有的場(chǎng)景/應(yīng)用都能從 Gen 0 中獲益。當(dāng)應(yīng)用程序并不怎么用到 Gen 0 時(shí),為了支持 Gen 0 在寫屏障中做的額外操作反而會(huì)拖慢性能。目前可以通過設(shè)置環(huán)境變量 DOTNET_gcGen0=0
來關(guān)閉 Gen 0,不過在 Satori GC 計(jì)劃中,將會(huì)實(shí)現(xiàn)根據(jù)實(shí)際應(yīng)用場(chǎng)景自動(dòng)決策 Gen 0 的開啟與關(guān)閉。
性能測(cè)試
說了這么多,新的 Satori GC 到底療效如何呢?讓我們擺出來性能測(cè)試來看看。
首先要說的是,測(cè)試前需要設(shè)置 <TieredCompilation>false</TieredCompilation>
關(guān)閉分層編譯,因?yàn)?tier-0 的未優(yōu)化代碼會(huì)影響對(duì)象的生命周期,從而影響 GC 行為。
測(cè)試場(chǎng)景 1
Unity 有一個(gè) GC 壓力測(cè)試,游戲在每次更新都需要渲染出一幀畫面,而這個(gè)測(cè)試則模擬了游戲在每幀中分配大量的數(shù)據(jù),但是卻不渲染任何的內(nèi)容,從而通過單幀時(shí)間來反映 GC 的實(shí)際暫停。
代碼如下:
Copy
class Program
{
const int kLinkedListSize = 1000;
const int kNumLinkedLists = 10000;
const int kNumLinkedListsToChangeEachFrame = 10;
private const int kNumFrames = 100000;
private static Random r = new Random();
class ReferenceContainer
{
public ReferenceContainer rf;
}
static ReferenceContainer MakeLinkedList()
{
ReferenceContainer rf = null;
for (int i = 0; i < kLinkedListSize; i++)
{
ReferenceContainer link = new ReferenceContainer();
link.rf = rf;
rf = link;
}
return rf;
}
static ReferenceContainer[] refs = new ReferenceContainer[kNumLinkedLists];
static void UpdateLinkedLists(int numUpdated)
{
for (int i = 0; i < numUpdated; i++)
{
refs[r.Next(kNumLinkedLists)] = MakeLinkedList();
}
}
static void Main(string[] args)
{
GCSettings.LatencyMode = GCLatencyMode.LowLatency;
float maxMs = 0;
UpdateLinkedLists(kNumLinkedLists);
Stopwatch totalStopWatch = new Stopwatch();
Stopwatch frameStopWatch = new Stopwatch();
totalStopWatch.Start();
for (int i = 0; i < kNumFrames; i++)
{
frameStopWatch.Start();
UpdateLinkedLists(kNumLinkedListsToChangeEachFrame);
frameStopWatch.Stop();
if (frameStopWatch.ElapsedMilliseconds > maxMs)
maxMs = frameStopWatch.ElapsedMilliseconds;
frameStopWatch.Reset();
}
totalStopWatch.Stop();
Console.WriteLine($"Max Frame: {maxMs}, Avg Frame: {(float)totalStopWatch.ElapsedMilliseconds/kNumFrames}");
}
}
測(cè)試結(jié)果如下:
GC | 最大幀時(shí)間 | 平均幀時(shí)間 | 峰值內(nèi)存占用 |
---|
Server GC | 323 ms | 0.049ms | 5071.906 MB |
DATAS GC | 139 ms | 0.146ms | 1959.301 MB |
Workstation GC | 23 ms | 0.563 ms | 563.363 MB |
Satori GC | 26 ms | 0.061 ms | 1449.582 MB |
Satori GC (低延時(shí)) | 8 ms | 0.050 ms | 1540.891 MB |
Satori GC (低延時(shí),關(guān) Gen 0) | 3 ms | 0.042 ms | 1566.848 MB |
可以看到 Satori GC 在擁有 Server GC 的吞吐量性能同時(shí)(平均幀時(shí)間),還擁有著優(yōu)秀的最大暫停時(shí)間(最大幀時(shí)間)。
測(cè)試場(chǎng)景 2
在這個(gè)測(cè)試中,代碼中將產(chǎn)生大量的 Gen 2 -> Gen 0 的反向引用讓 GC 變得非常繁忙,然后通過大量分配生命周期很短的臨時(shí)對(duì)象觸發(fā)大量的 Gen 0 GC。
Copy
using System.Diagnostics;
using System.Runtime;
using System.Runtime.CompilerServices;
object[] a = new object[100_000_000];
var sw = Stopwatch.StartNew();
var sw2 = Stopwatch.StartNew();
var count = GC.CollectionCount(0) + GC.CollectionCount(1) + GC.CollectionCount(2);
for (var iter = 0; ; iter++)
{
object o = new object();
for (int i = 0; i < a.Length; i++)
{
a[i] = o;
}
sw.Restart();
Use(a, o);
for (int i = 0; i < 1000; i++)
{
GC.KeepAlive(new string('a', 10000));
}
var newCount = GC.CollectionCount(0) + GC.CollectionCount(1) + GC.CollectionCount(2);
if (newCount != count)
{
Console.WriteLine($"Gen0: {GC.CollectionCount(0)}, Gen1: {GC.CollectionCount(1)}, Gen2: {GC.CollectionCount(2)}, Pause on Gen0: {sw.ElapsedMilliseconds}ms, Throughput: {(iter + 1) / sw2.Elapsed.TotalSeconds} iters/sec, Max Working Set: {Process.GetCurrentProcess().PeakWorkingSet64 / 1048576.0} MB");
count = newCount;
iter = -1;
sw2.Restart();
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
static void Use(object[] arr, object obj) { }
由于這個(gè)測(cè)試主要就是測(cè)試 Gen 0 的回收性能,因此測(cè)試結(jié)果中將不包含關(guān)閉 Gen 0 的情況。
GC | 單次暫停 | 吞吐量 | 峰值內(nèi)存占用 |
---|
Server GC | 59 ms | 7.485 iter/s | 1286.898 MB |
DATAS GC | 60 ms | 6.362 iter/s | 859.722 MB |
Workstation GC | 1081 ms | 0.804 iter/s | 805.453 MB |
Satori GC | 0 ms | 4.448 iter/s | 801.441 MB |
Satori GC (低延時(shí)) | 0 ms | 4.480 iter/s | 804.761 MB |
這個(gè)測(cè)試中 Satori GC 表現(xiàn)得非常亮眼:擁有不錯(cuò)的吞吐量性能的同時(shí),做到了亞毫秒級(jí)別的暫停時(shí)間:可以說在這個(gè)測(cè)試中 Satori GC 壓根就沒有暫停過我們的應(yīng)用程序!
測(cè)試場(chǎng)景 3
這次我們使用 BinaryTree Benchmark 進(jìn)行測(cè)試,這個(gè)測(cè)試由于會(huì)短時(shí)間大量分配對(duì)象,因此對(duì)于 GC 而言是一項(xiàng)壓力很大的測(cè)試。
Copy
using System.Diagnostics;
using System.Diagnostics.Tracing;
using System.Runtime;
using System.Runtime.CompilerServices;
using Microsoft.Diagnostics.NETCore.Client;
using Microsoft.Diagnostics.Tracing;
using Microsoft.Diagnostics.Tracing.Analysis;
using Microsoft.Diagnostics.Tracing.Parsers;
class Program
{
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
static void Main()
{
var pauses = new List<double>();
var client = new DiagnosticsClient(Environment.ProcessId);
EventPipeSession eventPipeSession = client.StartEventPipeSession([new("Microsoft-Windows-DotNETRuntime",
EventLevel.Informational, (long)ClrTraceEventParser.Keywords.GC)], false);
var source = new EventPipeEventSource(eventPipeSession.EventStream);
source.NeedLoadedDotNetRuntimes();
source.AddCallbackOnProcessStart(proc =>
{
proc.AddCallbackOnDotNetRuntimeLoad(runtime =>
{
runtime.GCEnd += (p, gc) =>
{
if (p.ProcessID == Environment.ProcessId)
{
pauses.Add(gc.PauseDurationMSec);
}
};
});
});
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true, true);
GC.WaitForPendingFinalizers();
GC.WaitForFullGCComplete();
Thread.Sleep(5000);
new Thread(() => source.Process()).Start();
pauses.Clear();
Test(22);
source.StopProcessing();
Console.WriteLine($"Max GC Pause: {pauses.Max()}ms");
Console.WriteLine($"Average GC Pause: {pauses.Average()}ms");
pauses.Sort();
Console.WriteLine($"P99.9 GC Pause: {pauses.Take((int)(pauses.Count * 0.999)).Max()}ms");
Console.WriteLine($"P99 GC Pause: {pauses.Take((int)(pauses.Count * 0.99)).Max()}ms");
Console.WriteLine($"P95 GC Pause: {pauses.Take((int)(pauses.Count * 0.95)).Max()}ms");
Console.WriteLine($"P90 GC Pause: {pauses.Take((int)(pauses.Count * 0.9)).Max()}ms");
Console.WriteLine($"P80 GC Pause: {pauses.Take((int)(pauses.Count * 0.8)).Max()}ms");
using (var process = Process.GetCurrentProcess())
{
Console.WriteLine($"Peak WorkingSet: {process.PeakWorkingSet64} bytes");
}
}
static void Test(int size)
{
var bt = new BinaryTrees.Benchmarks();
var sw = Stopwatch.StartNew();
bt.ClassBinaryTree(size);
Console.WriteLine($"Elapsed: {sw.Elapsed.TotalMilliseconds}ms");
}
}
public class BinaryTrees
{
class ClassTreeNode
{
class Next { public required ClassTreeNode left, right; }
readonly Next? next;
ClassTreeNode(ClassTreeNode left, ClassTreeNode right) =>
next = new Next { left = left, right = right };
public ClassTreeNode() { }
internal static ClassTreeNode Create(int d)
{
return d == 1 ? new ClassTreeNode(new ClassTreeNode(), new ClassTreeNode())
: new ClassTreeNode(Create(d - 1), Create(d - 1));
}
internal int Check()
{
int c = 1;
var current = next;
while (current != null)
{
c += current.right.Check() + 1;
current = current.left.next;
}
return c;
}
}
public class Benchmarks
{
const int MinDepth = 4;
public int ClassBinaryTree(int maxDepth)
{
var longLivedTree = ClassTreeNode.Create(maxDepth);
var nResults = (maxDepth - MinDepth) / 2 + 1;
for (int i = 0; i < nResults; i++)
{
var depth = i * 2 + MinDepth;
var n = 1 << maxDepth - depth + MinDepth;
var check = 0;
for (int j = 0; j < n; j++)
{
check += ClassTreeNode.Create(depth).Check();
}
}
return longLivedTree.Check();
}
}
}
這一次我們使用 Microsoft.Diagnostics.NETCore.Client
來精準(zhǔn)的跟蹤每一次 GC 的暫停時(shí)間。
測(cè)試結(jié)果如下:
性能指標(biāo) | Workstation GC | Server GC | DATAS GC | Satori GC | Satori GC (低延時(shí)) | Satori GC (關(guān) Gen 0) |
---|
執(zhí)行所要時(shí)間 (ms) | 63,611.3954 | 22,645.3525 | 24,881.6114 | 41,515.6333 | 40,642.3008 | 13528.3383 |
峰值內(nèi)存占用 (bytes) | 1,442,217,984 | 4,314,828,800 | 2,076,291,072 | 1,734,955,008 | 1,537,855,488 | 1,541,136,384 |
最大暫停時(shí)間 (ms) | 48.9107 | 259.9675 | 197.7212 | 6.5239 | 4.0979 | 1.2347 |
平均暫停時(shí)間 (ms) | 6.117282383 | 12.00785067 | 3.304014164 | 0.673435691 | 0.437758553 | 0.1391 |
P99.9 暫停時(shí)間 (ms) | 46.8537 | 243.2844 | 172.3259 | 5.8535 | 3.6835 | 0.9887 |
P99 暫停時(shí)間 (ms) | 44.0532 | 207.3627 | 57.4681 | 5.2661 | 3.2012 | 0.5814 |
P95 暫停時(shí)間 (ms) | 39.4903 | 48.7269 | 8.92 | 3.0054 | 1.3854 | 0.3536 |
P90 暫停時(shí)間 (ms) | 23.1327 | 21.4588 | 2.8013 | 1.7859 | 0.9204 | 0.2681 |
P80 暫停時(shí)間 (ms) | 8.3317 | 4.7577 | 1.7581 | 0.8009 | 0.6006 | 0.1942 |
這一次 Satori GC 的標(biāo)準(zhǔn)模式和低延時(shí)模式都做到了非常低的延時(shí),而關(guān)閉 Gen 0 后 Satori GC 更是直接衛(wèi)冕 GC 之王,不僅執(zhí)行性能上跑過了 Server GC,同時(shí)還做到了接近 Workstation GC 級(jí)別的內(nèi)存占用,并且還做到了亞毫秒級(jí)別的最大 STW 時(shí)間!
測(cè)試場(chǎng)景 4
這個(gè)場(chǎng)景來自社區(qū)貢獻(xiàn)的 GC 測(cè)試:https://github.com/alexyakunin/GCBurn
這個(gè)測(cè)試包含三個(gè)不同的重分配的測(cè)試項(xiàng)目,模擬三種場(chǎng)景:
- Cache Server:使用一半的內(nèi)存(大約 16G),分配大約 1.86 億個(gè)對(duì)象
- Stateless Server:一個(gè)無狀態(tài) Web Server
- Worker Server:一個(gè)有狀態(tài)的 Worker 始終占據(jù) 20% 內(nèi)存(大約 6G),分配大概 7400 萬個(gè)對(duì)象
測(cè)試結(jié)果由其他社區(qū)成員提供。
首先看分配速率:

Server GC 是針對(duì)吞吐量進(jìn)行大量?jī)?yōu)化的,因此做到最高的吞吐量性能并不意外。Satori GC 在 Cache Server 場(chǎng)景有所落后,但是現(xiàn)實(shí)中并不會(huì)有在一秒內(nèi)分配超過 2 千萬個(gè)對(duì)象的場(chǎng)景,因此這個(gè)性能水平并不會(huì)造成實(shí)際的性能瓶頸。
然后看暫停時(shí)間:

注意時(shí)間的單位是微秒(0.001ms),并且縱坐標(biāo)進(jìn)行了對(duì)數(shù)縮放。Satori GC 成功地做到了亞毫秒(小于 1000 微秒)級(jí)別的暫停。
最后看峰值內(nèi)存占用:

可以看到 Satori GC 相比其他 GC 而言有著出色的內(nèi)存占用,在所有測(cè)試結(jié)果中幾乎都是那個(gè)內(nèi)存占用最低的。
綜合以上三點(diǎn),我們可以看到 Satori GC 在犧牲少量的吞吐量性能的同時(shí),做到了亞毫秒級(jí)別的延時(shí)和低內(nèi)存占用。只能說:干得漂亮!
大量分配速率測(cè)試
這同樣是來自其他社區(qū)成員貢獻(xiàn)的測(cè)試結(jié)果。在這個(gè)測(cè)試中,代碼使用一個(gè)循環(huán)在所有的線程上大量分配對(duì)象并立馬釋放。
測(cè)試結(jié)果如下:

可以看到 Satori GC 的默認(rèn)模式在這個(gè)測(cè)試中做到了最好的分配吞吐量性能,成功做到每秒分配 20 億個(gè)對(duì)象。
總結(jié)
Satori GC 的目標(biāo)是為 .NET 帶來了一種全新的低延時(shí)高吞吐自適應(yīng) GC,不僅有著優(yōu)秀的分配速率,同時(shí)還能做到亞毫秒級(jí)別的暫停時(shí)間和低內(nèi)存占用,與此同時(shí)做到 0 配置開箱即用。
目前 Satori GC 仍然處于實(shí)驗(yàn)性階段,還有不少的課題需要解決,例如運(yùn)行時(shí)自動(dòng)決策 Gen 0 的開關(guān)、更好的策略以均衡吞吐量性能和內(nèi)存占用以及適配更新版本的 .NET 等等,但是已經(jīng)可以用于真實(shí)世界應(yīng)用了,想要試用 Satori GC 的話可以參考下面的方法在自己的應(yīng)用中啟用。
osu! 作為一款從引擎到游戲客戶端都是純 C# 開發(fā)的游戲,已經(jīng)開始提供使用 Satori GC 的選項(xiàng)。在選歌列表的滾動(dòng)測(cè)試中,Satori GC 成功將幀數(shù)翻了一倍,從現(xiàn)在的 120 fps 左右提升到接近 300 fps。
相信等 Satori GC 成熟正式作為 .NET 默認(rèn) GC 啟用后,將會(huì)為 .NET 帶來大量的性能提升,并擴(kuò)展到更多的應(yīng)用場(chǎng)景。
啟用方法
截至目前(2025/5/22),Satori GC 僅支持在 .NET 8 應(yīng)用中啟用??紤]到這是目前最新的 LTS 穩(wěn)定版本,因此目前來看也足夠了,另外 Satori GC 的開發(fā)人員已經(jīng)在著手適配 .NET 9。
osu! 團(tuán)隊(duì)配置了 CI 自動(dòng)構(gòu)建最新的 Satori GC,因此我們不需要手動(dòng)構(gòu)建 .NET Runtime 源碼得到 GC,直接下載對(duì)應(yīng)的二進(jìn)制即可使用。
對(duì)于 .NET 8 應(yīng)用:
- 使用
dotnet publish -c Release -r <rid> --self-contained
發(fā)布一個(gè)自包含應(yīng)用,例如 dotnet publish -c Release -r win-x64 --self-contained
- 從 https://github.com/ppy/Satori/releases 下載對(duì)應(yīng)平臺(tái)的最新 Satori GC 構(gòu)建,例如
win-x64.zip
- 解壓得到三個(gè)文件:如果是 Windows 平臺(tái)就是
coreclr.dll
、clrjit.dll
和 System.Private.CoreLib.dll
;而如果是 Linux 則是 libcoreclr.so
、libclrjit.so
和 System.Private.CoreLib.dll
- 找到第一步發(fā)布出來的應(yīng)用(一般在
bin/Release/net8.0/<rid>/publish
文件夾里,例如 bin/Release/net8.0/win-x64/publish
),用第三步得到的三個(gè)文件替換掉發(fā)布目錄里面的同名文件
然后即可享受 Satori GC 帶來的低延時(shí)體驗(yàn)。
反饋渠道
如果在使用過程中遇到了任何問題,可以參考:
轉(zhuǎn)自https://www.cnblogs.com/hez2010/p/18889954/
該文章在 2025/6/3 10:41:46 編輯過