19

Is it possible to use the new System.Memory Span struct with two dimensional arrays of data?

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();

Whilst testArray and testString have a AsSpan extension, no such extension exists for testMulti.

Is the design of Span limited to working with single dimensional arrays of data?
I've not found an obvious way of working with the testMulti array using Span.

Mick
  • 6,527
  • 4
  • 52
  • 67

6 Answers6

10

You can create a Span with unmanaged memory. This will allow you to Slice and Dice indiscriminately.

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

Full Demo

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);
   }
}

Output

7
8
9
9.5
10

Other options would be to reallocate to a single dimension array, cop the penalty and do not Pass-Go

  • BlockCopy
  • or p/invoke memcpy directly and use unsafe and pointers
  • Cast<T> eg multiDimensionalArrayData.Cast<byte>().ToArray()

The first 2 will be more performant for large arrays.

TheGeneral
  • 79,002
  • 9
  • 103
  • 141
  • I think the length in your code is incorrect it should be var length = testMulti.GetLength(0) * testMulti.GetLength(1); – Mick Oct 11 '18 at 01:56
  • For lack of a better answer I'll mark this as the answer, obviously a solution that doesn't involve unsafe code would be preferable. Converting the structure to a jagged array seems to be the best way around what I think is a gap in the design of System.Memory library. – Mick Oct 11 '18 at 23:57
  • @Mick yeah there is no suitable answer really and its one of implementation and design, internally span is unsafe and pointers to do its thing, using a jagged array in your case is suitable, yet not if need to slice through dimensions. Though a blanket solution even using pointers is not going to work that well either and fixed can only be used with certain types. All this is not so much a limitation of the feature or that it just hasnt been built, its the limitation of the CLR without reallocating. As such you can create a span by pointers which seems to fill in the gap – TheGeneral Oct 12 '18 at 00:28
  • 1
    I think your length computation is oversized. The documentation says that the Span constructor of T takes the *number of T elements*, not the size in bytes. You can remove the `* sizeof(double)` – N8allan Jul 06 '19 at 04:18
6

You can use the new MemoryMarshal.CreateSpan and MemoryMarshal.GetArrayDataReference for this.

public static Span<T> AsSpan<T>(this Array array)
{
    return MemoryMarshal.CreateSpan(ref Unsafe.As<byte, T>(ref MemoryMarshal.GetArrayDataReference(array)), array.Length);
}

dotnetfiddle

This works on all dimensions of arrays. If you want generic type inference to work, you will need separate functions for every rank (level of dimension), such as AsSpan<T>(this T[,] array) and AsSpan<T>(this T[,,] array).

Charlieface
  • 52,284
  • 6
  • 19
  • 43
3

As John Wu already mentioned spans are one dimensional. You could of course implement a 2D span yourself, but Microsoft already did that for us.

Have a look into the docs here.
You can find the addressing nuget package here.
The package also provides a 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,..]);
Alex Fischer
  • 193
  • 1
  • 2
  • 12
2

All spans are one-dimensional because memory is one-dimensional.

You can of course map all manner of structures onto one-dimensional memory, but the Span class won't do it for you. But you could easily write something yourself, for example:

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;
        }
    }
}

The tricky part is implementing Slice(), since the semantics are sort of ambiguous for a two-dimensional structure. You can probably only slice this sort of structure by one of the dimensions, since slicing it by the other dimension would result in memory that is not contiguous.

John Wu
  • 50,556
  • 8
  • 44
  • 80
  • This is a nifty idea as well – TheGeneral Oct 11 '18 at 01:13
  • 1
    You can only accomodate a field of a `ref struct` type on another `ref struct`. `Span` is a `ref struct` while, in your code, `Span2D` is a class. – Eduard Dumitru Nov 29 '19 at 21:19
  • @EduardDumitru is correct - this code won't compile as-is. Either Span2D would need to be a `ref struct` instead of a class, or the class member holding the data would need to be a Memory instead of a Span. – Joel Mueller Feb 04 '20 at 18:57
0

As @saruman I don't believe it's possible.

You will need to get a new single dimension array first using techniques shown in Fast way to convert a two dimensional array to a List ( one dimensional ) or Convert 2 dimensional array, for example.

tymtam
  • 31,798
  • 8
  • 86
  • 126
0

Perhaps there's more success to be had working with a jagged array instead of a multidimensional array.

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();

OUTPUT

9.5
10
9, 10.5, 10, 11
Mick
  • 6,527
  • 4
  • 52
  • 67
  • Strictly speaking not an answer to my question as the testMulti type is different. – Mick Oct 11 '18 at 01:36
  • Seems a little odd that this works for a jagged array but there's no easy solution for multidimensional arrays – Mick Oct 11 '18 at 03:28