文章

C# 无 unsafe 的非托管大数组

在 C# 里,有时候我需要能够申请一个很大的数组、使用之、然后立即释放其占用的内存。

由于在 C# 里提供的 int[] array = new int[1000000]; 这样的数组,其内存释放很难由程序员完全控制,在申请一个大数组后,程序可能会变得很慢。

UnmanagedArray

/// <summary>
/// 元素类型为sbyte, byte, char, short, ushort, int, uint, long, ulong, float, double, decimal, bool或其它struct的非托管数组。
/// <para>不能使用enum类型作为T。</para>
/// </summary>
/// <typeparam name="T">sbyte, byte, char, short, ushort, int, uint, long, ulong, float, double, decimal, bool或其它struct, 不能使用enum类型作为T。</typeparam>
public class UnmanagedArray<T> : UnmanagedArrayBase where T : struct
{
    /// <summary>
    ///元素类型为sbyte, byte, char, short, ushort, int, uint, long, ulong, float, double, decimal, bool或其它struct的非托管数组。 
    /// </summary>
    /// <param name="count"></param>
    [MethodImpl(MethodImplOptions.Synchronized)]
    public UnmanagedArray(int count) : base(count, Marshal.SizeOf(typeof(T)))
    {
    }
      
    /// <summary>
    /// 获取或设置索引为<paramref name="index"/>的元素。
    /// </summary>
    /// <param name="index"></param>
    /// <returns></returns>
    public T this[int index]
    {
        get
        {
            if (index < 0 || index >= this.Count)
                throw new IndexOutOfRangeException("index of UnmanagedArray is out of range");
 
            var pItem = this.Header + (index * elementSize);
            //var obj = Marshal.PtrToStructure(pItem, typeof(T));
            //T result = (T)obj;
            T result = Marshal.PtrToStructure<T>(pItem);// works in .net 4.5.1
            return result;
        }
        set
        {
            if (index < 0 || index >= this.Count)
                throw new IndexOutOfRangeException("index of UnmanagedArray is out of range");
                 
            var pItem = this.Header + (index * elementSize);
            //Marshal.StructureToPtr(value, pItem, true);
            Marshal.StructureToPtr<T>(value, pItem, true);// works in .net 4.5.1
        }
    }
 
    /// <summary>
    /// 按索引顺序依次获取各个元素。
    /// </summary>
    /// <returns></returns>
    public IEnumerable<T> GetElements()
    {
        if (!this.disposed)
        {
            for (int i = 0; i < this.Count; i++)
            {
                yield return this[i];
            }
        }
    }
}
 
/// <summary>
/// 非托管数组的基类。
/// </summary>
public abstract class UnmanagedArrayBase : IDisposable
{
    /// <summary>
    /// 数组指针。
    /// </summary>
    public IntPtr Header { get; private set; }
 
    /// <summary>
    /// 元素数目。
    /// </summary>
    public int Count { get; private set; }
 
    /// <summary>
    /// 单个元素的字节数。
    /// </summary>
    protected int elementSize;
 
    /// <summary>
    /// 申请到的字节数。(元素数目 * 单个元素的字节数)。
    /// </summary>
    public int ByteLength
    {
        get { return this.Count * this.elementSize; }
    }
             
    /// <summary>
    /// 非托管数组。
    /// </summary>
    /// <param name="elementCount">元素数目。</param>
    /// <param name="elementSize">单个元素的字节数。</param>
    [MethodImpl(MethodImplOptions.Synchronized)]
    protected UnmanagedArrayBase(int elementCount, int elementSize)
    {
        this.Count = elementCount;
        this.elementSize = elementSize;
 
        int memSize = elementCount * elementSize;
        this.Header = Marshal.AllocHGlobal(memSize);
 
        allocatedArrays.Add(this);
    }
 
    private static readonly List<IDisposable> allocatedArrays = new List<IDisposable>();
 
    /// <summary>
    /// 立即释放所有<see cref="UnmanagedArray"/>。
    /// </summary>
    [MethodImpl(MethodImplOptions.Synchronized)]
    public static void FreeAll()
    {
        foreach (var item in allocatedArrays)
        {
            item.Dispose();
        }
        allocatedArrays.Clear();
    }
 
    ~UnmanagedArrayBase()
    {
        Dispose();
    }
 
    #region IDisposable Members
 
    /// <summary>
    /// Internal variable which checks if Dispose has already been called
    /// </summary>
    protected Boolean disposed;
 
    /// <summary>
    /// Releases unmanaged and - optionally - managed resources
    /// </summary>
    /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
    protected void Dispose(Boolean disposing)
    {
        if (disposed)
        {
            return;
        }
 
        if (disposing)
        {
            //Managed cleanup code here, while managed refs still valid
        }
        //Unmanaged cleanup code here
        IntPtr ptr = this.Header;
 
        if (ptr != IntPtr.Zero)
        {
            this.Count = 0;
            this.Header = IntPtr.Zero;
            Marshal.FreeHGlobal(ptr);
        }
 
        disposed = true;
   }
 
    /// <summary>
    /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
    /// </summary>
    public void Dispose()
    {
       this.Dispose(true);
       GC.SuppressFinalize(this);
    }
 
    #endregion             
}

如何使用

UnmanagedArray 使用方式十分简单,就像一个普通的数组一样:

Using UnamangedAray is just like a normal array(int[], vec3[], etc.):

internal static void TypicalScene()
{
    const int count = 100;

    // 测试float类型
    var floatArray = new UnmanagedArray<float>(count);
    for (int i = 0; i < count; i++)
    {
        floatArray[i] = i;
    }
    for (int i = 0; i < count; i++)
    {
        var item = floatArray[i];
        if (item != i)
        { throw new Exception(); }
    }

    // 测试int类型
    var intArray = new UnmanagedArray<int>(count);
    for (int i = 0; i < count; i++)
    {
       intArray[i] = i;
    }
    for (int i = 0; i < count; i++)
    {
        var item = intArray[i];
        if (item != i)
        { throw new Exception(); }
    }

    // 测试bool类型
    var boolArray = new UnmanagedArray<bool>(count);
    for (int i = 0; i < count; i++)
    {
        boolArray[i] = i % 2 == 0;
    }
    for (int i = 0; i < count; i++)
    {
        var item = boolArray[i];
        if (item != (i % 2 == 0))
        { throw new Exception(); }
    }

    // 测试vec3类型
    var vec3Array = new UnmanagedArray<vec3>(count);
    for (int i = 0; i < count; i++)
    {
        vec3Array[i] = new vec3(i * 3 + 0, i * 3 + 1, i * 3 + 2);
    }
    for (int i = 0; i < count; i++)
    {
        var item = vec3Array[i];
        var old = new vec3(i * 3 + 0, i * 3 + 1, i * 3 + 2);
        if (item.x != old.x || item.y != old.y || item.z != old.z)
        { throw new Exception(); }
    }

    // 测试foreach
    foreach (var item in vec3Array.GetElements())
    {
        Console.WriteLine(item);
    }

    // 释放此数组占用的内存,这之后就不能再使用vec3Array了。
    vec3Array.Dispose();

    // 立即释放所有非托管数组占用的内存,这之后就不能再使用上面申请的数组了。
    UnmanagedArrayBase.FreeAll();
}

快速读写 UnmanagedArray

UnmanagedArrayHelper

由于很多时候需要申请和使用很大的 UnmanagedArray,直接使用 this [index] 索引方式速度会偏慢,所以我添加了几个辅助方法,专门解决快速读写 UnmanagedArray 的问题。

public static class UnmanagedArrayHelper
{
    /// <summary>
    /// 获取非托管数组的第一个元素的地址。
    /// </summary>
    /// <param name="array"></param>
    /// <returns></returns>
    public static unsafe void* FirstElement(this UnmanagedArrayBase array)
    {
       var header = (void*)array.Header;
        return header;
    }

    public static unsafe void* LastElement(this UnmanagedArrayBase array)
    {
        var last = (void*)(array.Header + (array.ByteLength - array.ByteLength / array.Length));
        return last;
    }

    /// <summary>
    /// 获取非托管数组的最后一个元素的地址再向后一个单位的地址。
    /// </summary>
    /// <param name="array"></param>
    /// <returns></returns>
    public static unsafe void* TailAddress(this UnmanagedArrayBase array)
    {
        var tail = (void*)(array.Header + array.ByteLength);
        return tail;
    }
}

如何使用

这个类型实现了 3 个扩展方法,可以获取 UnmanagedArray 的第一个元素的位置、最后一个元素的位置、最后一个元素 + 1 的位置。用这种 unsafe 的方法可以实现 C 语言一样的读写速度。

下面是一个例子。用 unsafe 的方式读写 UnmanagedArray,速度比 this [index] 方式快 10 到 70 倍

public static void TypicalScene()
{
    int length = 1000000;
    UnmanagedArray<int> array = new UnmanagedArray<int>(length);
    UnmanagedArray<int> array2 = new UnmanagedArray<int>(length);

    long tick = DateTime.Now.Ticks;
    for (int i = 0; i < length; i++)
    {
        array[i] = i;
    }
    long totalTicks = DateTime.Now.Ticks - tick;

    tick = DateTime.Now.Ticks;
    unsafe
    {
        int* header = (int*)array2.FirstElement();
        int* last = (int*)array2.LastElement();
        int* tailAddress = (int*)array2.TailAddress();
        int value = 0;
        for (int* ptr = header; ptr <= last/*or: ptr < tailAddress*/; ptr++)
        {
            *ptr = value++;
        }
    }
    long totalTicks2 = DateTime.Now.Ticks - tick;
    Console.WriteLine("ticks: {0}, {1}", totalTicks, totalTicks2);// unsafe method works faster.

    for (int i = 0; i < length; i++)
    {
        if (array[i] != i)
        {
            Console.WriteLine("something wrong here");
        }
        if (array2[i] != i)
        {
            Console.WriteLine("something wrong here");
        }
    }

    array.Dispose();
    array2.Dispose();
}

License:  CC BY 4.0