Search code examples
c#.netmemoryunsafe

C# - Can MemoryMarshal.Cast() convert any struct or primitive with compatible endianness on Linux and Windows


I am looking for fast way to process arrays of PointF type. Casting them with the following code gives a span, so i can use fma, avx and sse intrinsics to speed up the code. This function works correctly on my machine. The problem is in safety. Is it safe to perform such cast on different platforms? Microsoft documentation says that big-endian architecture can reverse values if they are splitted or merged:

https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.memorymarshal.cast?view=net-6.0#system-runtime-interopservices-memorymarshal-cast-2(system-span((-0)))

using System;
using System.Drawing;
using System.Runtime.InteropServices;

public static class PontFExtension
{
    public static Span<float> AsSingleSpan(this PointF[] points) 
    {
        var span = new Span<PointF>(points);
        
        return MemoryMarshal.Cast<PointF, float>(span);
    }
}

Solution

  • The short answer is no because PointF to Float are two different structs with different fields and layouts (i.e. PointF.X and PointF.Y are fields on the PointF struct). It may work on one OS and .Net version, but may fail on others.

    I've included three methods of achieving your objective safely across OS and .Net versions, ensuring you get X and Y in a Span<float[]> (see Implementation Multidimension) or alternating in Span<float> (see Implementation Single Dimension).

    Endianness

    The implementations I've provided will not cause problems with Endianness between OS - even when types are serialized and written to a network stream in one format (e.g. Big) and read in the other (e.g. Little) by .Net on the other side - as the .Net BCL checks and rotates the bits accordingly.

    I cannot say the same for your implementation using Marshal.Cast as (i) it's a solution that may yield unpredictable results based on OS/.Net version, and (ii) I don't know enough about the intrinsics to opine on Marshal.Cast. If I were you, I'd keep it simple and use one of the methods I've suggested.

    Benchmark

    The Linq is the slowest as to be expected. The AsSpanPtr implementation in faster the 'Ref' (iterator reference) implementation by 4x. The following BenchmarkDotNet results are 10M iterations with the same set.

    | Method |      Mean |     Error |    StdDev | Ratio | RatioSD |
    |------- |----------:|----------:|----------:|------:|--------:|
    |    Ptr |  6.762 ms | 0.0731 ms | 0.0648 ms |  1.00 |    0.00 |
    |    Ref | 27.169 ms | 0.4086 ms | 0.3822 ms |  4.02 |    0.06 |
    |   Linq | 31.896 ms | 0.4622 ms | 0.4098 ms |  4.72 |    0.08 |
    

    Implementation Multidimension

    public static class PontFExtension
    {
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static Span<float[]> AsSpanLinq(this PointF[] points)
           => points.Select(x => new float[] { x.X, x.Y }).ToArray().AsSpan();
    
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static Span<float[]> AsSpanRef(this PointF[] points)
        {
            Span<float[]> floats = new float[points.Length][];
            for (int i = 0; i < points.Length; i++)
            {
                PointF point = points[i];
                floats[i] = new[] { point.X, point.Y };
            }
            return floats;
        }
    
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public unsafe static Span<float[]> AsSpanPtr(this PointF[] points)
        {
            Span<float[]> floats = new float[points.Length][];
            fixed (PointF* pinned = points)
            {
                for (int i = 0; i < points.Length; i++)
                {
                    PointF* pnt = &pinned[i];
                    floats[i] = new[] { (*pnt).X, (*pnt).Y };
                }
            }
            return floats;
        }
    }
    

    Implementation - Single Dimension (X and Y alternating per row)

    public static unsafe class PontFExtensionSingleDimension
    {
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static Span<float> AsSpanLinq(this PointF[] points)
            =>  points.SelectMany(x => new[] { x.X, x.Y}).ToArray();
    
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static Span<float> AsSpanRef(this PointF[] points)
        {
            var len = points.Length;
            Span<float> floats = new float[len * 2];
            for (int i = 0; i < len; i++)
            {
                var pnt = points[i];
                floats[i*2] = pnt.X; floats[i*2+1] = pnt.Y;
            }
            return floats;
        }
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public unsafe static Span<float> AsSpanPtr(this PointF[] points)
        {
            var len = points.Length;
            Span<float> floats = new float[len * 2];
            fixed (PointF* pinned = points)
            {
                for (int i = 0; i < len; i++)
                {
                    PointF* pnt = &pinned[i];
                    floats[i*2] = (*pnt).X; floats[i*2+1] = (*pnt).Y ;
                }
            }
            return floats;
        }
        
    }