CLR顺序结构的布局:对齐和大小
本文关键字:对齐 布局 顺序 结构 CLR | 更新日期: 2023-09-27 18:15:35
默认情况下c#中的所有struct
都被视为[StructLayout(LayoutKind.Sequential)]
标记的值类型。因此,我们取一些struct
s并检查struct
s的大小:
using System;
using System.Reflection;
using System.Linq;
using System.Runtime.InteropServices;
class Foo
{
struct E { }
struct S0 { byte a; }
struct S1 { byte a; byte b; }
struct S2 { byte a; byte b; byte c; }
struct S3 { byte a; int b; }
struct S4 { int a; byte b; }
struct S5 { byte a; byte b; int c; }
struct S6 { byte a; int b; byte c; }
struct S7 { int a; byte b; int c; }
struct S8 { byte a; short b; int c; }
struct S9 { short a; byte b; int c; }
struct S10 { long a; byte b; }
struct S11 { byte a; long b; }
struct S12 { byte a; byte b; short c; short d; long e; }
struct S13 { E a; E b; }
struct S14 { E a; E b; int c; }
struct S15 { byte a; byte b; byte c; byte d; byte e; }
struct S16 { S15 b; byte c; }
struct S17 { long a; S15 b; }
struct S18 { long a; S15 b; S15 c; }
struct S19 { long a; S15 b; S15 c; E d; short e; }
struct S20 { long a; S15 b; S15 c; short d; E e; }
static void Main()
{
Console.WriteLine("name: contents => size'n");
foreach (var type in typeof(Foo).GetNestedTypes(BindingFlags.NonPublic))
{
var fields = type.GetFields(BindingFlags.NonPublic | BindingFlags.Instance);
Console.WriteLine("{0}: {2} => {1}", type.Name, Marshal.SizeOf(type),
string.Join("+", fields.Select(_ => Marshal.SizeOf(_.FieldType))));
}
}
}
输出如下(x86/x64相同):
name: contents => size
E: => 1
S0: 1 => 1
S1: 1+1 => 2
S2: 1+1+1 => 3
S3: 1+4 => 8
S4: 4+1 => 8
S5: 1+1+4 => 8
S6: 1+4+1 => 12
S7: 4+1+4 => 12
S8: 1+2+4 => 8
S9: 2+1+4 => 8
S10: 8+1 => 16
S11: 1+8 => 16
S12: 1+1+2+2+8 => 16
S13: 1+1 => 2
S14: 1+1+4 => 8
S15: 1+1+1+1+1 => 5
S16: 5+1 => 6
S17: 8+5 => 16
S18: 8+5+5 => 24
S19: 8+5+5+1+2 => 24
S20: 8+5+5+2+1 => 24
看到这个结果,我无法理解CLR用于顺序结构的布局(字段对齐和总大小)规则集。有人能解释一下这种行为吗?
所有字段根据其类型对齐。原生类型(int
、byte
等)都是按大小对齐的。例如,int
将始终是4字节的倍数,而一个字节可以是任何位置。
如果较小的字段出现在int
之前,则必要时将添加填充以确保int
正确对齐为4字节。这就是为什么S5
(1+1+4 = 8)和S8
(1+2+4 = 8)将有填充,最终大小相同:
[1][1][ ][ ][4] // S5
[1][ ][ 2 ][4] // S8
此外,结构体本身继承了其最对齐字段的对齐方式。对于S5
和S8
, int
是最对齐的字段,所以它们都有4)对齐。对齐是这样继承的,所以当你有一个结构体数组时,所有结构体中的所有字段都将正确对齐。所以,4+2 = 8。
[4][2][ ][ ] // starts at 0
[4][2][ ][ ] // starts at 8
[4][2][ ][ ] // starts at 16
注意4总是按4对齐。如果不从最对齐字段继承,数组中的每个其他元素的int
将对齐6个字节,而不是4个字节:
[4][2] // starts at 0
[4][2] // starts at 6 -- the [4] is not properly aligned!
[4][2] // starts at 12
这将是非常糟糕的,因为并不是所有的体系结构都允许从未对齐的内存地址中读取,即使是那些允许这样做的体系结构也会有(可能相当大的,如果在缓存行或页面边界上)性能损失。
除了基本性能之外,对齐还与并发性有关。c#内存模型保证本地类型的读/写最多4个字节是原子的,而。net有像Interlocked
类这样的原子特性。像这样的原子操作可以归结为CPU指令,这些指令本身需要对齐内存访问才能工作。
正确对齐非常重要!
你会经常看到聪明的本地编码人员在布局他们的结构时牢记这一切,从大到小对所有字段进行排序,以尽量保持填充,从而使结构大小最小。