Span和二维数组

19

使用新的System.Memory Span 结构处理二维数组数据是否可行?

double[,] testMulti = 
    {
        { 1, 2, 3, 4 },
        { 5, 6, 7, 8 },
        { 9, 9.5f, 10, 11 },
        { 12, 13, 14.3f, 15 }
    };

double[] testArray = { 1, 2, 3, 4 };
string testString = "Hellow world";

testMulti.AsSpan(); // Compile error
testArray.AsSpan();
testString.AsSpan();

虽然testArray和testString都有AsSpan扩展,但testMulti没有这样的扩展。

Span的设计是否仅限于处理单维数组数据?
我还没有找到使用Span处理testMulti数组的明显方法。

6个回答

11

您可以使用未托管的内存创建一个 Span 对象。这将使您能够随意地进行切片和拼接操作

unsafe
{
    Span<T> something = new Span<T>(pointerToarray, someLength); 
}

完整演示

unsafe public static void Main(string[] args)
{
   double[,] doubles =  {
         { 1, 2, 3, 4 },
         { 5, 6, 7, 8 },
         { 9, 9.5f, 10, 11 },
         { 12, 13, 14.3f, 15 }
      };

   var length = doubles.GetLength(0) * doubles.GetLength(1);

   fixed (double* p = doubles)
   {
      var span = new Span<double>(p, length);
      var slice = span.Slice(6, 5);

      foreach (var item in slice)
         Console.WriteLine(item);
   }
}

输出

7
8
9
9.5
10

其他选项包括将其重新分配到单维数组中,付出惩罚并 不要通过Go

  • BlockCopy
  • 或直接调用memcpy并使用unsafe和指针
  • Cast<T> 例如multiDimensionalArrayData.Cast<byte>().ToArray()

前两个选项对于大型数组来说性能更高。


我认为你的代码中长度不正确,应该是 var length = testMulti.GetLength(0) * testMulti.GetLength(1); - Mick
由于没有更好的答案,我将把这个标记为答案,显然,一个不涉及不安全代码的解决方案会更好。将结构转换为锯齿数组似乎是绕过我认为是System.Memory库设计中的一个漏洞的最佳方法。 - Mick
@Mick 没有一个合适的答案,这是实现和设计的问题。内部 span 不安全,并使用指针来处理数据,如果需要通过各个维度组合,则在您的情况下使用锯齿状的数组是合适的。然而,即使使用指针,也没有一种通用的解决方案可以很好地工作,而 fixed 只能用于特定类型。所有这些不是该功能的限制或尚未构建,而是 CLR 本身的限制,无法重新分配内存。因此,您可以通过指针创建 span,以填补这一空白。 - TheGeneral
1
我认为你的长度计算过大了。文档说,T类型的Span构造函数需要* T元素的数量*,而不是字节大小。你可以删除 * sizeof(double) - N8allan

7
您可以使用新的MemoryMarshal.CreateSpanMemoryMarshal.GetArrayDataReference来实现此操作。
public static Span<T> AsSpan<T>(this Array array)
{
    return MemoryMarshal.CreateSpan(ref Unsafe.As<byte, T>(ref MemoryMarshal.GetArrayDataReference(array)), array.Length);
}

dotnetfiddle

这适用于所有数组的维度。如果你想让泛型类型推断生效,你需要为每个秩(维度级别)编写不同的函数,例如AsSpan<T>(this T[,] array)AsSpan<T>(this T[,,] array)


4
如John Wu所提到的,Spans是一维的。当然你可以自己实现一个二维的Span,但是Microsoft已经为我们做好了这个工作。
请查看文档here。 你可以在这里找到NuGet包here。 该包也提供了Memory2D。
 var arr = new int[,] { {1,2,3},{2,2,3},{3,2,3} };
 var spn = arr.AsSapn2D();
 // Now use it similar to a normal span
 // The access is of course a bit different since we are using a 2D data structure.
 Console.WriteLine(spn[0..2,..]);

2

所有的跨度都是一维的,因为内存是一维的。

当然,你可以将各种结构映射到一维内存上,但Span类不会为你完成这个过程。但你可以很容易地自己编写一些代码,例如:

public class Span2D<T> where T : struct
{
    protected readonly Span<T> _span;
    protected readonly int _width;
    protected readonly int _height;

    public Span2D(int height, int width)
    {
        T[] array = new T[_height * _width];
        _span = array.AsSpan();
    }

    public T this[int row, int column]
    {
        get
        {
            return _span[row * _height + column];
        }
        set
        {
            _span[row * _height + column] = value;
        }
    }
}

实现Slice()的难点在于,对于二维结构来说,语义有些模糊。你可能只能通过其中一个维度来切片这种结构,因为通过另一个维度切片会导致不连续的内存。


这是一个很棒的想法。 - TheGeneral
1
你只能在另一个 ref struct 上容纳一个 ref struct 类型的字段。Span<T> 是一个 ref struct,而在你的代码中,Span2D<T> 是一个类。 - Eduard Dumitru
@EduardDumitru 是正确的 - 这段代码不能直接编译。要么 Span2D 需要是一个 ref struct 而不是一个类,要么持有数据的类成员需要是 Memory 而不是 Span。 - Joel Mueller

0
也许使用不规则数组而不是多维数组会更成功。
double[][] testMulti = 
    {
        new double[] { 1, 2, 3, 4 },
        new double[] { 5, 6, 7, 8 },
        new double[] { 9, 9.5f, 10, 11 },
        new double[] { 12, 13, 14.3f, 15 }
    };

Span<double[]> span = testMulti.AsSpan(2, 1);
Span<double> slice = span[0].AsSpan(1, 2);

foreach (double d in slice)
    Console.WriteLine(d);

slice[0] = 10.5f;

Console.Write(string.Join(", ", testMulti[2]));

Console.ReadLine();

输出

9.5
10
9, 10.5, 10, 11

严格来说,这并不是对我的问题的回答,因为testMulti类型是不同的。 - Mick
似乎有点奇怪,这适用于锯齿数组,但是多维数组没有简单的解决方案。 - Mick

0

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