在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提示我希望在不指定常量字段偏移的情况下打包字段?
TwoInt32Wrappers
,或者一个Int64
和一个会发生TwoInt32Wrappers
什么?如果先创建泛型Pair<T1,T2> {public T1 f1; public T2 f2;}
,然后再创建Pair<string,Pair<int,int>>
and Pair<string,Pair<Int32Wrapper,Int32Wrapper>>
呢?哪些组合迫使JITter垫东西?
Pair<string, TwoInt32Wrappers>
确实只提供16个字节,因此可以解决此问题。迷人。
Marshal.SizeOf
将返回将传递给本机代码的结构的大小,而本机代码与.NET代码中的结构的大小没有任何关系。
Ref<T>
而是在使用,string
而不是应该有所作为。