On .Net Core 3.1, I have many large 2D arrays where I need to operate on a slice of a single row in the array. The same slice may be used by multiple operations so I would like to perform the slice just once and reuse the slice.
The sample code below slices an array and then calls 2 functions to operate on the slice.
public void MyFunc()
{
double[,] array = ...; // populate the array
// select which part of the array to slice, values not important
int index0 = 0;
int startIndex1 = 1;
int sliceLength = 2;
// slice the array
ReadOnlySpan<double> slice = Slice(array, index0, startIndex1, sliceLength);
// do things with the slice
DoSomething1(slice);
DoSomething2(slice);
}
public unsafe ReadOnlySpan<double> Slice(double[,] array, int index0, int startIndex1, int sliceLength)
{
int arrayLength = array.GetLength(0) * array.GetLength(1);
int arrayStartIndex = index0 * array.GetLength(1) + startIndex1;
ReadOnlySpan<double> slice;
fixed (double* arrayPtr = array)
{
slice = new ReadOnlySpan<double>(arrayPtr, arrayLength).Slice(arrayStartIndex, sliceLength);
}
// does it matter if slice is returned inside or outside of the fixed block?
return slice;
}
public void DoSomething1(ReadOnlySpan<double> slice)
{
...
}
public void DoSomething2(ReadOnlySpan<double> slice)
{
...
}
"Fixed" ensures that the GC won't move "array" while I'm creating "slice". After creating "slice", if the GC moves "array", will it also update "slice" to refer to the new "array" address or will "slice" still reference the old address? In other words, will DoSomething1(...) and DoSomething2(...) always operate on the intended slice of the original array, or could they inadvertently operate on a random block of memory?
Also, does it matter whether "return slice;" is inside or outside the "fixed" block?
EDIT With inspiration from https://stackoverflow.com/a/40589439/13532170 I managed write a test to prove V0ldek is correct about GC updating the ReadOnlySpan's address when the parent array is moved.
public static unsafe void ReadOnlySpanTest()
{
// create 2D array
double[,] array = new double[,] { {1, 2, 3}, {4, 5, 6} };
// parameters to convert 2D array to 1D span
int arrayLength = array.GetLength(0) * array.GetLength(1);
int sliceStartIndex = 1;
int sliceLength = 2;
// create span
IntPtr arrayAddressBeforeMove;
ReadOnlySpan<double> spanFromPointer;
fixed (double* arrayPtr = array)
{
arrayAddressBeforeMove = (IntPtr)arrayPtr;
// spanFromPointer should contain { 2, 3 }
spanFromPointer = new ReadOnlySpan<double>(arrayPtr, arrayLength).Slice(sliceStartIndex, sliceLength);
}
// trick GC into moving the array
GC.AddMemoryPressure(10000000);
GC.Collect();
GC.RemoveMemoryPressure(10000000);
// check array address and span contents again
IntPtr arrayAddressAfterMove;
fixed (double* arrayPtr = array)
{
// arrayAddressAfterMove should be different from arrayAddressBeforeMove
arrayAddressAfterMove = (IntPtr) arrayPtr;
// spanFromPointer should still contain { 2, 3 }
}
}
Stepping over ReadOnlySpanTest() in the debugger, I can see that arrayAddressAfterMove != arrayAddressBeforeMove, indicating that GC did move my array. I can also see that spanFromPointer contains { 2, 3 } both before and after the array was moved. So doesn't matter that the ReadOnlySpan was created with a "fixed" block, it can still be safely used after leaving the "fixed" block.