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

9
我是一个64位PC,拥有128GB的内存,我使用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;

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

为什么多维数组不能超过 4294967295 个元素?我看到了以下回答:https://dev59.com/kXE95IYBdhLWcg3wXsyR#2338797

我也查看了gcAllowVeryLargeObjects的文档,并看到了以下备注。

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

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

我需要在内存中使用这些元素,因为我想使用 Intel 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);

System.Numerics.BigInteger。所以,我认为你需要用这种类型的指数器实现自己的数据结构。 - Dmitriy
6
由于您需要与非托管代码进行交互,我想您可以自己分配内存并将指针传递给函数,而不是使用多维数组。 - Evk
使用其他数据结构(例如Dictionary,其中键为$"{row}-{columns}")是否是一个选项?我同意性能可能会更差(因为您无法获得超快的数组索引查找),但它可以允许您存储更多数据,因为在幕后没有单个数组。 - mjwills
2个回答

8

为什么CLR不支持大数组

CLR不支持在托管堆上创建大数组有多种原因。

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

这篇博客文章详细介绍了限制大小(O)对象的原因。由于内存分配问题,决定限制最大对象大小。实施处理更大对象的成本与事实相比较,即没有太多使用案例需要这样的大对象,而那些确实需要此类大对象的情况通常是程序员设计的错误。 但由于对于CLR来说,所有都是对象,所以该限制也适用于数组。为了强制执行此限制,数组索引器设计为带有有符号整数。

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

上述提到的博客文章还演示了如何在不进入非托管领域的情况下实现大数组。

但正如Evk在评论中指出的那样,您想通过PInvoke将整个数组作为一个整体传递给外部函数。这意味着您需要在非托管堆上拥有该数组,或者在调用过程中对其进行封送处理。但是,对于如此大的数组来说,整个封送处理是一个不好的想法。

解决方法

因此,由于托管堆不可用,您需要在非托管堆上分配空间并使用该空间存储数组。

假设您需要8 GB的空间:

long size = (1L << 33);
IntPtr basePointer = System.Runtime.InteropServices.Marshal.AllocHGlobal((IntPtr)size);

太好了!现在您有一个虚拟内存区域,可以存储多达8 GB的数据。

如何将其转换为数组?

在C#中有两种方法

"不安全"方法

这将让您使用指针。指针可以转换为数组。(在普通C语言中它们经常是一体的)

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

这里是一个指针

"Marshal"方法

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

您将要使用的两个主要功能是PtrToStructure和反向StructureToPtr。其中一个可以从非托管堆的指定位置获取值类型(例如double)的副本。使用另一个,您将在非托管堆上放置值类型的副本。

这两种方法都是“不安全”的。您需要了解指针

常见陷阱包括但不限于:

  • 忘记严格检查边界
  • 混淆元素的大小
  • 搞乱对齐方式
  • 混淆所需的2D数组类型
  • 忘记使用2D数组填充
  • 忘记释放内存
  • 忘记释放内存并仍在使用它

您可能想将2D数组设计转换为1D数组设计


无论如何,您都希望将其全部包装到具有适当检查和析构函数的类中。

启发基本示例

以下是一个基于非托管堆的“类似”数组的通用类。

功能包括:

  • 它具有接受64位整数的索引访问器。
  • 它限制了T可以成为的类型为值类型。
  • 它具有边界检查并且是可处置的。
如果您观察到,我没有进行任何类型检查,因此如果Marshal.SizeOf未返回正确的数字,则我们将落入上述某个陷阱中。您需要自己实现以下特性:2D访问器和2D数组算术(取决于其他库需要什么,通常是像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));
    }
}

感谢您的回答。使用marshal方法可能是一种解决方法。 CLR不支持在托管堆上使用大数组有多个原因,其中一些是技术性的,另一些可能是“范式”的。在这里添加一些细节会很好。我仍然不明白为什么有这个限制。 - Wollmich
我所链接的博客文章提供了一位 .Net 设计团队成员关于为什么存在限制的深刻见解。我会在我的回答中更加清晰地表达。 - MrPaulch

1
我使用了这个答案MrPaulch 的基本 "Marshal" 方法示例来创建下面的类 HugeMatrix<T>
public class HugeMatrix<T> : IDisposable
    where T : struct
{
    public IntPtr Pointer
    {
        get { return pointer; }
    }

    private IntPtr pointer = IntPtr.Zero;

    public int NRows
    {
        get { return Transposed ? _NColumns : _NRows; }
    }

    private int _NRows = 0;

    public int NColumns
    {
        get { return Transposed ? _NRows : _NColumns; }
    }

    private int _NColumns = 0;

    public bool Transposed
    {
        get { return _Transposed; }
        set { _Transposed = value; }
    }

    private bool _Transposed = false;

    private ulong b_element_size = 0;
    private ulong b_row_size = 0;
    private ulong b_size = 0;
    private bool disposed = false;


    public HugeMatrix()
        : this(0, 0)
    {
    }

    public HugeMatrix(int nrows, int ncols, bool transposed = false)
    {
        if (nrows < 0)
            throw new ArgumentException("The number of rows can not be negative");
        if (ncols < 0)
            throw new ArgumentException("The number of columns can not be negative");
        _NRows = transposed ? ncols : nrows;
        _NColumns = transposed ? nrows : ncols;
        _Transposed = transposed;
        b_element_size = (ulong)(Marshal.SizeOf(typeof(T)));
        b_row_size = (ulong)_NColumns * b_element_size;
        b_size = (ulong)_NRows * b_row_size;
        pointer = Marshal.AllocHGlobal((IntPtr)b_size);
        disposed = false;
    }

    public HugeMatrix(T[,] matrix, bool transposed = false)
        : this(matrix.GetLength(0), matrix.GetLength(1), transposed)
    {
        int nrows = matrix.GetLength(0);
        int ncols = matrix.GetLength(1);
        for (int i1 = 0; i1 < nrows; i1++)
            for (int i2 = 0; i2 < ncols; i2++)
                this[i1, i2] = matrix[i1, i2];
    }

    public void Dispose()
    {
        if (!disposed)
        {
            Marshal.FreeHGlobal(pointer);
            _NRows = 0;
            _NColumns = 0;
            _Transposed = false;
            b_element_size = 0;
            b_row_size = 0;
            b_size = 0;
            pointer = IntPtr.Zero;
            disposed = true;
        }
    }

    public void Transpose()
    {
        _Transposed = !_Transposed;
    }

    public T this[int i_row, int i_col]
    {
        get
        {
            IntPtr p = getAddress(i_row, i_col);
            return (T)Marshal.PtrToStructure(p, typeof(T));
        }
        set
        {
            IntPtr p = getAddress(i_row, i_col);
            Marshal.StructureToPtr(value, p, true);
        }
    }

    private IntPtr getAddress(int i_row, int i_col)
    {
        if (disposed)
            throw new ObjectDisposedException("Can't access the matrix once it has been disposed");
        if (i_row < 0)
            throw new IndexOutOfRangeException("Negative row indices are not allowed");
        if (i_row >= NRows)
            throw new IndexOutOfRangeException("Row index is out of bounds of this matrix");
        if (i_col < 0)
            throw new IndexOutOfRangeException("Negative column indices are not allowed");
        if (i_col >= NColumns)
            throw new IndexOutOfRangeException("Column index is out of bounds of this matrix");
        int i1 = Transposed ? i_col : i_row;
        int i2 = Transposed ? i_row : i_col;
        ulong p_row = (ulong)pointer + b_row_size * (ulong)i1;
        IntPtr p = (IntPtr)(p_row + b_element_size * (ulong)i2);
        return p;
    }
}

现在我可以使用Intel 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] IntPtr a, lapack_int lda, [In, Out] double[] w);

对于参数IntPtr a,我传递了HugeMatrix<T>类的Pointer属性。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接