为啥 16 字节是 C# 中 struct 的推荐大小?
Posted
技术标签:
【中文标题】为啥 16 字节是 C# 中 struct 的推荐大小?【英文标题】:Why is 16 byte the recommended size for struct in C#?为什么 16 字节是 C# 中 struct 的推荐大小? 【发布时间】:2011-01-25 08:20:32 【问题描述】:我阅读了 Cwalina 的书(关于 .NET 应用程序的开发和设计的建议)。
他说设计良好的结构必须小于 16 字节(出于性能目的)。
为什么会这样?
而且(更重要的是)如果我在 Windows 7 x64 下在 Core i7 上运行我的 .NET 3.5(即将成为 .NET 4.0)64 位应用程序,我能否拥有更大的结构和相同的效率 (这个限制是基于 CPU / OS 的吗)?
再次强调一下——我需要尽可能高效的结构。我试着一直把它放在堆栈上。该应用程序是高度多线程的,以亚毫秒间隔运行,当前结构的大小为 64 字节。
【问题讨论】:
是什么让您相信将数据保存在堆栈上更有效?堆栈与堆是 .NET 中的一个实现细节,开发人员不应该关心(参见***.com/questions/477101/…) 询问效率而不说您使用的指标有点含糊。这就像要求最高效的车辆 - 无需提及您是想节省通勤费用,还是想运输 30 吨货物。 Divo 原则上你错了。为了使我的应用程序更好,我应该关心什么不在本主题范围内。谢谢 @maxima120:我的意思不是要批评你个人,但在 .NET 中,比堆栈与堆更重要的是值类型语义与引用类型语义。将变量放在堆栈上并不意味着“更有效率”。但是,拥有值类型可能意味着负面影响,因为它们在传递时会被复制。当然,我上面的评论不是对您的主要问题的回答,但仍然是一个相关方面(这就是我没有将其发布为答案的原因)。恕我直言,在 C# 中使用结构的理由很少(例如 P/Invoke,或在处理“值”如 DateTime 时)。 另见***.com/questions/1082311/… 【参考方案1】:您错误地引用了这本书(至少是第 2 版)。 Jeffrey Richter 指出,如果满足以下条件,值类型可以超过 16 个字节:
您不打算将它们传递给其他人 方法或将它们复制到和从 集合类。
另外 Eric Gunnerson 添加(关于 16 字节限制)
使用本指南作为触发点 更多调查。
结构“必须小于 16 字节”是不正确的。这完全取决于使用情况。
如果您正在创建结构并同时使用它并且担心性能,那么请使用分析器比较结构与类,看看哪个最适合您。
【讨论】:
我说——它必须被认为是一个好的设计。请引用整个句子。从你如何扭曲我的话来看,结构必须是 16 个字节或更少......不,它没有!我昨天刚读了这本书。我知道 Richter 所说的,这不适用于我的应用程序,因此我只是忽略了这一点......因为主题不是讨论这本书,而是帮助我提高我的应用程序效率非常感谢!至于甘纳森——这正是我在这里想要做的——进一步调查! @Maxima120。我没有歪曲你的话。你错误地引用了这本书,我纠正了你。这本书确实声明要避免实例大小不低于 16 字节的结构(然后添加警告)。这显然与“设计良好的结构必须小于 16 字节”不同。【参考方案2】:只有你知道你的结构在你的程序中是如何被使用的。但如果不出意外,您可以随时自己测试。例如,如果它经常被传递给其他函数,以下可能会启发您:
class MainClass
static void Main()
Struct64 s1 = new Struct64();
Class64 c1 = new Class64();
DoStuff(s1);
DoStuff(c1);
Stopwatch sw = new Stopwatch();
sw.Start();
for (int i = 0; i < 10000; i++)
s1 = DoStuff(s1);
sw.Stop();
Console.WriteLine("Struct 0", sw.ElapsedTicks);
sw.Reset();
sw.Start();
for (int i = 0; i < 10000; i++)
c1 = DoStuff(c1);
sw.Stop();
Console.WriteLine("Class 0", sw.ElapsedTicks);
sw.Reset();
Console.ReadLine();
与:
public class Class64
public long l1;
public long l2;
public long l3;
public long l4;
public long l5;
public long l6;
public long l7;
public long l8;
public struct Struct64
public long l1;
public long l2;
public long l3;
public long l4;
public long l5;
public long l6;
public long l7;
public long l8;
用有代表性的结构/类尝试这种事情,看看你会得到什么结果。 (在我的机器上,上面的测试,这个类似乎快了 3 倍)
【讨论】:
用一个循环替换基准测试,该循环对结构执行for (int i=0; i<100000; i++) mystruct.l1 += i;
,然后尝试使用for (int i=0; i<100000; i++) mything = new Whatever(mything.l1+i, mything.l2, mything.l3, mything.l4, mything.l5, mything.l6, mything.l7, mything.l8);
对结构和类进行测试,简单的可变结构将赢得一英里,执行时间这与字段的数量无关。然而,“immutable-struct-style”版本倾向于以相当大的优势胜过类版本。添加数十个 long
字段不会改变这一点。【参考方案3】:
JIT 编译器生成特殊情况代码来复制小于特定阈值的结构,并为较大的结构生成稍慢的通用构造。我猜想在写这个建议时,阈值是 16 字节,但在今天的 32 位框架中,它似乎是 24 字节;对于 64 位代码,它可能会更大。
话虽如此,创建任何大小类对象的成本大大高于复制包含相同数据的结构的成本。如果代码创建一个具有 32 字节字段的类对象,然后传递或以其他方式复制对该对象的引用 1,000 次,则复制 1,000 个对象引用而不是复制 1,000 个 32 字节结构所节省的时间可能会超过创建类对象。但是,如果对象实例在引用仅被复制两次后就被放弃,则创建对象的成本可能会大大超过复制 32 字节结构两次的成本。
还请注意,在许多情况下,可以避免通过值传递结构或以其他方式冗余复制它们,如果有人努力这样做的话。例如,将任何大小的结构作为ref
参数传递给方法,只需要传递一个单机器字(4 或8 字节)地址。因为.net 缺少任何类型的const ref
概念,所以只能以这种方式传递可写字段或变量,并且.net 中内置的集合——除了System.Array
——不提供通过ref
访问成员的方法。但是,如果愿意使用数组或自定义集合,即使是巨大的(100 字节或更多)结构也可以非常有效地处理。在许多情况下,使用结构而不是不可变类的性能优势可能会随着封装数据的大小而增加。
【讨论】:
【参考方案4】:更大的结构效率不高,但是……如果你有更多的数据,你就有了。在那里谈论效率毫无意义。
64 字节应该没问题。
主要原因可能是复制操作....如果结构较大,这会使 IIRC 变慢。而且它必须被大量复制。
我通常会建议在这里使用一个类;)但是在不知道结构的内容的情况下,这有点棘手。
【讨论】:
汤姆谢谢老兄。您能否提供一些信息,概述为什么我的 CPU 可以使用 64 字节?顺便说一句 - 结构是避免锁的自然选择。如果我使用不可变类或锁代替,开发成本会更高......所以让我们传递类与结构...... 嗯,没有参考。我只是觉得 64 字节可能不会太多。这真的取决于那里的数据。我通常不使用这样的结构。我有一个金融交易应用程序(大量数据通过 - 很少,大约 64 字节,每秒 50.000 多个项目)并且我使用类。 我对 struct vs class 的看法可能是错误的......值得研究一下。【参考方案5】:实际上,在 64 位机器上,32 字节是用于结构数据的最大大小。根据我的测试,32 字节结构(四个长字节)的性能与对象引用差不多。
您可以在此处找到测试代码并自行运行测试: https://github.com/agileharbor/structBenchmark
最后几个测试将大量结构/对象从一个数组复制到另一个数组,您可以看到类在哪些方面优于具有 8 个 long 的 struct,但它与具有 4 个 long 的 struct 大致相同。
【讨论】:
【参考方案6】:我在进行汇编语言编程时学到的一件事是,将结构(和其他数据)对齐在 8 或 16 字节边界上(在 MASM 中实际上有一个预处理器命令)。这样做的原因是在内存中存储或移动内容时内存访问速度要快得多。将结构的大小保持在 16 字节以下将确保可以成功完成此内存对齐,因为没有任何东西会越过对齐边界。
但是,我希望在使用 .NET 框架时,您基本上不会看到这类事情。如果您的结构大于 16 字节,那么我希望框架(或 JIT 编译器)在 32 字节边界上对齐您的数据,依此类推。看看那些书中对 cmets 的一些验证会很有趣,看看速度上有什么不同。
【讨论】:
结构的布局并不是什么秘密。查找 LayoutStruct C# 属性。例如vsj.co.uk/articles/display.asp?id=501 这是一个很好的链接。我更多的是谈论框架/JITter 的行为,并想知道为什么 16 是一个如此神奇的数字(以及它是否仍然如此神奇,或者它是否已经增加或无关紧要)。【参考方案7】:我认为另一个关键点是,如果你有一个包含一百万个结构的列表,那仍然只是堆上的一个内存分配,而一个包含一百万个类的列表将有大约一百万个单独的堆对象.在这两种情况下,垃圾收集负载是完全不同的。从这个角度来看,struct 可能会走在前面。
【讨论】:
如果您将结构存储在堆分配的对象中,您最好将它们也分配到堆中!否则,当您当前的堆栈帧死亡(连同堆栈分配的对象)并且您尝试通过堆分配的对象访问它们时,事情会变得非常有趣。这就是为什么所有成员(包括类型的值)都存储在堆上的原因之一 @Rune FS - 是的,但整个结构向量将位于一个大的、连续的内存块中。与具有独立生命周期的一百万个小部件相反。 这对于基于节点的列表实现是正确的。但是,System.Collections.Generic.List<T>
在内部由 T[]
支持,它可以根据需要调整大小。以上是关于为啥 16 字节是 C# 中 struct 的推荐大小?的主要内容,如果未能解决你的问题,请参考以下文章
C# Struct 到字节数组的编组方式与 Delphi 中的打包记录相同
不同应用程序(WPF 和 Unity)中的 C# struct 序列化返回不同大小的字节数组
为啥c#中bool要占4个字节 32位呢 为啥不用像byte 1个字节存储呢