30

I've used C# to solve the following requirement.. - create an app the can receive a lot of data fast - you must be able to analyse the received data while more are incoming. - use as little CPU and disk as possible

My idea for an algorithm was..

SIZE = 10MB
Create a mmf with the size of SIZE
On data recived:
  if data can't fit mmf: increase mmf.size by SIZE
  write the data to mmf

-> The size on the disc are increased in chuncks of 10MB when the previous "room/space" are used.

How is the "increase mmf.size by SIZE" done in C#? I have found a lot of simple examples on creating mmfs and views but the only place (link) I have seen code that acutally increases the mmfs area uses code that can't compile. Any help will be greatly appriciated.

EDIT This causes an exception :

private void IncreaseFileSize()
{
    int theNewMax = this.currentMax + INCREMENT_SIZE;
    this.currentMax = theNewMax;

    this.mmf.Dispose();

    this.mmf = MemoryMappedFile.CreateFromFile(this.FileName, FileMode.Create, "MyMMF", theNewMax);
    this.view = mmf.CreateViewAccessor(0, theNewMax);            
}

This exception is thrown : The process cannot access the file 'C:\Users\moberg\Documents\data.bin' because it is being used by another process.

Glenn Slayden
  • 17,543
  • 3
  • 114
  • 108
Moberg
  • 783
  • 1
  • 8
  • 17
  • 1
    Why does the code on that page not compile? It looks valid to me. – Edwin de Koning May 23 '11 at 11:41
  • It uses a non-exsisting overload - "MemoryMappedFile.CreateFromFile(file, null, 1000);" – Moberg May 23 '11 at 11:47
  • The process cannot access the file 'C:\Users\molsgaar\Documents\data.bin' because it is being used by another process. – Moberg May 23 '11 at 14:00
  • Does this _have_ to be done using an MMF? Could you not just regular file access - create or open a file for append and just keep writing the data to the end of the file (which will then grow automatically). Could you perhaps give more context on how the data is to be analysed, or what will be analysing it? – Allison Lock May 23 '11 at 15:11

5 Answers5

28

Once you map a file in memory, you cannot increase its size. This is a known limitation of memory mapped files.

...you must calculate or estimate the size of the finished file because file mapping objects are static in size; once created, their size cannot be increased or decreased.

One strategy would be to use chunks stored in non-persisted memory mapped files of a given size, say 1GB or 2GB. You would manage these through a top level ViewAccessor of your own design (probably doing basic passthru of the methods you need from the MemoryMappedViewAccessor).

Edit: or you could just create a non-persisted memory mapped file of a maximal size you expect to use (say 8GB to start, with a parameter to tune it on start-up of your application) and retrieve MemoryMappedViewAccessor's per logical chunk. The non-persisted file will not use physical resources until each view is requested.

user7116
  • 63,008
  • 17
  • 141
  • 172
  • Thanks. I had the feeling I was messing in an area a bit to unknown to me. – Moberg May 23 '11 at 19:13
  • You can increase their size with `NtExtendSection` https://undocumented.ntinternals.net/UserMode/Undocumented%20Functions/NT%20Objects/Section/NtExtendSection.html – Matt Jul 18 '18 at 07:42
  • @Matt It seems like your link specifically mentions that the 'shared (memory) section' API in `ntdll.dll` doesn't apply to file-system backed memory-mapped files... "If section it's [sic.] a mapped file, function fails." More information at [this (dubious) link](https://www.blackhat.com/presentations/bh-europe-05/BH_EU_05-Cerrudo/BH_EU_05_Cerrudo.pdf). – Glenn Slayden Jan 02 '19 at 22:57
7

Well, you can!!.

Here is my implementation of a growable memory mapped file:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.IO.MemoryMappedFiles;

namespace MmbpTree
{
    public unsafe sealed class GrowableMemoryMappedFile : IDisposable
    {

        private const int AllocationGranularity = 64 * 1024;

        private class MemoryMappedArea
        {
            public MemoryMappedFile Mmf;
            public byte* Address;
            public long Size;
        }


        private FileStream fs;

        private List<MemoryMappedArea> areas = new List<MemoryMappedArea>();
        private long[] offsets;
        private byte*[] addresses;

        public long Length
        {
            get {
                CheckDisposed();
                return fs.Length;
            }
        }

        public GrowableMemoryMappedFile(string filePath, long initialFileSize)
        {
            if (initialFileSize <= 0 || initialFileSize % AllocationGranularity != 0)
            {
                throw new ArgumentException("The initial file size must be a multiple of 64Kb and grater than zero");
            }
            bool existingFile = File.Exists(filePath);
            fs = new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
            if (existingFile)
            {
                if (fs.Length <=  0 || fs.Length % AllocationGranularity != 0)
                {
                    throw new ArgumentException("Invalid file. Its lenght must be a multiple of 64Kb and greater than zero");
                }
            }
            else
            { 
                fs.SetLength(initialFileSize);
            }
            CreateFirstArea();
        }

        private void CreateFirstArea()
        {
            var mmf = MemoryMappedFile.CreateFromFile(fs, null, fs.Length, MemoryMappedFileAccess.ReadWrite,  null, HandleInheritability.None, true);
            var address = Win32FileMapping.MapViewOfFileEx(mmf.SafeMemoryMappedFileHandle.DangerousGetHandle(), 
                Win32FileMapping.FileMapAccess.Read | Win32FileMapping.FileMapAccess.Write,
                0, 0, new UIntPtr((ulong) fs.Length), null);
            if (address == null) throw new Win32Exception();

            var area = new MemoryMappedArea
            {
                Address = address,
                Mmf = mmf,
                Size = fs.Length
            };
            areas.Add(area);

            addresses = new byte*[] { address };
            offsets = new long[] { 0 };

        }


        public void Grow(long bytesToGrow)
        {
            CheckDisposed();
            if (bytesToGrow <= 0 || bytesToGrow % AllocationGranularity != 0)  {
                throw new ArgumentException("The growth must be a multiple of 64Kb and greater than zero");
            }
            long offset = fs.Length;
            fs.SetLength(fs.Length + bytesToGrow);
            var mmf = MemoryMappedFile.CreateFromFile(fs, null, fs.Length, MemoryMappedFileAccess.ReadWrite, null, HandleInheritability.None, true);
            uint* offsetPointer = (uint*)&offset;
            var lastArea = areas[areas.Count - 1];
            byte* desiredAddress = lastArea.Address + lastArea.Size;
            var address = Win32FileMapping.MapViewOfFileEx(mmf.SafeMemoryMappedFileHandle.DangerousGetHandle(), 
                Win32FileMapping.FileMapAccess.Read | Win32FileMapping.FileMapAccess.Write,
                offsetPointer[1], offsetPointer[0], new UIntPtr((ulong)bytesToGrow), desiredAddress);
            if (address == null) {
                address = Win32FileMapping.MapViewOfFileEx(mmf.SafeMemoryMappedFileHandle.DangerousGetHandle(),
                   Win32FileMapping.FileMapAccess.Read | Win32FileMapping.FileMapAccess.Write,
                   offsetPointer[1], offsetPointer[0], new UIntPtr((ulong)bytesToGrow), null);
            }
            if (address == null) throw new Win32Exception();
            var area = new MemoryMappedArea {
                Address = address,
                Mmf = mmf,
                Size = bytesToGrow
            };
            areas.Add(area);
            if (desiredAddress != address) {
                offsets = offsets.Add(offset);
                addresses = addresses.Add(address);
            }
        }

        public byte* GetPointer(long offset)
        {
            CheckDisposed();
            int i = offsets.Length;
            if (i <= 128) // linear search is more efficient for small arrays. Experiments show 140 as the cutpoint on x64 and 100 on x86.
            {
                while (--i > 0 && offsets[i] > offset);
            }
            else // binary search is more efficient for large arrays
            {
                i = Array.BinarySearch<long>(offsets, offset);
                if (i < 0) i = ~i - 1;
            }
            return addresses[i] + offset - offsets[i];
        }

        private bool isDisposed;

        public void Dispose()
        {
            if (isDisposed) return;
            isDisposed = true;
            foreach (var a in this.areas)
            {
                Win32FileMapping.UnmapViewOfFile(a.Address);
                a.Mmf.Dispose();
            }
            fs.Dispose();
            areas.Clear();
        }

        private void CheckDisposed()
        {
            if (isDisposed) throw new ObjectDisposedException(this.GetType().Name);
        }

        public void Flush()
        {
            CheckDisposed();
            foreach (var area in areas)
            {
                if (!Win32FileMapping.FlushViewOfFile(area.Address, new IntPtr(area.Size))) {
                    throw new Win32Exception();
                }
            }
            fs.Flush(true);
        }
    }
}

Here is the Win32FileMapping class:

using System;
using System.Runtime.InteropServices;

namespace MmbpTree
{
    public static unsafe class Win32FileMapping
    {
        [Flags]
        public enum FileMapAccess : uint
        {
            Copy = 0x01,
            Write = 0x02,
            Read = 0x04,
            AllAccess = 0x08,
            Execute = 0x20,
        }

        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern byte* MapViewOfFileEx(IntPtr mappingHandle,
                                            FileMapAccess access,
                                            uint offsetHigh,
                                            uint offsetLow,
                                            UIntPtr bytesToMap,
                                            byte* desiredAddress);

        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern bool UnmapViewOfFile(byte* address);


        [DllImport("kernel32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool FlushViewOfFile(byte* address, IntPtr bytesToFlush);
    }
}

And here you have the Extensions class:

using System;

namespace MmbpTree
{
    public static class Extensions
    {
        public static T[] Add<T>(this T[] array, T element)
        {
            var result = new T[array.Length + 1];
            Array.Copy(array, result, array.Length);
            result[array.Length] = element;
            return result;
        }

        public static unsafe byte*[] Add(this byte*[] array, byte* element)
        {
            var result = new byte*[array.Length + 1];
            Array.Copy(array, result, array.Length);
            result[array.Length] = element;
            return result;
        }
    }
}

As you can see I take the unsafe approach. It's the only way to get the performance benefits of memory mapped files.

To work with this you need to consider the following concepts:

  • The block or page. This is your minimal region of continuous memory address and storage space you work with. The size of a block or page must be a multiple of underlying system page size (4Kb).
  • The initial file size. It must be a multiple of the block or page size and it must be a multiple of the system allocation granularity (64Kb).
  • The file growth. It must be a multiple of the block or page size and it must be a multiple of the system allocation granularity (64Kb).

For example you may want to work with a page size of 1Mb, a file growth of 64Mb and an initial size of 1Gb. You can get a pointer to a page by calling GetPointer, grow the file using Grow and flush the file using Flush:

const int InitialSize = 1024 * 1024 * 1024;
const int FileGrowth = 64 * 1024 * 1024;
const int PageSize = 1024 * 1024;
using (var gmmf = new GrowableMemoryMappedFile("mmf.bin", InitialSize))
{
    var pageNumber = 32;
    var pointer = gmmf.GetPointer(pageNumber * PageSize);

    // you can read the page content:
    byte firstPageByte = pointer[0];
    byte lastPageByte = pointer[PageSize - 1];

    // or write it
    pointer[0] = 3;
    pointer[PageSize -1] = 43;


    /* allocate more pages when needed */
    gmmf.Grow(FileGrowth);

    /* use new allocated pages */

    /* flushing the file writes to the underlying file */ 
    gmmf.Flush();

}
Jesús López
  • 8,338
  • 7
  • 40
  • 66
  • Throws exception when compiled against .NET 4.0, but works great otherwise. – Greg Mulvihill Aug 17 '16 at 15:52
  • So how does MapViewOfFileEx compare to using a MemoryMappedViewAccessor in .Net? Is there a very big speed difference? – millejos Jun 16 '17 at 20:53
  • 1
    MemoryMappedViewAccessor is even slower than FileStream. You can however get a pointer from MemoryMappedViewAccessor, and that is fast. But MemoryMappedFile.CreateViewAccessor doesn't allow you to ask for a desired address, this is key for keeping mapped blocks as contiguous as possible. – Jesús López Jun 19 '17 at 09:00
  • @GlennSlayden. Thank you. Currently I'm using another approach, introducing the concept of session and remapping when no contiguous memory can be allocated, in a way I always have the entire file mapped to contiguous memory. Take a look to a pet project I'm developing https://github.com/jesuslpm/PersistentHashing – Jesús López Jan 03 '19 at 18:51
  • Well, in fact so am I already. The proper way to do what your code suggests is to leverage `MEM_RESERVE_PLACEHOLDER`, and `MEM_REPLACE_PLACEHOLDER` in **MapViewOfFile3**, **VirtualAlloc2**, etc... – Glenn Slayden Jan 03 '19 at 21:11
  • 1
    Are there equivalent external external calls for Linux? I know about [`mmap`](http://man7.org/linux/man-pages/man2/mmap.2.html), I'm just not sure how to put it together. – Bruno Zell Nov 27 '19 at 01:55
  • I see you use raw pointer without using fixed(...). If GC move that pointer, you are dead :) See more https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/unsafe-code – ekalchev Feb 07 '23 at 14:58
1

I found that closing and recreating the mmf with the same name but new size works to all intents and purposes

                using (var mmf = MemoryMappedFile.CreateOrOpen(SenderMapName, 1))
                {
                    mmf.SafeMemoryMappedFileHandle.Close();
                }
                using (var sender = MemoryMappedFile.CreateNew(SenderMapName, bytes.Length))

and it's really fast.

Justin
  • 3,255
  • 3
  • 22
  • 20
1

The reason that the code does not compile is because it uses a non-existing overload. Either create a filestream yourself and pass it to the correct overload (assuming 2000 will be your new size):

FileStream fs = new FileStream("C:\MyFile.dat", FileMode.Open);
MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile(fs, "someName", 2000,
 MemoryMappedFileAccess.ReadWriteExecute, null, HandleInheritablity.None, false);

Or use this overload to skip the filstream creation:

MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile("C:\MyFile.dat", 
          FileMode.Open, "someName", 2000);
Edwin de Koning
  • 14,209
  • 7
  • 56
  • 74
0

Use the overload of MemoryMappedFile.CreateFromFile that takes a capacity parameter.

Martin Liversage
  • 104,481
  • 22
  • 209
  • 256
BlackICE
  • 8,816
  • 3
  • 53
  • 91