【发布时间】:2014-09-04 17:04:25
【问题描述】:
在Noda Time v2 中,我们正在转向纳秒级分辨率。这意味着我们不能再使用 8 字节整数来表示我们感兴趣的整个时间范围。这促使我研究了 Noda Time 的(许多)结构的内存使用情况,这反过来又引导了我以发现 CLR 对齐决策中的一点点奇怪之处。
首先,我意识到这是一个实现决策,并且默认行为可能随时改变。我意识到我可以使用[StructLayout] 和[FieldOffset] 对其进行修改,但如果可能的话,我宁愿想出一个不需要这样做的解决方案。
我的核心场景是我有一个struct,其中包含一个引用类型字段和两个其他值类型字段,其中这些字段是int 的简单包装器。我曾希望在 64 位 CLR 上将其表示为 16 个字节(8 个用于引用,4 个用于其他每个字节),但由于某种原因,它使用了 24 个字节。顺便说一句,我正在使用数组来测量空间 - 我知道布局在不同情况下可能会有所不同,但这感觉是一个合理的起点。
这是一个演示该问题的示例程序:
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);
}
}
还有我笔记本电脑上的编译和输出:
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的大小为 16) - 将两者结合起来,每个
Int32Wrapper字段似乎都填充/对齐为8 个字节。 (RefAndTwoInt32Wrappers的大小为 24。) - 在调试器中运行相同的代码(但仍然是发布版本)显示大小为 12。
其他一些实验也产生了类似的结果:
- 将引用类型字段放在值类型字段之后没有帮助
- 使用
object而不是string没有帮助(我希望它是“任何引用类型”) - 使用另一个结构作为引用的“包装器”并没有帮助
- 使用通用结构作为引用的包装器没有帮助
- 如果我继续添加字段(为简单起见,成对添加),
int字段仍占 4 个字节,Int32Wrapper字段占 8 个字节 - 将
[StructLayout(LayoutKind.Sequential, Pack = 4)]添加到每个可见的结构不会改变结果
有没有人对此有任何解释(最好有参考文档)或建议我如何向 CLR 提示我希望在不指定常量字段的情况下打包字段偏移量?
【问题讨论】:
-
您实际上并没有使用
Ref<T>,而是使用string,并不是说它应该有所作为。 -
如果你把两个创建一个带有两个
TwoInt32Wrappers,或者一个Int64和一个TwoInt32Wrappers的结构会发生什么?如果你创建一个通用的Pair<T1,T2> {public T1 f1; public T2 f2;}然后创建Pair<string,Pair<int,int>>和Pair<string,Pair<Int32Wrapper,Int32Wrapper>>怎么样?哪些组合会强制 JITter 填充内容? -
@supercat:您最好自己复制代码并进行试验 - 但
Pair<string, TwoInt32Wrappers>确实 只提供 16 个字节,这样就可以解决问题。令人着迷。 -
@SLaks:有时当结构传递给本机代码时,运行时会将所有数据复制到具有不同布局的结构中。
Marshal.SizeOf将返回将传递给本机代码的结构的大小,这与 .NET 代码中结构的大小没有任何关系。 -
有趣的观察:Mono 给出了正确的结果。环境: Unix 3.13.0.24(64 位)上的 CLR 4.0.30319.17020 Int32Wrapper:4 TwoInt32s:8 TwoInt32Wrappers:8 RefAndTwoInt32s:16 RefAndTwoInt32Wrappers:16
标签: c# .net struct clr memory-alignment