.NET值类型在内存中的布局

14
我有以下.NET值类型:
[StructLayout(LayoutKind.Sequential)]
public struct Date
{
    public UInt16 V;
}

[StructLayout(LayoutKind.Sequential)]
public struct StringPair
{
    public String A;
    public String B;
    public String C;
    public Date D;
    public double V;
}

我有一段代码,将指向值类型的指针与通过调用System.Runtime.InteropServices.Marshal.OffsetOf发现的偏移量一起传递给非托管代码。 非托管代码正在填充日期和双精度值。

对于StringPair结构报告的偏移量正是我所期望的:0、8、16、24、32

下面是我在测试函数中的代码:

FieldInfo[] fields = typeof(StringPair).GetFields(BindingFlags.Instance|BindingFlags.Public);

for ( int i = 0; i < fields.Length; i++ )
{
    int offset = System.Runtime.InteropServices.Marshal.OffsetOf(typeof(StringPair), fields[i].Name).ToInt32();

    Console.WriteLine(String.Format(" >> field {0} @ offset {1}", fields[i].Name, offset));
}

打印出确切的偏移量。

 >> field A @ offset 0
 >> field B @ offset 8
 >> field C @ offset 16
 >> field D @ offset 24
 >> field V @ offset 32

我有一些测试代码:

foreach (StringPair pair in pairs) { Date d = pair.D; double v = pair.V; ...

在调试器中,它与以下汇编程序相关联:

               Date d = pair.D;
0000035d  lea         rax,[rbp+20h] 
00000361  add         rax,20h 
00000367  mov         ax,word ptr [rax] 
0000036a  mov         word ptr [rbp+000000A8h],ax 
00000371  movzx       eax,word ptr [rbp+000000A8h] 
00000378  mov         word ptr [rbp+48h],ax 

                double v = pair.V;
0000037c  movsd       xmm0,mmword ptr [rbp+38h] 
00000381  movsd       mmword ptr [rbp+50h],xmm0 

它正在加载偏移量为32(0x20)处的D字段和偏移量为24(0x38-0x20)处的V字段。 JIT已经改变了顺序。 Visual Studio调试器也显示了这种倒置顺序。

为什么会这样呢?我一直在翻阅代码,试图找出逻辑错误出在哪里。如果我在结构体中交换D和V的顺序,那么一切都能正常工作,但是这段代码需要能够处理插件架构,其他开发人员定义了结构体,并且不能指望他们记住深奥的布局规则。

3个回答

16

如果您需要显式布局... 使用显式布局...

[StructLayout(LayoutKind.Explicit)]
public struct StringPair
{
    [FieldOffset(0)] public String A;
    [FieldOffset(8)] public String B;
    [FieldOffset(16)] public String C;
    [FieldOffset(24)] public Date D;
    [FieldOffset(32)] public double V;
}

2
我敢肯定在过去的几天里我曾经尝试过那个..但现在它似乎可以工作了。然而,关键是顺序应该能够工作,框架正在报告不匹配实际使用的偏移量。我真的不希望这个插件架构的用户必须指定一个属性并自己计算才能使其工作。 - Rob Walker

14

从Marshal类获取的信息仅在类型实际被marshal时相关。除了可能偷窥汇编代码之外,托管结构的内部内存布局是不可通过任何记录手段发现的。

这意味着CLR可以自由地重新组织结构的布局并优化其打包。将D和V字段交换可以使您的结构更小,因为双精度所需的对齐要求。在64位机器上,它可以节省6个字节。

不确定为什么这会成为您的问题,这不应该是一个问题。考虑使用Marshal.StructureToPtr(),以按您想要的方式布置结构。


1
谢谢 - 我错过了Marshall.*方法只适用于已编组指针的事实。出于性能原因,我希望避免额外复制数据,但如果我想支持任意结构体,那看起来是不可避免的。 - Rob Walker
1
+1 是为了澄清 JIT 结构布局是完全独立于 CLR 指定的问题,因为从原则上讲,它不能被托管程序所看到,因此也不会对其产生影响。我注意到 JITer 似乎会将托管字段放在非托管字段之前。 - Glenn Slayden
值类型(实例)在内存中的确切位布局可能与决定是否实现IEquatable<T>(以及推荐的object.Equals和GetHashCode重写)有关。如果不实现IEquatable,则我认为默认生成的代码会对内存中的位进行按位比较。如果布局中存在间隙,则可能会浪费时间比较无关紧要的位(在C++中,您还可能冒着正确性的风险,因为这些位可能未初始化-但我认为它们在C#中总是被初始化的)。了解布局可以指导决策要实现什么。 - Reb.Cabin

1

两件事情:

  • StructLayout(Sequential)不能保证结构体的紧密排列。你可能需要使用Pack=1,否则32位和64位平台可能会有所不同。

  • 字符串是引用而不是指针。如果字符串长度始终固定,你可能想使用固定长度的字符数组:

    public struct MyArray // This code must appear in an unsafe block
    {
        public fixed char pathName[128];
    }
    

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