首页
学习
活动
专区
工具
TVP
发布
社区首页 >问答首页 >为什么结构对齐取决于字段类型是原始类型还是用户定义的类型?

为什么结构对齐取决于字段类型是原始类型还是用户定义的类型?
EN

Stack Overflow用户
提问于 2014-07-15 01:30:46
回答 4查看 7.9K关注 0票数 123

Noda Time v2中,我们将转向纳秒分辨率。这意味着我们不能再使用8字节整数来表示我们感兴趣的整个时间范围。这促使我调查Noda Time的(许多)结构的内存使用情况,这反过来又让我发现了CLR对齐决策中的一个小小的奇怪之处。

首先,我意识到这是一个实现决策,默认行为随时可能改变。我意识到我可以使用[StructLayout][FieldOffset]修改它,但如果可能的话,我宁愿提出一个不需要这样做的解决方案。

我的核心场景是,我有一个包含一个引用类型字段和另外两个值类型字段的struct,其中这些字段是int的简单包装器。我曾希望在64位CLR上将其表示为16个字节(8个用于引用,4个用于其他引用),但由于某种原因,它使用了24个字节。顺便说一句,我是用数组来测量空间的--我知道在不同的情况下布局可能会不同,但这感觉像是一个合理的起点。

下面是一个演示该问题的示例程序:

代码语言:javascript
复制
using System;
using System.Runtime.InteropServices;

#pragma warning disable 0169

struct Int32Wrapper
{
    int x;
}

struct TwoInt32s
{
    int x, y;
}

struct TwoInt32Wrappers
{
    Int32Wrapper x, y;
}

struct RefAndTwoInt32s
{
    string text;
    int x, y;
}

struct RefAndTwoInt32Wrappers
{
    string text;
    Int32Wrapper x, y;
}    

class Test
{
    static void Main()
    {
        Console.WriteLine("Environment: CLR {0} on {1} ({2})",
            Environment.Version,
            Environment.OSVersion,
            Environment.Is64BitProcess ? "64 bit" : "32 bit");
        ShowSize<Int32Wrapper>();
        ShowSize<TwoInt32s>();
        ShowSize<TwoInt32Wrappers>();
        ShowSize<RefAndTwoInt32s>();
        ShowSize<RefAndTwoInt32Wrappers>();
    }

    static void ShowSize<T>()
    {
        long before = GC.GetTotalMemory(true);
        T[] array = new T[100000];
        long after  = GC.GetTotalMemory(true);        
        Console.WriteLine("{0}: {1}", typeof(T),
                          (after - before) / array.Length);
    }
}

以及我笔记本电脑上的编译和输出:

代码语言:javascript
复制
c:\Users\Jon\Test>csc /debug- /o+ ShowMemory.cs
Microsoft (R) Visual C# Compiler version 12.0.30501.0
for C# 5
Copyright (C) Microsoft Corporation. All rights reserved.


c:\Users\Jon\Test>ShowMemory.exe
Environment: CLR 4.0.30319.34014 on Microsoft Windows NT 6.2.9200.0 (64 bit)
Int32Wrapper: 4
TwoInt32s: 8
TwoInt32Wrappers: 8
RefAndTwoInt32s: 16
RefAndTwoInt32Wrappers: 24

所以:

  • 如果你没有一个引用类型字段,CLR很乐意把Int32Wrapper字段打包在一起(TwoInt32Wrappers的大小是8)
  • 即使有一个引用类型字段,CLR也很乐意把int字段打包在一起(RefAndTwoInt32s有一个引用类型字段的大小)这两个字段,每个Int32Wrapper字段看起来都被填充/对齐到8个字节。(RefAndTwoInt32Wrappers的大小为24。)在调试器(但仍是发布版本)中运行相同代码的
  • 显示大小为12。

其他一些实验也产生了类似的结果:

  • 将引用类型字段放在值类型字段之后对
  • 使用object而不是string没有帮助(我期望它是“任何引用类型”)
  • 使用另一个结构作为引用的“包装器”对

<代码>H132使用泛型结构作为引用的包装器没有帮助<代码>H233<代码>H134如果我继续添加字段(为简单起见),<代码>D35字段仍然是4个字节,和Int32Wrapper字段的大小为8个字节,

  • [StructLayout(LayoutKind.Sequential, Pack = 4)]添加到可见的每个结构中不会更改结果

有没有人对此有任何解释(最好是参考文档),或者我如何才能向CLR提示我希望在不指定常量字段偏移量的情况下打包字段?

EN

回答 4

Stack Overflow用户

发布于 2014-07-15 03:51:03

我想这是个bug。您正在看到自动布局的副作用,它喜欢将非平凡的字段与64位模式下8字节的倍数的地址对齐。即使在显式应用[StructLayout(LayoutKind.Sequential)]属性时也会发生这种情况。这是不应该发生的。

您可以通过将struct成员设置为公共并添加测试代码来查看它,如下所示:

代码语言:javascript
复制
    var test = new RefAndTwoInt32Wrappers();
    test.text = "adsf";
    test.x.x = 0x11111111;
    test.y.x = 0x22222222;
    Console.ReadLine();      // <=== Breakpoint here

当断点命中时,使用Debug +Window+ Memory + Memory 1.切换为4字节整数,并在地址字段中放入&test

代码语言:javascript
复制
 0x000000E928B5DE98  0ed750e0 000000e9 11111111 00000000 22222222 00000000 

0xe90ed750e0是我的机器(不是你的)上的字符串指针。您可以很容易地看到Int32Wrappers,额外的4个字节的填充将大小转换为24个字节。返回到结构,并将字符串放在最后。重复以上步骤,您将看到字符串指针仍然位于第一位。违反LayoutKind.Sequential,你得到了LayoutKind.Auto

这将很难说服微软来解决这个问题,它已经以这种方式工作了太长时间,所以任何变化都会破坏一些东西。CLR只尝试将[StructLayout]作为结构的托管版本,并使其可进行blittable,因此它通常会很快放弃。这对于任何包含DateTime的结构来说都是臭名昭著的。只有在封送处理结构时,才能获得真正的LayoutKind保证。正如Marshal.SizeOf()将告诉您的那样,编组版本肯定是16字节。

使用LayoutKind.Explicit可以修复它,而不是您想要听到的。

票数 87
EN

Stack Overflow用户

发布于 2014-07-15 13:28:41

EDIT2

代码语言:javascript
复制
struct RefAndTwoInt32Wrappers
{
    public int x;
    public string s;
}

这段代码将是8字节对齐的,因此结构将有16个字节。相比之下,这一点:

代码语言:javascript
复制
struct RefAndTwoInt32Wrappers
{
    public int x,y;
    public string s;
}

将是4字节对齐的,因此此结构也将具有16个字节。所以这里的基本原理是,CLR中的结构对齐是由最多对齐的字段的数量决定的,类显然不能做到这一点,所以它们将保持8字节对齐。

现在,如果我们将所有这些结合在一起并创建struct:

代码语言:javascript
复制
struct RefAndTwoInt32Wrappers
{
    public int x,y;
    public Int32Wrapper z;
    public string s;
}

它有24个字节,{x,y}每个有4个字节,{z,s}有8个字节。一旦我们在struct中引入了ref类型,CLR将始终对齐我们的自定义结构以匹配类对齐。

代码语言:javascript
复制
struct RefAndTwoInt32Wrappers
{
    public Int32Wrapper z;
    public long l;
    public int x,y;  
}

这段代码将有24个字节,因为Int32Wrapper将与long一样对齐。因此,自定义结构包装器将始终对齐到结构中最高/对齐最好的字段,或者对齐到它自己的内部最重要的字段。因此,在ref字符串是8字节对齐的情况下,结构包装器将与之对齐。

结构中的自定义结构字段将始终与结构中对齐程度最高的实例字段对齐。现在,如果我不确定这是不是一个bug,但没有任何证据,我会坚持我的观点,这可能是有意识的决定。

编辑

大小实际上只有在堆上分配时才是准确的,但是结构本身有更小的大小(它的字段的确切大小)。进一步分析seam,表明这可能是CLR代码中的错误,但需要证据来支持。

我将检查cli代码,并发布进一步的更新,如果会发现一些有用的东西。

这是.NET内存分配器使用的一种对齐策略。

代码语言:javascript
复制
public static RefAndTwoInt32s[] test = new RefAndTwoInt32s[1];

static void Main()
{
    test[0].text = "a";
    test[0].x = 1;
    test[0].x = 1;

    Console.ReadKey();
}

这段代码是在x64下用.net40编译的,在WinDbg中让我们执行以下操作:

让我们先找到堆上的类型:

代码语言:javascript
复制
    0:004> !dumpheap -type Ref
       Address               MT     Size
0000000003e72c78 000007fe61e8fb58       56    
0000000003e72d08 000007fe039d3b78       40    

Statistics:
              MT    Count    TotalSize Class Name
000007fe039d3b78        1           40 RefAndTwoInt32s[]
000007fe61e8fb58        1           56 System.Reflection.RuntimeAssembly
Total 2 objects

一旦我们有了它,让我们看看这个地址下有什么:

代码语言:javascript
复制
    0:004> !do 0000000003e72d08
Name:        RefAndTwoInt32s[]
MethodTable: 000007fe039d3b78
EEClass:     000007fe039d3ad0
Size:        40(0x28) bytes
Array:       Rank 1, Number of elements 1, Type VALUETYPE
Fields:
None

我们看到这是一个ValueType,它就是我们创建的那个。由于这是一个数组,我们需要获取数组中单个元素的ValueType定义:

代码语言:javascript
复制
    0:004> !dumparray -details 0000000003e72d08
Name:        RefAndTwoInt32s[]
MethodTable: 000007fe039d3b78
EEClass:     000007fe039d3ad0
Size:        40(0x28) bytes
Array:       Rank 1, Number of elements 1, Type VALUETYPE
Element Methodtable: 000007fe039d3a58
[0] 0000000003e72d18
    Name:        RefAndTwoInt32s
    MethodTable: 000007fe039d3a58
    EEClass:     000007fe03ae2338
    Size:        32(0x20) bytes
    File:        C:\ConsoleApplication8\bin\Release\ConsoleApplication8.exe
    Fields:
                      MT    Field   Offset                 Type VT     Attr            Value Name
        000007fe61e8c358  4000006        0            System.String      0     instance     0000000003e72d30     text
        000007fe61e8f108  4000007        8             System.Int32      1     instance                    1     x
        000007fe61e8f108  4000008        c             System.Int32      1     instance                    0     y

这个结构实际上是32个字节,因为它的16个字节是为填充保留的,所以实际上每个结构的大小从get go开始至少是16个字节。

如果你添加16个字节的int和一个字符串ref到: 0000000003e72d18 +8 bytes EE/padding,你将结束于0000000003e72d30,这是字符串引用的起点,因为所有引用都是从它们的第一个实际数据字段填充8个字节,这就构成了这个结构的32个字节。

让我们看看字符串是否真的是以这种方式填充的:

代码语言:javascript
复制
0:004> !do 0000000003e72d30    
Name:        System.String
MethodTable: 000007fe61e8c358
EEClass:     000007fe617f3720
Size:        28(0x1c) bytes
File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
String:      a
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
000007fe61e8f108  40000aa        8         System.Int32  1 instance                1 m_stringLength
000007fe61e8d640  40000ab        c          System.Char  1 instance               61 m_firstChar
000007fe61e8c358  40000ac       18        System.String  0   shared           static Empty
                                 >> Domain:Value  0000000001577e90:NotInit  <<

现在让我们以同样的方式分析上面的程序:

代码语言:javascript
复制
public static RefAndTwoInt32Wrappers[] test = new RefAndTwoInt32Wrappers[1];

static void Main()
{
    test[0].text = "a";
    test[0].x.x = 1;
    test[0].y.x = 1;

    Console.ReadKey();
}

0:004> !dumpheap -type Ref
     Address               MT     Size
0000000003c22c78 000007fe61e8fb58       56    
0000000003c22d08 000007fe039d3c00       48    

Statistics:
              MT    Count    TotalSize Class Name
000007fe039d3c00        1           48 RefAndTwoInt32Wrappers[]
000007fe61e8fb58        1           56 System.Reflection.RuntimeAssembly
Total 2 objects

我们的结构现在是48个字节。

代码语言:javascript
复制
0:004> !dumparray -details 0000000003c22d08
Name:        RefAndTwoInt32Wrappers[]
MethodTable: 000007fe039d3c00
EEClass:     000007fe039d3b58
Size:        48(0x30) bytes
Array:       Rank 1, Number of elements 1, Type VALUETYPE
Element Methodtable: 000007fe039d3ae0
[0] 0000000003c22d18
    Name:        RefAndTwoInt32Wrappers
    MethodTable: 000007fe039d3ae0
    EEClass:     000007fe03ae2338
    Size:        40(0x28) bytes
    File:        C:\ConsoleApplication8\bin\Release\ConsoleApplication8.exe
    Fields:
                      MT    Field   Offset                 Type VT     Attr            Value Name
        000007fe61e8c358  4000009        0            System.String      0     instance     0000000003c22d38     text
        000007fe039d3a20  400000a        8             Int32Wrapper      1     instance     0000000003c22d20     x
        000007fe039d3a20  400000b       10             Int32Wrapper      1     instance     0000000003c22d28     y

这里的情况是一样的,如果我们加上0000000003c22d18 +8字节的字符串ref,我们将在第一个Int包装器的开始处结束,其中的值实际上指向我们所在的地址。

现在我们可以看到每个值都是一个对象引用,让我们通过窥视0000000003c22d20来确认这一点。

代码语言:javascript
复制
0:004> !do 0000000003c22d20
<Note: this object has an invalid CLASS field>
Invalid object

实际上,这是正确的,因为它是一个struct,这个地址不能告诉我们这是一个obj还是vt。

代码语言:javascript
复制
0:004> !dumpvc 000007fe039d3a20   0000000003c22d20    
Name:        Int32Wrapper
MethodTable: 000007fe039d3a20
EEClass:     000007fe03ae23c8
Size:        24(0x18) bytes
File:        C:\ConsoleApplication8\bin\Release\ConsoleApplication8.exe
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
000007fe61e8f108  4000001        0         System.Int32  1 instance                1 x

因此,实际上,这更像是一个Union类型,这次将获得8字节对齐(所有填充都将与父结构对齐)。如果不是,那么我们最终会得到20个字节,这并不是最优的,所以内存分配器永远不会允许这种情况发生。如果你再计算一遍,就会发现这个结构确实有40个字节的大小。

因此,如果您想更保守地使用内存,则不应该将其打包在struct自定义struct类型中,而应使用简单的数组。另一种方法是在堆外分配内存(例如VirtualAllocEx),这样你就可以得到自己的内存块,并以你想要的方式管理它。

这里的最后一个问题是,为什么突然之间我们可能会得到这样的布局。好的,如果你比较struct[]和计数器字段递增的int[]递增的jited代码和性能,第二个会生成一个8字节对齐的地址作为联合,但当jited时,这将转换为更优化的汇编代码(单个LEA多个MOV.)。然而,在这里描述的情况下,性能实际上会更差,所以我认为这与底层的CLR实现是一致的,因为它是一个自定义类型,可以有多个字段,所以放入起始地址而不是值(因为这是不可能的)并在那里进行结构填充可能更容易/更好,从而导致更大的字节大小。

票数 19
EN

Stack Overflow用户

发布于 2014-07-15 07:20:14

总结看@Hans Passant的答案可能在上面。布局顺序不起作用

一些测试:

它肯定只在64位上,并且对象引用“毒害”了结构。32位实现了您所期望的功能:

代码语言:javascript
复制
Environment: CLR 4.0.30319.34209 on Microsoft Windows NT 6.2.9200.0 (32 bit)
ConsoleApplication1.Int32Wrapper: 4
ConsoleApplication1.TwoInt32s: 8
ConsoleApplication1.TwoInt32Wrappers: 8
ConsoleApplication1.ThreeInt32Wrappers: 12
ConsoleApplication1.Ref: 4
ConsoleApplication1.RefAndTwoInt32s: 12
ConsoleApplication1.RefAndTwoInt32Wrappers: 12
ConsoleApplication1.RefAndThreeInt32s: 16
ConsoleApplication1.RefAndThreeInt32Wrappers: 16

一旦添加了对象引用,所有结构都扩展为8字节,而不是其4字节大小。扩展测试:

代码语言:javascript
复制
Environment: CLR 4.0.30319.34209 on Microsoft Windows NT 6.2.9200.0 (64 bit)
ConsoleApplication1.Int32Wrapper: 4
ConsoleApplication1.TwoInt32s: 8
ConsoleApplication1.TwoInt32Wrappers: 8
ConsoleApplication1.ThreeInt32Wrappers: 12
ConsoleApplication1.Ref: 8
ConsoleApplication1.RefAndTwoInt32s: 16
ConsoleApplication1.RefAndTwoInt32sSequential: 16
ConsoleApplication1.RefAndTwoInt32Wrappers: 24
ConsoleApplication1.RefAndThreeInt32s: 24
ConsoleApplication1.RefAndThreeInt32Wrappers: 32
ConsoleApplication1.RefAndFourInt32s: 24
ConsoleApplication1.RefAndFourInt32Wrappers: 40

正如你所看到的,一旦添加了引用,每个Int32Wrapper就变成了8个字节,所以不是简单的对齐。我缩小了数组分配,以防它是以不同方式对齐的LoH分配。

票数 9
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/24742325

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档