C#并集和数组副本

本文关键字:数组 副本 | 更新日期: 2023-09-27 18:21:30

我正试图以最快的方式将Struct1的数组复制到Struct2的数组(相同的二进制表示)中。我已经定义了一个在Struct1[]Struct2[]之间转换的并集,但当我调用Array.Copy时,我得到一个异常,说数组是错误的类型。我该如何规避呢?Buffer.BlockCopy只接受基元类型。这是代码:

[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.
}

要提供更多详细信息,请执行以下操作:我想尝试一下,与一直使用同一个不可变结构相比,使用可变结构并更改其字段的值是否具有不同的性能特征。我认为它应该是相似的,但我认为它值得衡量。底层应用程序将是一个低延迟套接字库,我目前使用基于ArraySegment<byte>的套接字API。碰巧在SocketAsyncEventArgs api中,设置BufferList属性会触发一个数组副本,这就是我的"实验"失败的地方(我有一个MutableArraySegment数组,我无法通过与以前相同的方法将其转换为ArraySegment[],因此我的比较毫无意义)。

C#并集和数组副本

如果结构完全相同,则可以通过使用Marshal.PtrToStructure方法来实现。

您需要获得一个指向结构的指针,然后可以将其"反序列化"回另一个结构(该结构应该具有完全相同的布局)。

你可以在这里看到一个例子。

希望这能有所帮助,Ofir。

您是否意识到您不安全地将Struct1[]视为Struct2[],从而破坏了类型系统?这会使CLR处于未定义状态。可以假设Struct1[]类型的变量确实指向Struct1[]的实例。你现在几乎可以看到任何奇怪的行为。(这不是安全问题。此代码不可验证,需要完全信任。)

换句话说,您没有转换数组内容,而是获得了转换后的对象引用。

通常使用memcpy以最快的方式复制一个可blitable对象数组。手动复制循环相当于此,但我不相信JIT会将其优化为memcpy。JIT只执行当前版本的基本优化。

此代码故意不安全(由于您想要做的是不安全的,AFAIK CLR/JIT可以出于性能原因重新排序结构)

还要注意,MemCpy的签名可能会根据框架版本而更改(毕竟是内部的)

出于性能原因,您应该正确缓存该委托

这个问题的想法在这里

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

然后强迫你的数组是字节数组(实际上是空的,但不要介意细节)

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

警告: 这可能是一个危险的技巧,因为它确实绕过了类型系统,并且程序集是不可验证的。尽管如此,一些肤浅的测试并没有引起任何明显的问题,对于你的"实验"来说,这可能值得一试。不过,请查看@usr在评论中的警告

在您的假设下(如果您可以容忍不可验证的输出程序集),您根本不需要Marshal.XXXArray.Copymemcpy。可以将联合类型中的值读取为Struct1数组或Struct2数组。我的猜测是,尽管我没有证据支持它,但运行时和GC不会注意到数组类型和使用元素的方式之间的差异。

下面是一个将在LinqPad中运行的独立示例。默认的打包意味着您实际上不需要Struct1和Struct2中的LayoutKindFieldOffset注释(当然您在联合类型Struct12Converter中需要),但显式地显示这一点会有所帮助。

[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();
}