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