Memory management in MonoGame

Hi Guys,

I have started porting my terrain system to MonoGame and have come across an interesting issue.

To prevent garbage collections, I have written a memory manager for my renderer.

At boot up it allocates some arrays and uses a grow only allocator

The problem is that C# is manically type safe.

So I cannot read an array of shorts into a buffer in one hit.

As far as I can see the only way to read the data into an array is to loop over each value in the array reading a single short at a time. MUCH , MUCH , too slow.

I have thought about using a binaryformatter, but that doesn’t use my memory allocator and everything falls apart.

I have thought about having a temporary byte array, but it is entirely possible that I may have several terrain patches streaming in at the same time, so that becomes impracticable.

So I am going to try using a struct instead of an array and using [StructLayout(LayoutKind.Explicit)] to effectively create a union of two arrays (byte and short). So the code becomes…

ShortHeights.Shorts = RenderMemoryManager.GetShortMemory(RenderMemoryManager.MemoryUsage.HeightMap, 
                                                  AllocationSize, out ShortHandle);
stream.Read(ShortHeights.Bytes, 0, AllocationSize);

Anybody got a better solution

If you’re ok with pointers, you can use unsafe references.

var bytes = ShortHeights.Bytes;
stream.Read(bytes, 0, AllocationSize);
// Get the address of the first byte in the array
fixed (byte* b = &bytes[0])
{
    // Cast the address of the first byte into an address of the first short
    short* s = (short*)b;
    // Now you can read and index s as if it was a short[]
    short first = s[0];
    short fourth = s[3];
}

Of course, you have to mark the method as unsafe because you are now using pointers.

1 Like

Thought about that, but adding unsafe code is going to create problems for me in the long term.

Totally correct method though, but C# thinks pointers are evil. ;>

What problems do you anticipate?

In practice I don’t think it really matters if your assembly contains unsafe code. Or I don’t know where it would matter at least.

Unsafe C# is really nice with pointers. Much more usable than you’d expect and great for high-performance stuff.

Anyway, I’ve used the ‘union’ solution before and it works well too. Just gotta be careful with array length.

I have had problems publishing C# games that use unsafe code.

The approval procedure took months…

Approval for what exactly?

What about Buffer.BlockCopy?

Hi cvnk, yes that would work, but it is less efficient as you have to read in the data to a byte array and then copy it to the destination array.

More memory and more CPU sadly,

Hi Jjagg,

I had problems with Microsoft which led to a three week delay releasing Onslaught on Xbox Live.

I have had problems with other projects as well with various companies who don’t understand “unsafe” does not mean dangerous,

1 Like

You don’t need to use a byte array. Look at the MSDN example. It copies from a short[] to a long[] directly, without a byte[] middleman.

Strange how big are these arrays?

I just use file.ReadAllBytes half the time for regular numeric data types in straight dat files like for shorts id just copy it right in to shorts with bitmath and a while or for loop its really fast.

        private static short GetShortFromByteArray( int index )
        {
            // shift the bytes in
            return (short)( (data[index]) + (data[(index + 1)] << 8) );
        }       

One collect at load shouldn’t matter either and definately not to your entire render loop unless your doing something really weird.

This is for streaming terrain, I really don’t want to have to convert every byte pair into a short one by one

The faster I get the data from disk into memory the better

Again how big are the files your loading in ?

If your reading from disk its physical hardware speed which is going to be slow. 2,000 rpm’s might sound good but then think about the ram’s Mega or Giga hertz speeds. 2k vs millions.
Pointers wont work until the io is completed.

Anyways …
blockbuffer copy and readallbytes, 300mb in a half to a quarter second.

Im guessing the buffer block copy is doing some magic tricks under the hood as well. I should point out file.readallbytes allocates and reads a huge block of memory straight in so big files can be a risk.

Edit: cleaned up the test.

// console test

using System;
using System.IO;
using System.IO.Compression;
using System.IO.MemoryMappedFiles;
using System.Diagnostics;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TestPerformanceTest01
{
    class Program
    {
        public static Stopwatch stopWatch;

        static int len = 150000000;

        static void Main()
        {
            stopWatch = new Stopwatch();

            Console.WriteLine("file  : " + (len / 1000000) + " million short's" + "  " + (len / 1000000 * 2) + " mb's");
            Console.WriteLine(" ");

            //W();

            R0();
            
            R1();
            
            R2();
            
            R3();
            
            Console.WriteLine("done ");
            Console.ReadKey();
        }

        static void W()
        {
            stopWatch.Start();
            Random rnd = new Random();
            short[] a = new short[len];
            for (int i = 0; i < len; i++)
            {
                short s = (short)(rnd.Next(0, 1232));
                a[i] = s;
             }
            using (BinaryWriter b = new BinaryWriter(File.Open("mytestfile.bin", FileMode.Create)))
            {
                foreach (short s in a)
                {
                    b.Write(s);
                }
            }
            stopWatch.Stop();
            PrintOut("Write");
        }

        static void R3()
        {
            stopWatch.Start();
            using (FileStream fs = new FileStream("mytestfile.bin", FileMode.Open))
            {
                using (BinaryReader b = new BinaryReader(fs))
                {
                    int pos = 0;
                    int length = (int)b.BaseStream.Length;
                    while (pos < length)
                    {
                        short s = b.ReadInt16();
                        pos += 2;
                    }
                }
            }
            stopWatch.Stop();
            PrintOut("filestream and binary reader");
        }

        static void R2()
        {
            stopWatch.Start();
            using (BinaryReader b = new BinaryReader(File.Open("mytestfile.bin", FileMode.Open)))
            {
                int pos = 0;
                int length = (int)b.BaseStream.Length;
                while (pos < length)
                {
                    short s = b.ReadInt16();
                    pos +=2;
                }
            }
            stopWatch.Stop();
            PrintOut("file.open and binary reader");
        }

        static void R1()
        {
            stopWatch.Start();
            byte[] b = File.ReadAllBytes("mytestfile.bin");
            int len = b.Length / 2;
            short[] s = new short[len];
            Buffer.BlockCopy(b, 0, s, 0, len);
            stopWatch.Stop();
            PrintOut("File readallbytes and buffer.blockcopy");
        }

        private static void R0()
        {
            stopWatch.Start();
            byte[] b = File.ReadAllBytes("mytestfile.bin");
            int len = b.Length / 2;
            short[] s = new short[len];
                for (int i = 0; i < len; i += 2)
                    s[i] = (short)(b[i] + b[(i + 1)] << 8);
            //s[i] = (short)((b[i]) + (b[(i + 1)] << 8));
            stopWatch.Stop();
            PrintOut("File readallbytes and bit shift");
        }

        public static void PrintOut(string msg)
        {
            TimeSpan ts = stopWatch.Elapsed;
            Console.WriteLine(msg);
            string elapsedTime = String.Format("{0:00}:{1:00}:{2:00}.{3:00}",ts.Hours, ts.Minutes, ts.Seconds, ts.Milliseconds / 10);
            Console.WriteLine("RunTime " + elapsedTime);
            Console.WriteLine("");
            stopWatch.Reset();
        }

    }
}

output…

A small file.

file : 10 million short’s 20 mb’s

Write
RunTime 00:00:00.73

File readallbytes and bit shift
RunTime 00:00:00.04

File readallbytes and buffer.blockcopy
RunTime 00:00:00.02

file.open and binary reader
RunTime 00:00:00.36

filestream and binary reader
RunTime 00:00:00.37

done

small movie sized file

file : 150 million short’s 300 mb’s

File readallbytes and bit shift
RunTime 00:00:00.67

File readallbytes and buffer.blockcopy
RunTime 00:00:00.26

file.open and binary reader
RunTime 00:00:04.75

filestream and binary reader
RunTime 00:00:05.01

done

a gigabyte file

file : 500 million short’s 1000 mb’s

Write
RunTime 00:00:39.02

File readallbytes and bit shift
RunTime 00:00:02.71

File readallbytes and buffer.blockcopy
RunTime 00:00:01.41

file.open and binary reader
RunTime 00:00:17.08

filestream and binary reader
RunTime 00:00:15.08

done

That’s really interesting data.

Shows why I did not want to loop over all the shorts.

The way I see the code working is the texture assets will be placed in a list to be processed, the height data I want to be read instantly. Which is why I want it as fast as possible.

I haven’t got a figure for the size of the height data yet, it’s work in progress. It may be I end up using the same approach
as texture assets.

When it is added to the list, the memory is allocated and a buffer created for all mipmaps.

Then the lowest res bitmaps (probably 64 by 64 and below) will be instantly loaded and a task kicked off to load the rest.

I expect some of the textures to by 4K 24 bit, so it’s a lot of data to stream in.

Since this is for terrain, you shouldn’t see the lack of mipmaps since the terrain patch will be small when only the low mipmaps are available. By the time you get close enough to see it clearly the other mipmaps will have been loaded.

I will try and find the time to add a test for my approach so we can compare it against the ones you have tested.

Nice data, thank you.

Shows why I did not want to loop over all the shorts.

Wait wat 300mb.
File readallbytes and bit shift RunTime 00:00:00.67
My movie player doesn’t even load that fast, under a second ?.

What about Buffer.BlockCopy5?
you have to read in the data to a byte array and then copy it to the destination array.

I think you have some misapprehension that there can be some physical disk to ram magic. All the wrapped calls like… buffer.blockcopy or any other, Loop and copy under the hood be it pointers or not in some fashion.

What this shows is.

For images even 4k, xnbs, are compact, i doubt you can beat the content pipeline here.

As for arrays of terrain.

That for little files… it doesn’t matter.
.02 or .03 seconds.

That for big files… It doesn’t matter.
You need to compress it chunk it and or buffer it anyways you probably need to throw up a splash screen and or asyncronously load it. Unless you think your app crashing is passable.

If your data is that big ?

THAT’S A LIFETIME!!!

20 - 30 milliseconds. I have 8 milliseconds for an entire frame.

I am budgeting for 4 ms per frame for all streaming, and 4 milliseconds to get the streamed data into render buffers.
That’s taking an entire core out of play just for streaming data.

I am allowing the streaming core to get out of sync with other cores because you cannot even guess what CPU the end user will be running, not to mention what GPU they have, but even so I need to make everything as fast as humanly possible.

I also cannot use any temporary buffers that are not allocated from the stack. When the C# garbage collector kicks in, the world ends for your game for 100’s of milliseconds. ( I have even seen cases where it stalls other apps, the garbage collector is evil).

And I cannot assume that I will be using xnb’s. The game can be modded which means I may be streaming data from bitmaps on disk.

And a splash screen! No. Fly over a hill and the game stops to display a splash screen? No.

Anyway, looks like my solution is the fastest so far. I’ll just leave it that way for now.