C#中使用'new'的性能代价是什么?

43

C#中使用new关键字的性能成本是多少?我特别是在与游戏开发有关的情况下询问,我知道在C++中每个更新周期都新建东西是明确不允许的。这种情况适用于C#吗?我正在使用XNA并为移动设备开发。只是作为优化问题而已。

在C#中使用new关键字通常不会导致性能问题,因为.NET运行时拥有垃圾回收器(GC),可以自动管理内存。但是如果您在游戏循环中频繁地使用new关键字,则可能会导致GC频繁触发,从而降低性能。因此,对于需要高性能的游戏应用程序,建议使用对象池或重用对象来减少垃圾生成和GC次数。总之,在大多数情况下,使用new关键字不会对性能产生显著影响,但在具体情况下,需要根据实际情况进行考虑和优化。

1
你觉得你的问题真的需要加上C++标签吗?你的问题与C++毫无关系,而且可以在不提到它的情况下进行提问。(换句话说,一个没有C++经验的人也能回答这个问题。) - GManNickG
3
我更希望有C++经验的人回答这个问题,因为我本身是一名C++程序员,想了解它与C#之间微妙的区别。 - meds
2
你可能应该阅读这篇博客文章:http://blogs.msdn.com/b/shawnhar/archive/2007/07/02/twin-paths-to-garbage-collector-nirvana.aspx 问题不在于 new,而是它可能会触发垃圾回收。(new 本身非常快。) - Andrew Russell
10个回答

42

new 的成本包括三个部分:

  • 分配内存(如果是值类型可能不需要)
  • 运行构造函数(取决于你要做什么)
  • 垃圾回收成本(同样,如果是值类型并且上下文允许,则可能不适用)

在主代码中完全不创建 任何 新对象很难以符合 C# 的惯用法……虽然我敢说通过尽可能重复使用对象的方式,这是可行的。尝试获取一些真实设备,并查看游戏的性能。

我肯定同意像这样的微优化通常应该避免在编程中使用,但它更有可能适用于游戏循环而非其他地方——因为显然游戏对即使极小的暂停也非常敏感。但使用更多对象的成本相当难以判断,因为由于 GC 成本的缘故,这些成本在时间上有所分散。

.NET 中的分配器和垃圾回收器非常出色,尽管在设备上(如 Windows Phone 7,我假设?)可能会更简单。特别是,我不确定 Compact Framework CLR(WP7 使用的那个)是否具有分代 GC。


1
紧凑框架没有分代 GC——至少是在我上次查看时。但那是在 PocketPC 2003 时代,当时大多数设备只有 32MB 的内存... - Rei Miyasaka
@Rei:是的,旧版本肯定没有。不确定3.7,这是WP7中的版本。 - Jon Skeet
嗯,设计一个实验来找出答案会很酷。不过这将是一个挑战。 - Rei Miyasaka
@JonSkeet:使用单例模式可以避免创建过多的对象吗? - MagB
@MagB:是的,但在大多数情况下,我认为它的不灵活性仍然值得避免。 - Jon Skeet

27
C#中的分配比C++更快,只需增加堆指针并返回该指针即可。通常情况下,C#中会更频繁地创建对象,因为在字符串等方面涉及到了更多的不可变性。
正如其他人所指出的,真正的问题在于垃圾收集器,这有点难以进行分析。尽管如此,在大多数情况下,即使与C++中的delete相比,GC也同样快或者更快 - 只是您无法预测何时会发生。
.NET团队的性能专家Rico Mariani提供了一些提示: http://msdn.microsoft.com/en-us/library/ms973837.aspx 它有点过时,并且GC已经进行了一些改进,但大部分信息仍然相关。
我应该补充一点,即XNA / Compact Framework垃圾收集器比x86版本要慢一些,以换取CPU和内存性能,因此您应该注意这一点。
编辑

我忘了提到,这很重要:值类型,包括结构体,也使用new语法,但它们是在堆栈上创建的,而不是在堆上创建的,因此对于它们来说没有GC成本,除非你装箱它们。


对于GC的良好讨论和正确性给予加1。游戏中内存分配的最大问题在于垃圾收集器会停止所有线程进行收集。这很容易导致帧率出现卡顿。 - Andrew Russell
是的,有多种可用的GC实现来解决这个问题。在Windows上,有IIS使用的服务器GC和默认情况下.exe使用的应用程序GC。服务器GC更彻底,但锁定所有线程的时间更长,频率更低。如果我们很幸运,他们会调整CF / XNA的GC以使其更频繁,并且锁定时间更短。 - Rei Miyasaka
2
不确定手机的情况。但 Xbox 360 版本每1MB分配运行一次,且非代际式。它足够慢,需要关注它。另一方面,Windows版本在大多数情况下适用于游戏开发。 - Andrew Russell
4
当一个栈上的值类型包含一个引用时,它需要被回收;不是因为它本身可能已经死亡,而是因为它所包含的引用可能还活着。 包含引用类型的栈分配的值类型仍会对垃圾回收造成负担。 - Eric Lippert

7
新运算符本身的成本是微不足道的。可能造成成本的是在自定义构造函数中发生的处理。因此,如果在此构造函数中有很多事情要处理,那么这可能会成为一个问题。

5

在编写代码时,应该始终选择"最简单"的方式,然后检测是否存在瓶颈。

不管怎样,我永远不会用C#编写游戏。

编辑

由于引起了不满,我是指如果有C++可用,我永远不会使用C#进行编码。这是我认为的,因为OP也标记了C++。


1
据我所知,凯撒大帝4是用C#编写的,至少部分是这样 - 简直太糟糕了,就好像你可以感觉到GC正在运行一样 :) - Armen Tsirunyan
10
如果OP正在开发WP7应用,那么唯一的选项是VB。这是你想要的吗?!? - Rob Fonseca-Ensor
2
你不仅担心移动应用程序的瓶颈,还担心电池消耗。 - Rei Miyasaka
1
@Armen - 快速搜索得知它使用C#进行游戏逻辑,如果正确应用(JIT编译),将比使用脚本语言如Lua或UnrealScript提供更好的性能提升 - JulianR

4

new本身的成本可能不是很显著,但是在堆上分配新对象(创建引用类型实例时)可能会触发垃圾收集,这种副作用在游戏中可能非常显著。如果您担心GC,可以使用值类型或重复使用对象。

此外,正如Darin正确指出的那样,构造函数可能会执行大量工作。


2
在C#中,使用new操作符几乎没有成本,但是当你在引用类型上使用new时,将会有一个堆分配,并且这是由GC维护的。如果内存是连续的,那么分配内存只需要移动指针,但如果发现两个分配之间的间隙,则GC将查询可用内存的空闲列表(链接列表)以找到所需的对象,这有时需要O(n)的遍历时间。
参见Eric Lippert的文章。

1
通常情况下,对于像Rect、Point和其他微小结构体而言,C#错过堆栈分配会导致性能缺失。对于CPU而言,在堆栈上的空间分配仅为bp:sp = bp:sp + size,而使用New进行堆分配则需要许多算法、GC控制、大内存页面大小、最终在磁盘上的重定位等。

1
在C#中,垃圾回收器通过保持内存紧凑来进行碎片整理,因此查找和分配内存是非常快速的操作。然而,GC偶尔执行的碎片整理可能会显著减慢速度。

1
当您在C#中编写new时,CLR会在托管堆上为对象分配内存。
请查看以下链接以获取更多信息:

http://msdn.microsoft.com/en-us/library/ee787088(v=vs.100).aspx

由于C#中的垃圾回收是自动的(只有在测试期间使用GC.Collect手动触发时才会出现),因此在C#上进行游戏设计是个不好的主意。
你应该考虑使用C++,因为在C++中有析构函数。
~ClassName()
{
//kill my object and reclaim memory.
}

0

我的测试显示,一个“新”的引用类型有时可能需要长达2.2秒的时间,有时可能只需要几个滴答声。底线是,在.NET GC中没有实时保证,性能可以以随机方式显着变化。因此,我建议对于实时设计(例如游戏),不要在代码的实时部分(例如更新循环)中调用引用类型的“new”,因为没有实时保证。

当然,许多人会争辩说,“new”需要2.2秒的频率有多高,如果只发生几个小时一次,那么这是否重要。好吧,这必须是设计师的判断。但出于学术论证的目的,GC不是实时的,因此不要在实时代码中调用“new”。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接