为什么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));
}
}
Dictionary
,其中键为$"{row}-{columns}")是否是一个选项?我同意性能可能会更差(因为您无法获得超快的数组索引查找),但它可以允许您存储更多数据,因为在幕后没有单个数组。 - mjwills