就性能而言,在该列表中使用struct而不是class的预期相对结果是什么

本文关键字:class 相对 是什么 结果是 结果 性能 列表 struct | 更新日期: 2023-09-27 18:25:54

我们将LogEntry类型的日志项推送到List<LogEntry>类型的列表中,该列表稍后将保存到文件中。

编辑1:我们不立即将日志刷新到文件的原因是,这是在Windows Phone上的一个具有独立存储的高度多线程应用程序中。写入孤立存储本身就很慢,与桌面性能相去甚远。立即刷新每条消息的开销实际上会扼杀我们想要用日志观察的并发状态转换和交互结束编辑1

假设我们在不太长的时间间隔内将数百万个项目添加到列表中,那么考虑到项目的以下内容,我们会为项目使用值或引用类型吗?

internal struct LogEntry
{
    public int ThreadId { get; private set; }
    public DateTime Timestamp { get; private set; }
    public string Message { get; private set; }
    public LogEntry(int threadId, DateTime timestamp, string message)
        : this()
    {
        this.ThreadId = threadId;
        this.Timestamp = timestamp;
        this.Message = message ?? string.Empty;
    }
}

internal sealed class LogEntry
{
    public int ThreadId { get; private set; }
    public DateTime Timestamp { get; private set; }
    public string Message { get; private set; }
    public LogEntry(int threadId, DateTime timestamp, string message)
    {
        this.ThreadId = threadId;
        this.Timestamp = timestamp;
        this.Message = message ?? string.Empty;
    }
}

在实际将日志保存到文件之前,除了添加项目之外,我们不会对项目列表执行任何操作。我们不搜索、删除或枚举它,只添加项目。

你说呢,struct还是class真的重要?

EDIT 2:通过在线程池工作项中排队在UI线程上交错添加60000个条目的时间测量结果对于struct情况为65秒,对于class情况为69秒。由于这种差异很小,class实现稍微干净一些,而且我不需要值相等语义,所以我决定将LogEntry作为classEND EDIT 2

就性能而言,在该列表中使用struct而不是class的预期相对结果是什么

如果使用类,则该类类型的每个存储位置(变量、结构字段或数组元素)将为4/8字节(在x86/x64上),并且该类型的每个明显创建的实例(至少存在一个引用)将占用16/20字节的字段,外加8/16字节的类相关开销。如果使用一个结构,该结构类型的每个存储位置都将占用16/20字节,句点。除非您期望您类型的许多实例将有多个存储位置指向它们,否则结构将更高效。当使用所谓的不可变结构时,应该注意以下语句:

myLogEntry=新日志条目(someId、someTime、someMessage)

将实际在C#中作为执行

LogEntry温度;LogEntry.CallConstructor(ref temp,someId,someTime,someMessage);//不是真正的方法名称myLogEntry_ThreadId=临时_ThreadId;myLogEntry_时间戳=临时_Timestamp;myLogEntry_消息=临时消息;

也就是说,构造函数将生成一个新的临时实例,然后赋值将通过从临时实例中复制所有公共和私有字段来改变现有实例(结构赋值几乎总是通过有效地复制所有公共字段和私有字段而起作用)。在大多数单线程代码中,这不会是一个问题,但在多线程代码中需要注意的是,在编写结构时读取它可能会产生新旧数据的任意混合。在某种程度上,我更喜欢可变结构,因为直接编写字段可以更明显地表明它们实际上是单独编写的。

我对通过在线程池工作项中排队在UI线程上交错添加60000个条目的时间进行测量的结果是,结构情况下为65秒,类情况下为69秒。由于这种差异很小,类的实现也稍微干净一些,而且我不需要struct给出的默认值相等语义,所以我决定在应用程序开始时将LogEntry作为一个类

,在这两种情况下,应用程序的内存打印都非常小

如果选择堆栈(结构)路径,

  1. 在启动时,您将在启动
  2. 随着时间的推移,您可以将日志条目附加到此结构中
  3. --内存打印增长
  4. 您将其提交到文件
  5. -您清除了结构的内容(我推测)
  6. 该结构将保留在堆栈上

如果使用堆(类)路径

  1. 在应用程序启动时,实例化的对象少了一个
  2. 在第一次使用该类时,实例化该对象
  3. 您将日志条目添加到此类
  4. --内存打印增长,(速度相同)
  5. 您将内容提交到文件
  6. 并取消引用该对象,让垃圾收集器完成它的工作

唯一(我相信)最大的区别是应用程序生命周期中内存资源的保留

此外,Eric在解释值类型

上的垃圾收集方面做得很好

简短的回答:我会去上课。

作为一种值类型,结构在"局部作用域"(特定于一个类或方法)中创建时,通常会放在堆栈上。这允许快速访问,而且一旦您不再需要它,就很容易摆脱它,因此它是存储简单的、本地范围的变量的首选方法。但是,在您的情况下,此值类型将成为集合的元素(集合是一种引用类型,存储在堆中)。堆上的对象不能引用堆栈上的对象;必须将对象从堆栈复制到堆,并引用对象的新堆地址。这个过程被称为"装箱"。然后,当结构被分配给另一个局部变量,或通过值传递给方法时,它会被复制回堆栈上的某个位置("unboxing")。装箱和取消装箱的过程会影响性能,通常在可行的情况下应该避免(当然,有时你根本无法避免;值类型的列表并不是一件坏事,因为它需要装箱,如果它是适合工作的工具的话)。

相比之下,类是一种"引用类型",创建时默认存储在堆中。这意味着它可以根据需要挂起很长时间(只要它至少有一个由作用域中的其他变量对它的引用),并且堆栈上只需要一个32位整数"指针"(技术上与C/C++指针不同,但类似)到堆中对象的地址。要访问对象的数据成员,必须"取消引用"指针以从堆中的对象读取必要的数据,但在大多数情况下,当类没有被主动操作时,传递要容易得多(单个32位变量,而不是在不同地方复制"宽"多变量块)。

因此,由于您知道对象无论如何都必须放在堆上,而且在日志项的情况下,您可能不会在创建对象和将其数据持久化到文件之间进行大量操作,我认为您会发现该类的性能会更好。但是,坦率地说,栈与堆是一个实现细节,通常但并不总是正确的,根据结构的大小和传递它的次数,从一个栈到另一个堆一次复制的开销与初始化类的多指针取消引用的开销,根据您的确切代码,这两种方法都可能更好地工作。所以,我同意孙的观点;在单元测试中把两个候选者排成一行,也许最好的实现会胜出。