4

I am trying to copy an array of Struct1 into an array of Struct2 (same binary representation) the fastest way possible. I have defined an union to convert between Struct1[] and Struct2[] but when I call Array.Copy I get an exception saying the array is the wrong type. How can I circumvent that? Buffer.BlockCopy only accepts primitive types. Here is the code:

[StructLayout(LayoutKind.Explicit)]
public struct Struct12Converter
{
    [FieldOffset(0)]
    public Struct1[] S1Array;
    [FieldOffset(0)]
    public Struct2[] S2Array;
}

public void ConversionTest()
{
    var s1Array = new{new Struct1()}
    var converter = new Struct12Converter{S1Array = s1Array};
    var s2Array = new Struct2[1];
    Array.Copy(converter.S2Array,0,s2Array,0,1) //throws here
    //if you check with the debugger, it says converter.S2Array is a Struct1[], 
    //although the compiler lets you use it like a Struct2[]
    //this has me baffled as well.
}

To give more details: I wanted to experiment to see if working with a mutable struct and changing the value of its fields had different performance characteristics compared to working all the time using the same immutable struct. I think it should be similar but I thought it was worth a measurement. The underlying application would be a low latency socket library where I currently use ArraySegment<byte>-based socket APIs. It so happens that in the SocketAsyncEventArgs api, setting the BufferList property triggers an array copy which is where my 'experiment' fails (i have an array of MutableArraySegment which I cannot convert to ArraySegment[] through the same method as before, thus making my comparison pointless).

Mo Patel
  • 2,321
  • 4
  • 22
  • 37
JJ15k
  • 520
  • 6
  • 12
  • 4
    Could you not create a `Struct3` which is actually the union of `Struct1` and `Struct2`? Then you could create a `Struct3[]` to start with, assign all the `Struct1` values and then read out the `Struct2` values. – Jon Skeet Jul 06 '13 at 13:08
  • 1
    And if you could give more context, that would help too. The major problem is that an array object knows its specific type. A `Struct1[]` *isn't* a `Struct2[]` or vice versa. It's possible that you could sort things out using pointers... again, the more we know, the more we can help. – Jon Skeet Jul 06 '13 at 13:15
  • I could use a Struct3 but I wanted to copy the array without iterating over it (low latency application), and I have measured that the Array.Copy method is faster than a simple iteration/assignation loop (for arrays of a certain size) – JJ15k Jul 06 '13 at 13:47
  • PS: would be glad to get some pointers on how to do it with pointers (pun intended), I am having trouble because i cannot get a pointer to a managed type. However I very seldom use unsafe code and may be overlooking a simple solution. – JJ15k Jul 06 '13 at 14:49

4 Answers4

0

If the Structs are exactly the same, you can accomplish this by using Marshal.PtrToStructure method.

You need to obtain a pointer to your struct, and then you can "deserialize" it back into another struct (which should have the EXACT same layout).

You can see an example here.

Hope this helps, Ofir.

Ofir Makmal
  • 501
  • 2
  • 3
0

Are you aware that you have broken the type-system by unsafely treating a Struct1[] as a Struct2[]? This puts the CLR into an undefined state. It can assume that variables of type Struct1[] indeed point to an instance of Struct1[]. You can see pretty much any weird behavior now. (This is not a security issue. This code is not verifiable and required full trust.)

In other words, you have not converted the array contents but obtained a converted object reference.

Copying an array of blittable objects is usually done in the fastest way using memcpy. A manual copy-loop is equivalent to that but I would not trust the JIT to optimize it into a memcpy. The JIT only does the basic optimizations as of the current version.

usr
  • 168,620
  • 35
  • 240
  • 369
0

This code is deliberately unsafe (since what you want to do is unsafe, AFAIK the CLR/JIT can re-order structs for performance reasons)

Also note that the signature for MemCpy can change depending on framework version (it is internal after all)

For performance reasons you should cache that delegate properly

Idea from this question here

    unsafe delegate void MemCpyImpl(byte* src, byte* dest, int len);

    static MemCpyImpl memcpyimpl;

    public unsafe static void Copy(void* src, void* dst, int count)
    {
        byte* source = (byte*)src;
        byte* dest = (byte*)dst;
        memcpyimpl(source, dest, count);
    }

Then forcing your arrays to be byte arrays (actually void* but never mind the detail)

    public static void ConversionTest()
    {
        var bufferType = typeof(Buffer);

        unsafe
        {
            var paramList = new Type[3] { typeof(byte*), typeof(byte*), typeof(int) };
            var memcpyimplMethod = bufferType.GetMethod("Memcpy", BindingFlags.Static | BindingFlags.NonPublic, null, paramList, null);

            memcpyimpl = (MemCpyImpl)Delegate.CreateDelegate(typeof(MemCpyImpl), memcpyimplMethod);
        }

        Struct1[] s1Array = { new Struct1() { value = 123456789 } };
        var converter = new Struct12Converter { S1Array = s1Array };
        var s2Array = new Struct2[1];
        unsafe
        {
            fixed (void* bad = s2Array)
            {
                fixed (void* idea = converter.S2Array)
                {
                    Copy(bad, idea, 4);
                }
            }
        }
    }
Community
  • 1
  • 1
James
  • 9,774
  • 5
  • 34
  • 58
  • LayoutKind.Explicit with FieldOffset explicitly determine the managed memory layout as well (for blittable types), so the CLR/JIT can't reorder those struct members. – Govert Jun 06 '14 at 13:58
-1

Warning: This might be a dangerous trick, since it does circumvent the type system and the assembly would not be verifiable. Still - some superficial testing didn't raise any obvious problems, and for your 'experiment' it might be worth a go. Check through the warnings by @usr in the comments, though...

Under your assumptions (if you can tolerate an unverifiable output assembly) you need no Marshal.XXX, Array.Copy or memcpy at all. You can read the values from the union type as either a Struct1 array or a Struct2 array. My guess, though I have no evidence to back it up, is that the runtime and GC won't notice the discrepancy between the array type and how you're using the elements.

Here's a standalone example that will run in LinqPad. The default packing means you don't actually need the LayoutKind and FieldOffset annotations in Struct1 and Struct2 (though of course you do in the union type Struct12Converter), but it helps to shows this explicitly.

[StructLayout(LayoutKind.Explicit)]
public struct Struct1
{
    [FieldOffset(0)]
    public int Int1;
    [FieldOffset(4)]
    public int Int2;
}

[StructLayout(LayoutKind.Explicit)]
public struct Struct2
{
    [FieldOffset(0)]
    public long Long;
}

[StructLayout(LayoutKind.Explicit)]
public struct Struct12Converter
{
    [FieldOffset(0)]
    public Struct1[] S1Array;
    [FieldOffset(0)]
    public Struct2[] S2Array;
}


public void ConversionTest()
{
    var int1 = 987;
    var int2 = 456;
    var int3 = 123456;
    var int4 = 789123;

    var s1Array = new[] 
    { 
        new Struct1 {Int1 = int1, Int2 = int2},
        new Struct1 {Int1 = int3, Int2 = int4},
    };

    // Write as Struct1s
    var converter = new Struct12Converter { S1Array = s1Array };

    // Read as Struct2s
    var s2Array = converter.S2Array;

    // Check: Int2 is the high part, so that must shift up
    var check0 = ((long)int2 << 32) + int1;
    Debug.Assert(check0 == s2Array[0].Long);
    // And check the second element
    var check1 = ((long)int4 << 32) + int3;
    Debug.Assert(check1 == s2Array[1].Long);

    // Using LinqPad Dump:
    check0.Dump();
    s2Array[0].Dump();

    check1.Dump();
    s2Array[1].Dump();

}

void Main()
{
    ConversionTest();
}
Govert
  • 16,387
  • 4
  • 60
  • 70
  • I do not agree that miscasting object references is safe (implied by you saying it is a standard trick). Unioning *single instances* of structs is safe, but the CLR can behave erratically if you break the type system. An object ref is not just a pointer like in C. It is trusted by the CLR. – usr Jun 06 '14 at 12:32
  • Could you make an example of how this might misbehave, or how the type system danger might be exhibited? – Govert Jun 06 '14 at 12:40
  • Force-cast a S1 to an S2 and call GetType. The JIT can hard-code the return value because it is statically known. Except, that this now returns the wrong result. This is the most harmless example I could think of. What about GC crashes?! The GC gets an unexpected object type after all. Are you confident that *nothing* in the CLR relies on the static types to be accurate? A *lot* relies on that. (Update: I now downvotes because I really think this is a bad idea.) – usr Jun 06 '14 at 12:44
  • How does the GC care? The arrays contain the same bits. – Govert Jun 06 '14 at 12:58
  • You're assuming stuff about CLR internals that you have no knowledge over. (Me neither.) As you seem to require a concrete example: The GC could contain `assert(ObjectHeaderType == StaticType)` and there's your crash. – usr Jun 06 '14 at 13:11
  • Fair enough - one might need to delve into the IL to understand whether the type inconsistency is exposed to the runtime. If this type issue were dangerous, would it not indicate a bug in the JIT verifier? As you point out, we have a case where theArray.GetType() says Struct1[] but theArray[0].GetType() says Struct2. – Govert Jun 06 '14 at 13:34
  • This is unverifiable and will reliably be rejected if the skip verification permission is not present (in practice: if not under full trust). Even without the unsafe compiler option the compiler can emit unverifiable code.; Under full trust the JIT allows all kinds of malformed IL programs. You don't even have to use a union. You can just conv.i8 a reference to an integer AFAIK. The C++ CLI compiler regularly uses managed pointers. – usr Jun 06 '14 at 13:36
  • This is actually exactly the type of trick I was looking for. Without any memory copy, and yes, I am willing to "cheat" a bit as this is a performance optimization. Sorry I am a bit late to the party I had not enabled notifications – JJ15k Sep 09 '14 at 16:59