Skip to content

Instantly share code, notes, and snippets.

@jechter
Created May 12, 2020 09:48
Show Gist options
  • Save jechter/2730225240163a806fcc15c44c5ac2d6 to your computer and use it in GitHub Desktop.
Save jechter/2730225240163a806fcc15c44c5ac2d6 to your computer and use it in GitHub Desktop.
Standalone C# GC frame time stress test
using System;
using System.Diagnostics;
using System.Runtime;
/*
This little program is a stress test for GC performance we care about in Unity.
It will basically create a heap of managed memory with references between allocations
(just some linked list), and then randomly replace some memory every loop iteration - to
simulate a game which dirties some memory every frame. This allocation pattern may not be
representative, most games are much better in not generating this much garbage every frame,
but that does not matter much, since as long as a game is not free of creating some garbage
in non-short lived objects over time, eventually a full collection will occur, causing a GC
spike.
This test case prints two numbers upon running:
1. The longest time in milliseconds taken by any frame (spikes)
2. The average time in milliseconds taken per frame (throughput)
In Unity, we care more about 1. than 2.. A typical game has a frame rate target of 60 fps
(higher for VR), which means a frame time budget of 16ms. Given that the game also needs to
do all it's rendering, physics, animation, etc within those 16ms, that leaves a time budget
of maybe 3ms for GC in order not to miss the frame. Missing a frame (or multiple frames if we
hit a bigger spike) will result in a visible jerk in the smoothness of a game's animation.
We ran this program in:
1. Mono with Sgen GC
2. Mono with Boehm GC
3. Mono with Unity's custom Boehm GC modified for incremental collections (no binary distribution
available as we only distribute this as an embeddable dylib version for Unity, but can be built
from source from https://github.com/Unity-Technologies/mono).
4. CoreCLR using different settings for GCSettings.LatencyMode
It appears that only Mono-Boehm modified for incremental GC can guarantee a consistently low GC
delay in this sample. CoreCLR with GCLatencyMode.LowLatency looks promising at first, but when increasing
how long the simulation runs (kNumFrames), it will eventually also run into a situation where the
GC generates a spike. Fewer spikes are clearly better than frequent spikes - but this does not compete well
with our modified Boehm's promise of no spikes, ever.
Results:
Mono:
~/u/G/C/ConsoleApp2⚡️ ~/unity-hg/mono2/mono/mini/mono-sgen Program.exe
Max Frame: 44, Avg Frame: 0.717
~/u/G/C/ConsoleApp2⚡️ ~/unity-hg/mono2/mono/mini/mono-bdwgc Program.exe
Max Frame: 144, Avg Frame: 0.914
~/u/G/C/ConsoleApp2⚡️ ~/unity-hg/mono2/mono/mini/mono-bdwgc --gc-params=incremental=1 Program.exe
Max Frame: 2, Avg Frame: 0.831
CoreCLR, GCLatencyMode.Batch:
Max Frame: 323, Avg Frame: 1.308
CoreCLR, GCLatencyMode.Interactive:
Max Frame: 45, Avg Frame: 0.892
CoreCLR, GCLatencyMode.LowLatency:
Max Frame: 2, Avg Frame: 1.03
With kNumFrames = 100000:
Max Frame: 42, Avg Frame: 1.85963
CoreCLR, GCLatencyMode.SustainedLowLatency:
Max Frame: 61, Avg Frame: 0.943
*/
namespace ConsoleApp2
{
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}");
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment