P/Invoking using Span<T>

Observing the troops (Buddha Eden Garden, Bombarral, Portugal) by aalmada

Before Span<T> Era

.NET has three different ways to allocate contiguous memory:

  • new[] — Allocated on the heap and managed by the GC.
  • stackalloc[] — Allocated on the stack and released automatically when exiting scope.
  • Marshal.AllocHGlobal() — Allocated on the heap. Caller is responsible for releasing it by calling Marshal.FreeHGlobal().
  • The managed array needs to be fixed so that the GC doesn’t move it around while the sum is calculated. The memory is automatically released once no references are found by the GC.
  • The stack allocation is the simplest one as it stays in its position until automatically released when the code exits the method.
  • Memory allocated with Marshal.AllocHGlobal() is not managed by the GC so it’s not moved around but it has to be explicitly released. Although the GC doesn’t managed this memory, it’s good policy to inform it of the total heap memory used by the application. This can be done using GC.AddMemoryPressure() and GC.RemoveMemoryPressure():

Unsafe and MemoryMarshal classes

.NET has always supported passing value-type arguments by reference. Recently it was added support for return-by-reference and read-only-references.

ref T AsRef<T>(void* source);ref T AsRef<T>(in T source)void* AsPointer<T>(ref T value);ref T Add<T>(ref T source, int elementOffset);ref T Subtract<T>(ref T source, int elementOffset);
ref T GetReference<T>(Span<T> span);ref T GetReference<T>(ReadOnlySpan<T> span);
  • For the managed array, although we get a reference to the first position, it still has to be fixed which returns a pointer, forcing the use of the unsafe keyword
  • For the unmanaged allocation, there is no Span<T> or ReadOnlySpan<T> constructor that takes an IntPtr argument so it has to be converted to a pointer, also forcing the use of the unsafe keyword.
  • The use of the in keyword is optional in a method call.

Span<T> arguments

One of the advantages of using Span<T> is that methods can be abstracted from how the memory was allocated. We can move the call to the P/Invoke into a static Sum method with a ReadOnlySpan<T> argument.

MemoryManager<T>

The code for the unmanaged array handling is still somewhat complex as it has to release resources in a robust way. We should hide it in some IDisposable implementation.

Performance

I now want to know if these abstraction affect the performance in any way. To evaluate it, I commented out the buffer iteration in the native code so that only the memory management and the P/Invoke is taken into account.

  • The use of Span<T> makes almost no difference for the managed array.
  • There’s a big difference when using Span<T> with stack allocations.
  • “Fixing” the buffer even when not required, doesn’t seem to affect performance.
  • The use of the NativeMemoryManager<T> introduces some overhead (~50 percentage points).
  • The factor of time-elapsed increase is constant for all scenarios except for the stack allocation where there is a big difference between using Span<T> and not using it.
  • Interesting to see that the factor is 1 (100%) for all unmanaged allocation scenarios.

Conclusion

The use of Span<T> for P/Invoke calls allows cleaner, strongly-typed, reusable code.

 by the author.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store