0

I have an instance of ReadOnlyMemory<byte> over a byte[] that I need to convert to ArraySegment<byte>. I'm doing this as part of a major code refactor, and I'm having to adapt an existing interface for code that can't be changed at the moment. This conversion happens for every packet captured promiscuously on a very high-traffic network, so performance is important.

My current approach is using MemoryMarshal.TryGetArray<byte>(). This works, and it's fast. To be honest, it's probably fast enough that my question amounts to more of a curiosity than a problem in need of a solution. Still...

In looking at the source code for MemoryMarshal.TryGetArray<byte>(), I noticed that it calls ReadOnlyMemory<T>.GetObjectStartLength(). This is an internal method, so it's not available for direct use. But, it returns everything I'd need to create the ArraySegment<byte> instance, and there's always reflection, right?

So for due diligence and curiosity's sake, I profiled the MemoryMarshal approach against a reflection-based approach using Benchmark.NET. As expected, the MemoryMarshal approach won handily.

That'd be the end of it, except that I stumbled across this article by @JonSkeet, and it made me wonder about the performance of using a created open delegate instance against the MethodInfo for ReadOnlyMemory<T>.GetObjectStartLength().

I implemented the approach in the article, and while it compiles, I get a runtime error due to the two int out parameters (doesn't work with Func<TTarget, TInput1, TInput2, TResult>). Even if I could figure out how to work around that, I wonder if the fact that ReadOnlyMemory<byte> is a value type wouldn't cause yet another a problem since @JonSkeet purposefully limits the solution to classes ("It's feasible, but a bit of a faff").

I think I understand how the solution works, but I cannot figure out how to get it to work for an instance method on a value type that has two out parameters. And it needs to be an open delegate, one that can take a ReadOnlyMemory<byte> as input and return the resulting byte[] and the values for both int out parameters.

Can this even be done? Like I said, this is probably an academic exercise, but I'd really love to know how such a solution would stack up performance-wise.

8
  • If I understand correctly, the "faff" is just creating your own delegate type with the correct signature. Did you try that? Commented Oct 24 at 8:08
  • @Sweeper, the GetObjectStartLength() function operates against private members in the ReadOnlyMemory<T> instance. I can declare a delegate that matches its signature and call Delegate.CreateDelegate() against that delegate's type and the MethodInfo retrieved via reflection. But how do I get it to operate against an arbitrary instance of ReadOnlyMemory<T>? Commented Oct 24 at 8:27
  • I'm confused. You want a ArraySegment<byte>. MemoryMarshal.TryGetArray<byte>() gives you exactly that (if it succeed). So what would your method do differently, and why would it be better? Commented Oct 24 at 8:46
  • @JonasH, it's a question of performance at this point. Commented Oct 24 at 8:52
  • Yes, I get that it is about performance. But why do you think that your method would be faster than the existing one? Does TryGetArray do something that you think hurt performance? Commented Oct 24 at 9:06

2 Answers 2

2

I believe the "faff" that Jon Skeet mentioned is referring to the hassle of creating your own delegate type. Quote:

In particular, you can’t use Func as that doesn’t have any by-reference parameters.

For GetObjectStartLength, you can easily create a delegate type that matches its signature,

public delegate object? MyDelegate<T>(ref ReadOnlyMemory<T> mem, out int start, out int length);

Since this is a value type instance method, the first parameter (representing this) should be ref.

Example usage:

var arr = new byte[] { 1, 2, 3 };
var memory = new ReadOnlyMemory<byte>(arr, 1, 2);
var methodInfo = typeof(ReadOnlyMemory<byte>).GetMethod("GetObjectStartLength", BindingFlags.NonPublic | BindingFlags.Instance);
if (methodInfo is not null)
{
    var openDelegate = methodInfo.CreateDelegate<MyDelegate<byte>>();
    var obj = openDelegate(ref memory, out int start, out int length);
    Console.WriteLine(obj == arr); // True
    Console.WriteLine(start); // 1
    Console.WriteLine(length); // 2
    // new ArraySegment<byte>((byte[])obj!, start, length);
}
Sign up to request clarification or add additional context in comments.

5 Comments

Awesome!!! That works!! I think all I was missing was the ref on the first parameter. I don't remember reading that detail anywhere. Thanks so much!!
@Charlieface I was trying to find a generic version of that on Delegate and didn't find any. So that's where it were!
@MattDavis I tried benchmarking this and it turned out that using an open delegate is ever so slightly faster than MemoryMarshal.TryGetArray. Did you get similar results?
I'm seeing the same thing...open delegate just slightly faster. I'm going to add an answer that includes the benchmarks.
Actually, I wasn't comparing apples to apples. MemoryMarshal.TryGetArray returns the ArraySegment<byte> directly. With the open delegate, I have to create it from the return values. It's close, but MemoryMarshal is actually just a bit faster. And it's the published API, not an internal method.
1

Thanks to @Sweeper for the help.

I've profiled the three options. It turns out that the MemoryMarshal.TryGetArray solution is just slightly faster than the open delegate solution. @JonSkeet was right about making reflection fly, though. Here's the code I used with Benchmark.NET and the results.

[MemoryDiagnoser]
public class Program
{
    public delegate object Delegate<T>(ref ReadOnlyMemory<T> rom, out int start, out int length);

    public static MethodInfo mi = typeof(ReadOnlyMemory<byte>).GetMethod("GetObjectStartLength", BindingFlags.Instance | BindingFlags.NonPublic);
    public static Delegate<byte> method = mi.CreateDelegate<Delegate<byte>>();
    public static ReadOnlyMemory<byte> rom = new byte[] { 0, 1, 2, 3, 4 };
    public static ArraySegment<byte> segment;

    [Benchmark]
    public void InvokeViaMethodInfo()
    {
        var parameters = new object[] { 0, 0 };
        var array = (byte[])mi.Invoke(rom, parameters);
        var index = (int)parameters[0];
        var count = (int)parameters[1];
        segment   = new(array, index, count);
    }

    [Benchmark]
    public void InvokeViaMemoryMarshal()
    {
        if (!MemoryMarshal.TryGetArray(rom, out segment)) { throw new Exception(); }
    }

    [Benchmark]
    public void InvokeViaOpenDelegate()
    {
        var array = (byte[])method(ref rom, out var index, out var count);
        index &= 0x7FFFFFFF; // remove any pinned flag
        segment = new(array, index, count);
    }

    static void Main(string[] args)
    {
        var summary = BenchmarkRunner.Run<Program>();
        Console.ReadLine();
    }
}

enter image description here

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.