25

如果结构包含 DateTime 字段,为什么 LayoutKind.Sequential 的工作方式会有所不同?

考虑以下代码(必须在启用“不安全”的情况下编译的控制台应用程序):

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

现在,如果我运行上面的代码,我会得到类似于以下内容的输出:

struct 的地址 = 40F2CC
First 的地址 = 40F2D4 NotFirst
的地址 = 40F2CC

注意 First 的地址与结构体的地址不同;但是,NotFirst地址与结构的地址相同。

现在注释掉结构中的“DateTime WTF”字段,然后再次运行它。这一次,我得到类似这样的输出:

struct 的地址 = 15F2E0
First 的地址 = 15F2E0 NotFirst
的地址 = 15F2E8

现在“First”确实与结构具有相同的地址。

考虑到 LayoutKind.Sequential 的使用,我发现这种行为令人惊讶。任何人都可以提供解释吗?与使用 Com DATETIME 类型的 C/C++ 结构进行互操作时,此行为是否有任何影响?

[编辑] 注意:我已经验证,当您使用 Marshal.StructureToPtr() 编组结构时,数据正确的顺序编组,“First”字段位于第一位。这似乎表明它可以与互操作一起正常工作。奥秘在于为什么内部布局会发生变化——当然,内部布局从来没有被指定,所以编译器可以做它喜欢的事情。

[EDIT2] 从结构声明中删除了“不安全”(它是我正在做的一些测试留下的)。

[EDIT3] 这个问题的原始来源来自 MSDN C# 论坛:

http://social.msdn.microsoft.com/Forums/en-US/csharplanguage/thread/fb84bf1d-d9b3-4e91-823e-988257504b30

4

6 回答 6

20

如果结构包含 DateTime 字段,为什么 LayoutKind.Sequential 的工作方式会有所不同?

它与(令人惊讶的)事实有关,DateTime它本身具有布局“自动”(我自己链接到 SO 问题)。此代码重现了您看到的行为:

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

样本输出:

64 位:真
OneField 的布局:自动
复合布局:顺序
复合尺寸:24
大号:48F050
男:48F048
编号:48F058

如果我们从 中删除属性OneField,事情就会按预期运行。例子:

64 位:真
OneField 的布局:顺序
复合布局:顺序
复合尺寸:24
大号:48F048
男:48F050
编号:48F058

这些示例是使用x64平台编译的(所以大小为 24,三乘以八,不足为奇),但对于 x86,我们也看到相同的“无序”指针地址。

所以我想我可以得出结论,OneFieldDateTime在您的示例中)的布局对包含OneField成员的结构的布局有影响,即使该复合结构本身具有 layout Sequential。我不确定这是否有问题(甚至是必需的)。


根据 Hans Passant 在另一个线程中的评论,当其中一个成员是布局结构时,它不再尝试保持顺序。Auto

于 2014-02-22T21:05:18.847 回答
7

更仔细地阅读布局规则的规范。布局规则仅在对象暴露在非托管内存中时才管理布局。这意味着编译器可以随意放置字段,直到对象被实际导出。令我惊讶的是,FixedLayout 甚至是这样!

Ian Ringrose 关于编译器效率问题是正确的,这确实解释了此处选择的最终布局,但这与编译器忽略您的布局规范的原因无关。

一些人指出 DateTime 具有自动布局。那是你惊喜的最终来源,但原因有点模糊。Auto layout 的文档说“用 [Auto] layout 定义的对象不能暴露在托管代码之外。尝试这样做会产生异常。” 另请注意,DateTime 是一种值类型。通过将具有 Auto 布局的值类型合并到您的结构中,您无意中承诺您永远不会暴露包含结构到非托管代码(因为这样做会暴露 DateTime,并且会产生异常)。由于布局规则仅管理非托管内存中的对象,并且您的对象永远不会暴露给非托管内存,因此编译器在其布局选择上不受限制,可以自由地做任何想做的事情。在这种情况下,它将恢复为自动布局策略,以实现更好的结构打包和对齐。

那里!是不是很明显!

顺便说一下,所有这些在静态编译时都是可以识别的。事实上,编译器识别它是为了决定它可以忽略你的布局指令。认识到这一点后,编译器的警告似乎是有序的。你实际上并没有做错什么,但是当你写了一些没有效果的东西时被告知会很有帮助。

这里推荐固定布局的各种评论通常是很好的建议,但在这种情况下,这不一定有任何效果,因为包含 DateTime 字段会使编译器完全不遵守布局。更糟糕的是:编译器不需要尊重布局,但可以自由地尊重布局。这意味着 CLR 的后续版本可以自由地在这方面表现不同。

在我看来,布局的处理是 CLI 中的一个设计缺陷。当用户指定布局时,编译器不应该绕着他们走。最好让事情保持简单,让编译器按照它的指示去做。尤其是在布局方面。众所周知,“聪明”是一个四个字母的词。

于 2014-05-13T13:08:24.863 回答
3

几个因素

  • 如果它们对齐,双打会快得多
  • 如果撞击中没有“漏洞”,CPU 缓存可能会更好地工作

因此,C# 编译器有一些未记录的规则用于尝试获得“<em>最佳”结构布局,这些规则可能会考虑结构的总大小,和/或它是否包含另一个结构等。 如果您需要知道结构的布局,然后您应该自己指定它,而不是让编译器决定。

但是 LayoutKind.Sequential 确实会阻止编译器更改字段的顺序。

于 2010-11-09T10:53:40.700 回答
3

回答我自己的问题(按照建议):

问题:“与使用 Com DATETIME 类型的 C/C++ 结构进行互操作时,此行为是否有任何影响?”

答:不,因为在使用 Marshalling 时会尊重布局。(我凭经验验证了这一点。)

问题“任何人都可以提供解释吗?”。

答:我仍然不确定这一点,但是由于没有定义结构的内部表示,编译器可以做它喜欢的事情。

于 2010-11-09T14:28:44.313 回答
2

您正在检查托管结构中的地址。Marshal 属性不能保证托管结构中的字段排列。

它正确编组到本机结构的原因是因为使用编组值设置的属性将数据复制到本机内存中。

因此,托管结构的排列对本机结构的排列没有影响。只有属性影响原生结构的排列。

如果使用 marshal 属性设置的字段以与本机数据相同的方式存储在托管数据中,那么 Marshal.StructureToPtr 将毫无意义,您只需将数据按字节复制即可。

于 2011-11-08T20:57:12.630 回答
1

如果您要与 C/C++ 进行互操作,我将始终针对 StructLayout。我会使用 Explicit 而不是 Sequential,并使用 FieldOffset 指定每个位置。此外,添加您的 Pack 变量。

[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)。

Pack 变量在可能在具有不同字长的不同系统上编译的 C++ 代码中尤为重要。

我也会忽略使用不安全代码时可以看到的地址。只要编组正确,编译器做什么并不重要。

于 2011-02-14T21:13:28.150 回答