结构与类


93

我将在代码中创建100,000个对象。它们很小,只有2个或3个属性。我将它们放在通用列表中,当它们存在时,将循环它们并检查value a,也许更新value b

将这些对象创建为类还是结构更快/更好?

编辑

一个。属性是值类型(除了我认为的字符串?)

b。他们可能(我们不确定)有一个有效的方法

编辑2

我想知道:垃圾收集器是否平等地处理了堆和堆栈上的对象,或者工作原理有所不同?


2
他们只是要有公共场所,还是要有方法?类型是基本类型,例如整数吗?它们将包含在数组中还是在List <T>之类的文件中?
JeffFerguson 2010年

14
可变结构的列表?提防速动手。
Anthony Pegram

1
@安东尼:恐怕我错过了速激肽笑话:-s
米歇尔(Michel)2010年

5
迅猛龙的笑话来自XKCD。但是,当您四处乱扔“在堆栈上分配值类型”的误解/实现细节(如适用时删除),那么您需要当心Eric Lippert ...
Greg Beech 2010年

Answers:


137

难道更快创建这些对象的类或结构?

您是唯一可以确定该问题答案的人。两种方法都尝试一下,测量有意义的,以用户为中心的相关性能指标,然后您将知道更改是否对相关场景中的实际用户产生了有意义的影响。

结构消耗更少的堆内存(因为它们更小且更易于压缩,而不是因为它们“在堆栈上”)。但是,它们比参考副本需要更长的时间。我不知道您针对内存使用或速度的性能指标是什么;这里需要权衡,您就是知道这是什么的人。

它是更好的创建这些对象的类或结构?

也许是上课,也许是结构。作为一个经验法则:如果对象是:
1.小
2.逻辑上是一个不可变的值
3.有很多它们
然后我考虑将其构造为一个结构。否则我会坚持使用引用类型。

如果您需要更改结构的某些字段,通常最好构建一个构造函数,该构造函数返回正确设置了字段的整个新结构。这也许稍微慢一些(测量一下!),但是从逻辑上讲,更容易推理。

垃圾收集器是否平等地处理堆和堆栈上的对象?

,它们不一样,因为堆栈上的对象是集合的根。垃圾收集器不需要询问“堆栈中的这个东西还活着吗?” 因为该问题的答案始终是“是的,它在堆栈上”。(现在,您不能依靠它来使对象保持活动状态,因为堆栈是实现的详细信息。允许抖动引入优化,例如,注册通常是堆栈值的内容,然后再将其保存在堆栈中因此GC并不知道它还活着。注册的对象可以在不再次读取保存在其上的寄存器后立即主动收集其后代。)

但是垃圾收集器的确必须将堆栈上的对象视为活动对象,就像将已知的活动对象视为活动对象一样。堆栈上的对象可以引用需要保持活动状态的堆分配对象,因此,为了确定活动集,GC必须将堆栈对象视为活动的堆分配对象。但是显然,出于压缩堆的目的,它们被视为“活动对象”,因为它们首先不在堆中。

明白了吗?


埃里克(Eric),您知道编译器或抖动是否利用不变性(也许通过强制实施readonly)来进行优化。我不会让那影响对可变性的选择(理论上我对效率细节一窍不通,但实际上,我朝着效率迈进的第一步始终是尽我所能尽可能简单地保证正确性,因此不必在检查和边缘情况下浪费CPU周期和大脑周期,并在其中适当地进行可变或不可变会有所帮助),但这会抵消您所说的不变性可能会变慢的任何下意识的反应。
乔恩·汉娜

@Jon:C#编译器优化const数据,但不优化只读数据。我不知道jit编译器是否对只读字段执行任何缓存优化。
埃里克·利珀特

遗憾的是,就我所知,关于不变性的知识允许进行一些优化,但在那时达到了我的理论知识的极限,但这是我乐意扩展的极限。同时,“两种方式都可以更快,这就是为什么,现在进行测试并找出在这种情况下适用的方法”可以说:)
乔恩·汉娜

我建议阅读simple-talk.com/dotnet/.net-framework/…和您自己的文章(@Eric):blogs.msdn.com/b/ericlippert/archive/2010/09/30/…开始潜水进入细节。周围还有许多其他好的文章。顺便说一句,通过处理类的一些内存开销(〜2.3 MB),几乎无法注意到处理100 000个小型内存对象的区别。可以通过简单的测试轻松检查。
Nick Martyshchenko

是的,很明显。非常感谢您的综合(广泛更好?Google翻译提供了2种翻译。我的意思是说您花时间不写简短的答案,但也花时间写所有细节)答案。
米歇尔(Michel)2010年

23

有时,struct您不需要调用new()构造函数,而是直接分配字段,使其比通常更快。

例:

Value[] list = new Value[N];
for (int i = 0; i < N; i++)
{
    list[i].id = i;
    list[i].isValid = true;
}

比大约快2至3倍

Value[] list = new Value[N];
for (int i = 0; i < N; i++)
{
    list[i] = new Value(i, true);
}

其中Value是一个struct具有两个字段(idisValid)。

struct Value
{
    int id;
    bool isValid;

    public Value(int i, bool isValid)
    {
        this.i = i;
        this.isValid = isValid;
    }
}

另一方面,这些项目需要移动或选定的值类型会影响您的复制速度。为了获得确切的答案,我怀疑您必须分析代码并进行测试。


显然,当您在本机边界上封送值时,事情也会变得更快。
嬉皮

我建议使用除以外的名称list,因为指定的代码不适用于List<Value>
2015年

7

结构似乎与类相似,但是您应该注意一些重要的区别。首先,类是引用类型,而结构是值类型。通过使用结构,您可以创建行为类似于内置类型的对象,并享受它们的好处。

在类上调用New运算符时,它将在堆上分配。但是,当实例化一个结构时,它会在堆栈上创建。这将提高性能。同样,您将不会像使用类那样处理对结构实例的引用。您将直接使用struct实例。因此,在将结构传递给方法时,它是按值而不是作为引用传递的。

更多内容:

http://msdn.microsoft.com/zh-CN/library/aa288471(VS.71).aspx


4
我知道它是在MSDN上说的,但是MSDN并没有讲完整的故事。堆栈与堆是实现细节,结构并不总是放在堆栈上。有关最近的一个博客,请访问:blogs.msdn.com/b/ericlippert/archive/2010/09/30/…–
安东尼·佩格拉姆

“ ...按值传递...”引用和结构均按值传递(除非使用“ ref”)-是要传递的值还是引用不同,即结构按值传递,类对象按值传递,而ref标记的params则按引用传递。
Paul Ruane 2010年

10
该文章在几个关键点上具有误导性,我已经请MSDN团队修改或删除它。
埃里克·利珀特

2
@supercat:解决您的第一点:更大的一点是,在托管代码中,存储值或对值的引用基本上无关紧要。我们一直在努力创建一个内存模型,该内存模型在大多数情况下允许开发人员允许运行时代表他们做出智能存储决策。当无法像在C语言中那样理解崩溃时,这些区别就非常重要。在C#中不多。
埃里克·利珀特

1
@supercat:要解决您的第二点,没有可变结构主要是邪恶的。例如,void M(){S s = new S(); s.Blah(); N(s);}。重构为:void DoBlah(S s){s.Blah(); } void M(S s = new S(); DoBlah(s); N(s);}。刚刚引入了一个错误,因为S是可变结构。您立即看到了该错误吗?一个可变的结构对您隐藏错误?
埃里克·利珀特

6

结构数组在堆上的一个连续内存块中表示,而对象数组被表示为一个连续的引用块,而实际对象本身在堆中的其他位置,因此这两个对象及其数组引用都需要内存。

在这种情况下,当您将它们放置在a中List<>(并将a返回List<>到数组中)时,使用结构在内存方面将更为有效。

(但是请注意,大型阵列会在大型对象堆上找到位置,如果它们的寿命很长,可能会对您的进程的内存管理产生不利影响。另外,请记住,内存并不是唯一的考虑因素。)


您可以使用ref关键字来处理此问题。
嬉皮

“但是请注意,大型阵列将在大型对象堆上找到位置,如果它们的寿命很长,则可能会对进程的内存管理产生不利影响。” -我不太确定你为什么会那样想?在LOH上进行分配不会对内存管理造成任何不利影响,除非(可能)它是一个短暂的对象,并且您希望快速回收内存而不等待第二代收集。
乔恩·阿图斯

@Jon Artus:LOH并没有被压缩。任何长期存在的对象都会将LOH划分为之前和之后的空闲内存区域。分配需要连续的内存,如果这些区域不足以进行分配,则会向LOH分配更多的内存(即,您将得到LOH碎片)。
Paul Ruane 2010年

4

如果它们具有值语义,那么您应该使用结构。如果它们具有引用语义,那么您可能应该使用一个类。有一些例外,即使存在值语义,它们也大多倾向于创建类,但从那里开始。

至于您的第二次编辑,GC仅处理堆,但是堆空间比堆栈空间大得多,因此将内容放到堆栈上并不总是一个胜利。除此之外,结构类型列表和类类型列表都将以任何一种方式出现在堆上,因此在这种情况下这是无关紧要的。

编辑:

我开始认为邪恶一词是有害的。毕竟,如果不需要主动使类变为可变的,这是个坏主意,而且我也不排除使用可变的结构。尽管这是一个糟糕的主意,但通常几乎总是一个坏主意,但是在大多数情况下,它只是与值语义不一致,因此在给定情况下使用结构是没有意义的。

私有嵌套结构可能会有合理的例外,因此该结构的所有使用都被限制在非常有限的范围内。这不适用于这里。

确实,我认为“它会变异,所以结构不好”并没有比处理堆和栈好多少(即使确实经常出现错误,这至少会对性能产生影响)。“它是变异的,因此认为它具有值语义很可能没有任何意义,因此它是一个不好的结构”,只是略有不同,但重要的是,我认为。


3

最好的解决方案是先测量,然后再测量,然后再测量更多。可能会有您正在执行的操作的详细信息,可能使诸如“使用结构”或“使用类”之类的简单易懂的答案变得困难。


同意量度部分,但我认为这是一个简单明了的例子,我认为也许可以说一些通用的东西。事实证明,有些人做到了。
米歇尔(Michel)2010年

3

从本质上讲,结构只是字段的集合而已。在.NET中,有可能“假装”结构成为对象,并且对于每种结构类型,.NET都隐式定义了具有相同字段和方法的堆对象类型,这些字段和方法作为堆对象,其行为类似于对象。拥有对此类堆对象(“盒装”结构)的引用的变量将表现出引用语义,但是直接拥有结构的变量只是变量的集合。

我认为,结构与类的混淆很大程度上源于以下事实:结构具有两个非常不同的使用案例,它们应该具有非常不同的设计准则,但是MS准则并未区分它们。有时需要某种行为像一个对象;在这种情况下,尽管“ 16字节限制”应该更像24-32,但MS准则还是很合理的。但是,有时需要的是变量的汇总。用于此目的的结构应仅由一堆公共字段组成,并且可能Equals覆盖,ToString覆盖和IEquatable(itsType).Equals实施。用作字段聚合的结构不是对象,也不应该假装为对象。从结构的角度来看,字段的含义应无非是“写入该字段的最后一件事”。任何其他含义应由客户代码确定。

例如,如果变量聚合结构具有成员MinimumMaximum,则结构本身不应该保证Minimum <= Maximum。将这样的结构作为参数接收的代码应表现得好像它是分别传递Minimum和传递Maximum值一样。Minimum不大于要求的要求Maximum应被视为类似于Minimum不大于单独传递的参数的要求Maximum

有时要考虑的一个有用模式是让一个ExposedHolder<T>类定义如下:

class ExposedHolder<T>
{
  public T Value;
  ExposedHolder() { }
  ExposedHolder(T val) { Value = T; }
}

如果一个具有List<ExposedHolder<someStruct>>,其中someStruct是一个变量聚合结构,则可能会做类似的事情myList[3].Value.someField += 7;,但是赋予myList[3].Value其他代码将为其提供内容,Value而不是为其进行更改。相反,如果使用a List<someStruct>,则必须使用var temp=myList[3]; temp.someField += 7; myList[3] = temp;。如果使用可变的类类型,则将内容暴露myList[3]给外部代码将需要将所有字段复制到其他对象。如果使用了不可变的类类型或“对象样式”结构,则有必要构造一个新实例,该实例与myList[3]其他实例someField不同,然后将其存储到列表中。

还有一点需要注意:如果要存储大量类似的东西,最好将它们存储在可能嵌套的结构数组中,最好将每个数组的大小保持在1K到64K左右。结构数组是特殊的,在其中索引一个将直接引用其中的结构,因此可以说“ a [12] .x = 5;”。尽管可以定义类似数组的对象,但是C#不允许它们与数组共享这种语法。



1

从c ++的角度来看,我同意与类相比,修改structs属性会比较慢。但是我确实认为,由于结构是在堆栈而不是堆上分配的,因此读取它们的速度更快。从堆读取数据比从堆读取数据需要更多的检查。


1

好吧,如果您毕竟使用struct,那么请摆脱字符串并使用固定大小的char或字节缓冲区。

那就是:性能。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.