Why does LayoutKind.Sequential work differently if a struct contains a DateTime field?
如果结构包含日期时间字段,那么为什么layoutKind.Sequential的工作方式不同?
考虑以下代码(必须在启用"不安全"的情况下编译的控制台应用程序):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | using System; using System.Runtime.InteropServices; namespace ConsoleApplication3 { static class Program { static void Main() { Inner test = new Inner(); unsafe { Console.WriteLine("Address of struct =" + ((int)&test).ToString("X")); Console.WriteLine("Address of First =" + ((int)&test.First).ToString("X")); Console.WriteLine("Address of NotFirst =" + ((int)&test.NotFirst).ToString("X")); } } } [StructLayout(LayoutKind.Sequential)] public struct Inner { public byte First; public double NotFirst; public DateTime WTF; } } |
现在,如果我运行上面的代码,我会得到如下类似的输出:
结构地址=40F2CC第一个地址=40f2d4notfirst地址=40f2cc
注意,first的地址与结构的地址不同;但是notfirst的地址与结构的地址相同。
现在注释掉结构中的"datetime wtf"字段,然后再次运行它。这次,我得到的输出类似于:
结构地址=15f2e0第一个地址=15f2e0notfirst地址=15f2e8
现在"first"与结构具有相同的地址。
我发现这种行为令人惊讶,因为使用了布局类型。顺序。有人能解释一下吗?在使用COM DATETIME类型的C/C++结构互操作时,这种行为是否有任何影响?
[编辑]注意:我已经验证了当您使用marshal.structureToptr()封送结构时,数据是以正确的顺序封送的,"First"字段是第一个。这似乎表明它可以与interop一起工作。神秘的是为什么内部布局会改变——当然,内部布局从未被指定,所以编译器可以做它喜欢的事情。
[edit2]从结构声明中删除了"unsafe"(它是我所做的一些测试的剩余部分)。
[edit3]此问题的原始来源是来自msdn c论坛:
http://social.msdn.microsoft.com/forums/en-us/csharplanguage/thread/fb84bf1d-d9b3-4e91-823e-988257504b30
Why does LayoutKind.Sequential work differently if a struct contains a DateTime field?
这与(令人惊讶的)事实有关,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | static class Program { static unsafe void Main() { Console.WriteLine("64-bit: {0}", Environment.Is64BitProcess); Console.WriteLine("Layout of OneField: {0}", typeof(OneField).StructLayoutAttribute.Value); Console.WriteLine("Layout of Composite: {0}", typeof(Composite).StructLayoutAttribute.Value); Console.WriteLine("Size of Composite: {0}", sizeof(Composite)); var local = default(Composite); Console.WriteLine("L: {0:X}", (long)(&(local.L))); Console.WriteLine("M: {0:X}", (long)(&(local.M))); Console.WriteLine("N: {0:X}", (long)(&(local.N))); } } [StructLayout(LayoutKind.Auto)] // also try removing this attribute struct OneField { public long X; } struct Composite // has layout Sequential { public byte L; public double M; public OneField N; } |
样品输出:
1 2 3 4 5 6 7 | 64-bit: True Layout of OneField: Auto Layout of Composite: Sequential Size of Composite: 24 L: 48F050 M: 48F048 N: 48F058 |
如果我们从
1 2 3 4 5 6 7 | 64-bit: True Layout of OneField: Sequential Layout of Composite: Sequential Size of Composite: 24 L: 48F048 M: 48F050 N: 48F058 |
这些例子是用X64平台编译的(所以24的大小是8的3倍,这不足为奇),但是用X86我们也看到了相同的"无序"指针地址。
所以我想我可以得出结论,
根据另一个线程中HansPassant的评论,当其中一个成员是
更仔细地阅读布局规则的规范。仅当对象在非托管内存中公开时,布局规则才控制布局。这意味着编译器可以随意放置字段,直到实际导出对象为止。让我有点惊讶的是,对于FixedLayout,这甚至是正确的!
关于编译器效率问题,Ian Ringrose是对的,这确实说明了这里选择的最终布局,但这与编译器忽略布局规范的原因无关。
一些人指出日期时间具有自动布局。这是你惊喜的最终来源,但原因有点模糊。auto-layout的文档说"用[auto]layout定义的对象不能暴露在托管代码之外。尝试这样做会生成一个异常。"还要注意,datetime是一个值类型。通过将具有自动布局的值类型合并到您的结构中,您无意中承诺永远不会向非托管代码公开包含结构(因为这样做会公开日期时间,并且会生成异常)。由于布局规则只管理非托管内存中的对象,并且您的对象永远不能暴露于非托管内存中,因此编译器在选择布局时不受约束,可以自由地执行任何操作。在这种情况下,它将恢复到自动布局策略,以实现更好的结构打包和对齐。
那里!不是很明显吗?
顺便说一下,所有这些在静态编译时都是可以识别的。实际上,编译器识别它是为了决定它可以忽略布局指令。在识别出它之后,编译器发出的警告似乎是正确的。实际上你没有做错什么,但是当你写了一些没有效果的东西时,别人告诉你是很有帮助的。
这里建议固定布局的各种注释通常都是很好的建议,但在这种情况下,这不一定有任何效果,因为包括datetime字段完全免除了编译器遵守布局的义务。更糟的是:编译器不需要遵守布局,但可以自由遵守布局。这意味着连续版本的clr在这方面可以自由地表现出不同的行为。
在我看来,布局的处理是CLI中的一个设计缺陷。当用户指定一个布局时,编译器不应该绕着它们跑。最好保持简单,让编译器按照命令进行操作。尤其是在布局方面。"我们都知道,聪明是一个四个字母的词。
回答我自己的问题(按建议):
问:"在使用COM DATETIME类型的C/C++结构互操作时,这种行为有什么影响吗?"
答:没有,因为使用编组时要尊重布局。(我用经验证明了这一点。)
问题"有人能解释吗?".
答:我仍然不确定,但是由于没有定义结构的内部表示,编译器可以做它喜欢做的事情。
您正在检查托管结构中的地址。marshal属性对托管结构中的字段排列没有保证。
它正确封送到本机结构中的原因是,使用封送值设置的属性将数据复制到本机内存中。
因此,管理结构的排列对本机结构的排列没有影响。只有属性会影响本机结构的排列。
如果使用marshal属性设置的字段以与本机数据相同的方式存储在托管数据中,那么marshal.structureToptr中就没有点,您只需通过字节复制数据即可。
几个因素
- 双打如果对齐的话会快很多
- 如果在打击中没有"漏洞",CPU缓存可能工作得更好。
因此,C编译器有一些未经记录的规则,用来尝试获得结构的"最佳"布局,这些规则可以考虑结构的总大小,和/或如果它包含另一个结构等。如果您需要知道结构的布局,那么您应该自己指定它,而不是让编译器决定。
然而,layoutKind.Sequential确实会停止编译器更改字段顺序。
如果你要用C/C++来互操作,我会一直使用StuttDebug。我将使用显式而不是顺序,并使用fieldoffset指定每个位置。此外,添加pack变量。
1 2 3 4 5 6 7 8 9 10 | [StructLayout(LayoutKind.Explicit, Pack=1, CharSet=CharSet.Unicode)] public struct Inner { [FieldOffset(0)] public byte First; [FieldOffset(1)] public double NotFirst; [FieldOffset(9)] public DateTime WTF; } |
听起来,datetime不能以任何方式封送到字符串(bingle marshal datetime)。
包变量在C++代码中尤其重要,它可以编译在具有不同字大小的不同系统上。
我也会忽略使用不安全代码时可以看到的地址。只要编组正确,编译器做什么并不重要。