C#联合和数组复制

4

我正在尝试将一个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[],因此使我的比较毫无意义)。

4
你能否创建一个Struct3,实际上是由Struct1Struct2组成的联合体?然后你可以创建一个Struct3[],首先分配所有Struct1的值,然后读取出Struct2的值。 - Jon Skeet
1
如果您能提供更多上下文,那将会更有帮助。主要问题在于数组对象知道它的特定类型。Struct1[] 不是 Struct2[],反之亦然。使用指针可能可以解决问题……再次强调,我们了解得越多,就能提供越多帮助。 - Jon Skeet
我可以使用Struct3,但我想复制数组而不需要迭代它(低延迟应用程序),并且我已经测量出Array.Copy方法比简单的迭代/赋值循环更快(对于某些大小的数组)。 - JJ15k
PS:如果能得到一些指针的指导(双关语),我会很高兴。我遇到了麻烦,因为我无法获取托管类型的指针。然而,我很少使用不安全代码,可能忽略了一个简单的解决方案。 - JJ15k
4个回答

0

这段代码是故意不安全的(因为你想做的事情是不安全的,据我所知,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);
    }

然后将您的数组强制转换为字节数组(实际上是void*,但不要在意细节)

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

LayoutKind.Explicit与FieldOffset一起明确定义托管内存布局(对于可平坦化类型),因此CLR/JIT无法重新排序这些结构成员。 - Govert

0

如果结构体完全相同,您可以使用Marshal.PtrToStructure方法来实现这一点。

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

您可以在此处看到示例。

希望有所帮助, Ofir。


0

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

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

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


我的目标是避免任何内存复制。 - JJ15k

-1

警告:这可能是一种危险的技巧,因为它绕过了类型系统,程序集将无法验证。尽管如此,一些表面测试并没有引起任何明显的问题,对于您的“实验”,这可能值得一试。不过,请仔细查看评论中@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();
}

我不同意错误地使用对象引用是安全的(你说这是一种标准技巧,这暗示了这是安全的)。合并结构体的单个实例是安全的,但如果您破坏类型系统,CLR可能会表现出异常行为。对象引用不仅仅像C语言中的指针那样简单,它是由CLR信任的。 - usr
你能否举个例子说明这个可能会出现问题,或者类型系统的危险可能如何表现? - Govert
将S1强制转换为S2并调用GetType。JIT可以硬编码返回值,因为它是静态已知的。但是,现在返回了错误的结果。这是我能想到的最无害的例子。那么GC崩溃怎么办?毕竟GC得到了一个意外的对象类型。您是否有信心CLR中的任何内容都依赖于静态类型的准确性?很多东西都依赖于它。(更新:我现在会被downvotes,因为我真的认为这是一个坏主意。) - usr
如果没有跳过验证权限(实际上是:如果不在完全信任下),这是不可验证的,并且将可靠地被拒绝。即使没有使用不安全的编译器选项,编译器也可以发出不可验证的代码;在完全信任下,JIT允许各种格式错误的IL程序。您甚至不必使用联合。您只需将引用转换为整数AFAIK。C++ CLI编译器经常使用托管指针。 - usr
这实际上正是我正在寻找的技巧。没有任何内存复制,是的,我愿意“作弊”一点,因为这是性能优化。抱歉我来晚了,我没有启用通知。 - JJ15k
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接