具有超过 65535^2 个元素的二维数组 --> 数组维度超出支持范围

Wol*_*ich 7 c# arrays multidimensional-array .net-4.5 gcallowverylargeobjects

我有一台带有 128 GB RAM 的 64 位 PC,我使用的是 C# 和 .NET 4.5。我有以下代码:

double[,] m1 = new double[65535, 65535];
long l1 = m1.LongLength;

double[,] m2 = new double[65536, 65536]; // Array dimensions exceeded supported range
long l2 = m2.LongLength;
Run Code Online (Sandbox Code Playgroud)

我知道<gcAllowVeryLargeObjects enabled="true" />并且我已将其设置为 true。

为什么多维数组不能有超过 4294967295 个元素?我看到以下答案/sf/answers/163715821/

我还检查了gcAllowVeryLargeObjects的文档,我看到了以下评论。

数组中的最大元素数为UInt32.MaxValue (4294967295)。

我不明白为什么有这个限制?有解决方法吗?是否计划在即将发布的 .net 版本中删除此限制?

我需要内存中的元素,因为我想计算例如使用英特尔 MKL 的对称特征值分解。

[DllImport("custom_mkl", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true, SetLastError = false)]
internal static extern lapack_int LAPACKE_dsyevd(
    int matrix_layout, char jobz, char uplo, lapack_int n, [In, Out] double[,] a, lapack_int lda, [In, Out] double[] w);
Run Code Online (Sandbox Code Playgroud)

MrP*_*lch 6

免责声明:这个结果比预期的要长

为什么 CLR 不支持大数组

CLR 不支持托管堆上的大型数组的原因有多种。

其中一些是技术性的,其中一些可能是“范式”。

这篇博文探讨了存在限制的一些原因。本质上,由于内存碎片,决定限制(大写 O)对象的最大大小。实现处理较大对象的成本与以下事实进行权衡,即需要如此大对象的用例并不多,而那些需要如此大对象的用例——在大多数情况下——是由于程序员的设计谬误。因为对于 CLR 来说,一切都是对象,所以这个限制也适用于数组。为了强制执行此限制,数组索引器设计为带符号整数。

但是一旦您确定您的程序设计要求您拥有如此大的数组,您将需要一种解决方法。

上面提到的博客文章还展示了您可以在不进入非托管领域的情况下实现大数组。

但正如 Evk 在评论中指出的,您希望通过 PInvoke 将整个数组传递给外部函数。这意味着您将需要非托管堆上的数组,否则必须在调用期间对其进行封送处理。对于这么大的数组,将整件事编组是一个坏主意。

解决方法

因此,由于托管堆是不可能的,因此您需要在非托管堆上分配空间并将该空间用于数组。

假设您需要 8 GB 的空间:

long size = (1L << 33);
IntPtr basePointer = System.Runtime.InteropServices.Marshal.AllocHGlobal((IntPtr)size);
Run Code Online (Sandbox Code Playgroud)

伟大的!现在您在虚拟内存中有一个区域,您可以在其中存储多达 8 GB 的数据。

我如何把它变成一个数组?

那么在 C# 中有两种方法

“不安全”的方法

这将让您使用指针。指针可以转换为数组。(在香草 C 中,它们通常是一回事)

如果您对如何通过指针实现二维数组有很好的想法,那么这将是您的最佳选择。

这是一个指针

“元帅”方法

您不需要不安全的上下文,而是必须将数据从托管堆“编组”到非托管堆。您仍然需要了解指针算术。

您要使用的两个主要函数是PtrToStructure和反向StructureToPtr。使用一个,您将从非托管堆上的指定位置获得值类型(例如双精度)的副本。对于另一个,您将在非托管堆上放置一个值类型的副本。

从某种意义上说,这两种方法都是“不安全的”。你需要知道你的指针

常见的陷阱包括但不限于:

  • 忘记严格检查边界
  • 混合元素的大小
  • 搞乱对齐方式
  • 混合你想要什么样的二维数组
  • 忘记用二维数组填充
  • 忘记释放内存
  • 忘记释放内存并使用它

您可能希望将 2D 阵列设计转换为 1D 阵列设计


在任何情况下,您都希望使用适当的检查和析构函数将其全部包装到一个类中。

灵感的基本示例

接下来是一个基于非托管堆的“类似”数组的泛型类。

功能包括:

  • 它有一个接受 64 位整数的索引访问器。
  • 它限制了T可以变成值类型的类型。
  • 它有边界检查并且是一次性的。

如果您注意到,我不进行任何类型检查,因此如果Marshal.SizeOf未能返回正确的数字,我们将陷入上述坑之一。

您必须自己实现的功能包括:

  • 2D Accessor 和 2D Array 算术(取决于其他库的期望,通常类似于 p = x * size + y
  • 用于 PInvoke 目的的公开指针(或内部调用)

因此,如果有的话,仅将其用作灵感。

using static System.Runtime.InteropServices.Marshal;

public class LongArray<T> : IDisposable where T : struct {
    private IntPtr _head;
    private Int64 _capacity;
    private UInt64 _bytes;
    private Int32 _elementSize;

    public LongArray(long capacity) {
        if(_capacity < 0) throw new ArgumentException("The capacity can not be negative");
        _elementSize = SizeOf(default(T));
        _capacity = capacity;
        _bytes = (ulong)capacity * (ulong)_elementSize;

        _head = AllocHGlobal((IntPtr)_bytes);   
    }

    public T this[long index] {
        get {
            IntPtr p = _getAddress(index);

            T val = (T)System.Runtime.InteropServices.Marshal.PtrToStructure(p, typeof(T));

            return val;
        }
        set {
            IntPtr p = _getAddress(index);

            StructureToPtr<T>(value, p, true);
        }
    }

    protected bool disposed = false;
    public void Dispose() {
        if(!disposed) {
            FreeHGlobal((IntPtr)_head);
            disposed = true;
        }
    }

    protected IntPtr _getAddress(long index) {
        if(disposed) throw new ObjectDisposedException("Can't access the array once it has been disposed!");
        if(index < 0) throw new IndexOutOfRangeException("Negative indices are not allowed");
        if(!(index < _capacity)) throw new IndexOutOfRangeException("Index is out of bounds of this array");
        return (IntPtr)((ulong)_head + (ulong)index * (ulong)(_elementSize));
    }
}
Run Code Online (Sandbox Code Playgroud)