Hello There, I'm the owner of this repo.
So nice that has ended here somehow.
Gonna answer the upper quests...
- Do you ever find that the Garbage Collector causes noticeable pausing in the emulator?
The GC dosn't cause noticeable pausing on my setup. Even tho it could allocate less.
- Did you do this with 100% managed memory, or do you need to use unsafe code / real pointers?
In fact it was done without unsafe code or pointers till some of the latest commits. Where i switched to unsafe because performance reasons. On my setup that was arround 10 fps more.
Search the repo for "new" and you'll see that everything is allocated at construction time. Everything hangs off the window class so essentially all memory is allocated as the project starts up. You'd probably want to pre-allocate in any language because but this also relieves GC pressure.
Another thing you can do in C# is leverage stack allocation using structs.
Relatedly, the new .NET Span<T> APIs open up a lot more opportunities for stack allocation and "pointer arithmetic-style" work in "safe" code. Span<T> is really fascinating at the performance opportunities it opens in .NET (and real performance gains already achieved in, for instance, ASP.NET Core).
I skimmed the code a little and did a few Github searches on it and it looks like the only unsafe blocks of code are in the core data bus. It's not entirely clear why it was done that way, as the CPU itself is using managed arrays to represent memory. Perhaps it was necessary for speed, as it seems primarily concerned with loading the executables that are to be emulated.
Otherwise, everything looks statically allocated. So there should be no GC collections at all.
I use it with DeviceIOControl for interoperating with a driver.
I didn't learn by following someone's example, in my case, I have to work with so many different structs in a high performance use case that I took the time to re-learn pointers. (Haven't used them in years.)
1: Memory is allocated using new byte[bufferSize]
2: That byte[] is pinned to a byte* or void* via fixed
3: The pointer is passed to DeviceIOControl
4: Traditional c-style typecasting inside of fixed
5: Traditional GC cleans up your byte[]. This avoids problems with managed programs that allocate lots of native memory.
I don't know how to use pointers with Span<T>...
BUT: I do similar things with ArraySegment<byte[]>. All that's needed is pointer arithmetic:
ArraySegment<byte> arraySegment = ...
fixed (byte* dataArrayPtr = arraySegment.Array)
{
var dataPtr = dataArrayPtr + arraySegment.Offset;
var ptr = (SomeStruct*)dataPtr;
}
I only mention it because I'm not entirely clear on what is going on or how it works (largely because I tend to only be able to learn things by doing them, which is my own issue, not your failure to describe it), but after reading the docs on MemoryMappedFiles, I wonder if it's something that could be useful to you.
In fact, the techniques for C# here would be pretty much the same as for C++ or C: Don't do dynamic allocation in the hot path. And if you must, keep the lifetime short so it gets cleaned up predictably (usually in gen0).
However, for an ancient platform on modern hardware I guess the per-frame budget is plentiful even for an emulator.
In what case isn't it obvious? C# makes it quite clear, you allocate to memory on the heap whenever you see the "new" keyword or whenever you see a new closure (because closures are of course just a poor man's objects).
Not necessarily just those things. A foreach loop might have a heap allocation, async/await almost certainly, calling a method with a params[] parameter as well. Everything you call from the BCL might allocate, too and it's not always visible or obvious.
That being said, it is still doable to avoid allocations and the standard library has become much better with not allocating unless really necessary. A lot of the newer things with Span<T> enable zero-allocation usage of certain APIs that before would have allocated.
And even if you use structs with the goal of avoiding allocations, the language doesn't prevent you from accidentally writing code that upcasts them to `object` (eg by calling anything that the struct inherits from `object` and isn't overridden by the struct itself.) This transparently copies the struct to a heap allocation and negates your intent.
This is not entirely true as of C# 7.2. That version of the language adds "ref structs" - which can only be allocated on the stack and cannot be boxed [1].
This requires some care though, as the struct may still be copied needlessly if it is not also marked as `readonly` [2].
Typically, you choose where to optimize after collecting metrics.
The .Net garbage collector is a highly optimized generational garbage collector. Practices like pre-allocating and pooling are discouraged, because most of the time they are premature optimization.
In general, the best way to think about it is to pretend that the garbage collector is a built-in library for pooling and reusing objects.
If, early in your project's lifecycle, you suspect that you'll need to implement your own object pool, just start with stub "Get/Recycle" methods that call new / no-op. You can always swap in a pool later.
- Do you ever find that the Garbage Collector causes noticeable pausing in the emulator?
- Did you do this with 100% managed memory, or do you need to use unsafe code / real pointers?
(Apologies that I didn't read the source code.)