新列表还是列表池?

huangapple go评论68阅读模式
英文:

new List or List Pool?

问题

在 C# 中,哪种方式对性能更好?

假设这一切都在一个名为 "MyStaticClass" 的类中声明:

         void FunctionThatGetsCalledAlot(...)
         {
			List<SomeClass> previousAttributes = new List<SomeClass>();
			previousAttributes.AddRange(SomeStuff);
			node.RemoveAttributes();
            ....
         }

或者这个:

		private class ThreadSafeObjectPool<T> where T : new()
		{
			private readonly Stack<T> pool = new Stack<T>();

			public T Get()
			{
				lock (this.pool)
				{
					if (this.pool.Count > 0)
					{
						return this.pool.Pop();
					}
				}

				return new T();
			}

			public void Return(T v)
			{
				lock (this.pool)
				{
					this.pool.Push(v);
				}
			}
		}

         private static readonly ThreadSafeObjectPool<List<SomeClass>> previousAttributes = new ObjectPool<List<SomeCLass>>();
         void FunctionThatGetsCalledAlot(...)
         {
			List<SomeClass> previousAttributes = MyStaticClass.previousAttributes.Get();

            try
            {
               previousAttributes.Clear();
			   previousAttributes.AddRange(SomeStuff);
               ....
            }
            finally
            {
                 MyStaticClass.previousAttributes.Return(previousAttributes);
            }
         }

不是每次都使用新的 List<>,我们有一种"List 池",避免每次调用 new,并且垃圾回收器不必清理它。理论上,这样做是否更快,或者由于每次调用 Clear() 而失去所有优化价值呢?

英文:

In C#, what is better for performance?

Assuming this is all declared in a class called "MyStaticClass"

     void FunctionThatGetsCalledAlot(...)
     {
		List&lt;SomeClass&gt; previousAttributes = new List&lt;SomeClass&gt;();
		previousAttributes.AddRange(SomeStuff);
		node.RemoveAttributes();
        ....
     }

Or this:

	private class ThreadSafeObjectPool&lt;T&gt; where T : new()
	{
		private readonly Stack&lt;T&gt; pool = new Stack&lt;T&gt;();

		public T Get()
		{
			lock (this.pool)
			{
				if (this.pool.Count &gt; 0)
				{
					return this.pool.Pop();
				}
			}

			return new T();
		}

		public void Return(T v)
		{
			lock (this.pool)
			{
				this.pool.Push(v);
			}
		}
	}

     private static readonly ThreadSafeObjectPool&lt;List&lt;SomeClass&gt;&gt; previousAttributes = new ObjectPool&lt;List&lt;SomeCLass&gt;&gt;();
     void FunctionThatGetsCalledAlot(...)
     {
		List&lt;SomeClass&gt; previousAttributes = MyStaticClass.previousAttributes.Get();

        try
        {
           previousAttributes.Clear();
		   previousAttributes.AddRange(SomeStuff);
           ....
        }
        finally
        {
             MyStaticClass.previousAttributes.Return(previousAttributes);
        }
     }

Instead of using a new List<> every time, we have a kind of 'List Pool', to avoid having to call 'new' every time, and the garbage collector doesn't have to clean it up. In theory, should this be faster or would it lose all of its optimization value because of the call to Clear() every time?

答案1

得分: 3

以下是翻译好的内容:

当然,答案是:

取决于情况

您没有显示SomeStuff的类型或大小。您也没有说明previousAttributesFunctionThatGetsCalledAlot执行期间是否会增加其初始大小。

如果SomeStuff(只是集合,而不是其内容(除非它们是值类型实例))超过85千字节,那么这些讨论的一些内容将不得不改变 - 超过85k的对象存储在大对象堆(LOH)中,遵循不同的规则。

重要的是要意识到.NET垃圾收集器(GC)是一种分代GC。它有三代,Gen0(用于自上次收集以来创建的对象),Gen1(用于恰好存活一次收集的对象)和Gen2(用于较旧的对象)。Gen0收集非常便宜。因此,您的简单实现 不应该 对GC施加很大压力 - 每个列表将存在很短的时间,并且将在Gen0收集中迅速清理。其中较大的成本之一将是每次创建一个新列表对象时将其清零(但您的Clear调用可能执行了同样工作的效率较低的版本)。

您可以考虑的一件事是创建每个列表,使其具有正确的初始大小:

var previousAttributes = new List<SomeClass>(SomeStuff.Length);  //或Count()

当您创建一个列表然后使其比其初始大小更大时,它会通过分配新数组并使旧数组符合GC的条件来调整底层数组的大小。通过正确获取大小(或稍大于可能需要的大小),您可以消除调整大小的需求。

当您拥有大对象(存储在LOH中)时,_池_非常方便。出于许多原因,LOH对象确实会对垃圾收集器造成很大压力(它们仅在昂贵的Gen2收集中收集,其堆管理遵循非常不同的规则...)。然而,编写一个能够处理LOH的池是困难的。

总的来说,您很可能最好选择简单的解决方案。这是一种GC在这种情况下做得相当不错的情景。

英文:

Of course, the answer is:

It Depends

You don't show the type or the size of SomeStuff. Nor do you say whether previousAttributes grows from its initial size during the execution of FunctionThatGetsCalledAlot.

If SomeStuff (just the collection, not its contents (unless they are value type instances)) is over 85 kbytes, then some of this discussion will have to change - objects over 85k are stored in the Large Object Heap (LOH) and follow different rules.

It's important to realize that the .NET Garbage Collector (GC) is a generational GC. It has three generations, Gen0 (for objects that were created since the last collection), Gen1 (for objects that have survived exactly one collection) and Gen2 (for older objects). Gen0 collections are very cheap. So, your simple implementation should not put a lot of stress on the GC - each list will last a short while and will be quickly cleaned up in a Gen0 collection. One of the larger costs will be the zero-ing out of the new list objects every time you create on (but your Clear call probably does a less efficient version of the same work).

One thing you might consider is to create each list so it's the right size to start with:

var previousAttributes = new List&lt;SomeClass&gt;(SomeStuff.Length);  //or Count()

When you create a list and then you make it bigger than its initial size, it resizes the underlying array by allocating a new array and making the old one eligible for GC. By getting the size right (or a little bigger than possibly needed), you get rid of the resize.

Where pools are handy are when you have large objects (those stored in the LOH). For a bunch of reasons, LOH objects really stress the garbage collector (they are only every collected in expensive Gen2 collections, their heap management follows very different rules...). Writing a LOH-capable pool, however, is hard.

All in all, you are very likely better off going with the simple solution. It's the kind of scenario where the GC does a pretty good job.

答案2

得分: -1

大多数垃圾收集器都针对存活对象进行优化,忽略垃圾(不可达对象)。

一个示例的垃圾收集器将堆分为两半,一半是活动的,另一半是非活动的,并将从 Main 可达的所有对象复制到非活动的一半(然后变为活动的)。如果有 95% 的垃圾,那么只有 5% 的实际内存需要进行处理(而分配只是递增一个指针)。

这是浪费的,实际使用中的垃圾收集更复杂和高效(大多数活动对象在每次收集时也不需要任何处理),但重点是丢弃对象通常可以被视为“免费”。

我想说,在这种情况下,对象初始化(分配后)的成本大致与清理它的成本相当(可能会有内部对象无论如何都会被丢弃,所以你并没有避免所有的垃圾)。

英文:

Most garbage collectors are optimized for live objects, ignoring garbage (unreachable objects).

An example garbage collector splits the heap into two halves, one active and the other inactive, and copies all objects reachable from Main into the inactive half (which then becomes active). If it's 95% garbage, only the 5% real memory takes any work (and allocating merely increments a pointer).

This is wasteful, and garbage collection in real use is more complex and efficient (most active objects also take zero processing each collection), but the point is discarding objects can be thought of as "free" normally.

I'd say in this case, the object initialization (after allocation) would be roughly as expensive as cleaning it up (there will probably be internal objects that are discarded anyway, so you're not avoiding all garbage).

huangapple
  • 本文由 发表于 2023年6月6日 04:19:40
  • 转载请务必保留本文链接:https://go.coder-hub.com/76409743.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定