c# - 管理数百个类而不创建和销毁它们?
问题描述
例如,我有一个适用于数百或数千个类的 A 类,每个类都有一个带有一些计算的方法。A 类有一种方法,它可以选择运行数百或数千个类中的哪个类。并且A类的方法在短时间内运行了很多次。
一开始我认为的解决方案是在A类中已经创建了类,以避免每次执行事件时都必须创建和销毁类,并且垃圾收集器会消耗CPU。但是,正如我所说,这个 A 类将有成百上千个类要运行,并且全部加载它们在内存中的开销太高(我认为)。
我的问题是,你能想出一种处理成百上千个类的最佳方法,它每秒都会运行其中一些类,而不必在每次执行与它们一起工作的方法时创建和销毁它?
编辑:
第一个例子:创建并保存类然后使用它们,我认为这将是内存开销。但是不要让垃圾收集器工作太多。
public class ClassA {
Class1 class1;
Class2 class2;
// ... more classes
Class100 class100;
public ClassA() {
class1 = new Class1();
// ... initializations
class100 = new Class100();
}
public ChooseClass(int numberClass) {
switch (numberClass) {
case 1:
class1.calculate();
break;
case 2:
class2.run();
break;
// ... more cases, one for each class
case 100:
class100.method();
break;
default:
break;
}
}
}
第二个例子:使用时创建类,节省内存但垃圾收集器消耗大量CPU。
public class ClassA {
public ChooseClass(int numberClass) {
switch (numberClass) {
case 1:
Class1 class1 = new Class1();
class1.calculate();
break;
case 2:
Class2 Class2 = new Class2();
class2.run();
break;
// ... more cases, one for each class
case 100:
Class100 class100 = new Class100();
class100.method();
break;
default:
break;
}
}
}
解决方案
当您开始增加类实例的数量时,您面临的基本问题是它们都需要在垃圾收集操作期间进行统计和跟踪,即使您从未释放这些实例,垃圾收集器仍然需要跟踪它们。当程序花费更多时间执行垃圾收集而不是实际工作时,就会出现这样的情况。我们在使用二叉搜索树时遇到了这种性能问题,该树最终包含数百万个最初是类实例的节点。
我们能够通过使用List<T>
结构而不是类来规避这个问题。(列表的内存由数组支持,对于结构,垃圾收集器只需要跟踪对该数组的单个引用)。现在,我们不是对类的引用,而是将索引存储到这个列表中,以便访问所需的结构实例。
事实上,我们也遇到了问题(注意较新版本的 .NET 框架取消了这个限制),即使在 64 位下,支持数组也不能超过 2GB,因此我们将存储拆分为多个列表(256)并使用一个 32 位索引,其中 8 位用作列表选择器,其余 24 位用作列表的索引。
当然,构建一个抽象所有这些细节的类很方便,而且你需要注意,在修改结构时,实际上需要将其复制到本地实例,修改它然后将原始结构替换为修改后的实例,否则您的更改将发生在结构的临时副本中,并且不会反映在您的数据收集中。此外,还有性能影响,幸运的是,一旦收集足够大,就会得到回报,垃圾收集周期非常快。
这里有一些代码(相当陈旧),展示了这些想法,并且通过将我们的搜索树迁移到这种方法,从服务器花费接近 100% 的 CPU 时间到大约 15%。
public class SplitList<T> where T : struct {
// A virtual list divided into several sublists, removing the 2GB capacity limit
private List<T>[] _lists;
private Queue<int> _free = new Queue<int>();
private int _maxId = 0;
private const int _hashingBits = 8;
private const int _listSelector = 32 - _hashingBits;
private const int _subIndexMask = (1 << _listSelector) - 1;
public SplitList() {
int listCount = 1 << _hashingBits;
_lists = new List<T>[listCount];
for( int i = 0; i < listCount; i++ )
_lists[i] = new List<T>();
}
// Access a struct by index
// Remember that this returns a local copy of the struct, so if changes are to be done,
// the local copy must be copied to a local struct, modify it, and then copy back the changes
// to the list
public T this[int idx] {
get {
return _lists[(idx >> _listSelector)][idx & _subIndexMask];
}
set {
_lists[idx >> _listSelector][idx & _subIndexMask] = value ;
}
}
// returns an index to a "new" struct inside the collection
public int New() {
int result;
T newElement = new T();
// are there any free indexes available?
if( _free.Count > 0 ) {
// yes, return a free index and initialize reused struct to default values
result = _free.Dequeue();
this[result] = newElement;
} else {
// no, grow the capacity
result = ++_maxId;
List<T> list = _lists[result >> _listSelector];
list.Add(newElement);
}
return result;
}
// free an index and allow the struct slot to be reused.
public void Free(int idx) {
_free.Enqueue(idx);
}
}
下面是我们的二叉树实现最终如何使用这个SplitList
支持容器类的片段:
public class CLookupTree {
public struct TreeNode {
public int HashValue;
public int LeftIdx;
public int RightIdx;
public int firstSpotIdx;
}
SplitList<TreeNode> _nodes;
…
private int RotateLeft(int idx) {
// Performs a tree rotation to the left, here you can see how we need
// to retrieve the struct to a local copy (thisNode), modify it, and
// push back the modifications to the node storage list
// Also note that we are working with indexes rather than references to
// the nodes
TreeNode thisNode = _nodes[idx];
int result = thisNode.RightIdx;
TreeNode rightNode = _nodes[result];
thisNode.RightIdx = rightNode.LeftIdx;
rightNode.LeftIdx = idx;
_nodes[idx] = thisNode;
_nodes[result] = rightNode;
return result;
}
}