我想知道为什么orderBy会消耗更多的内存,然后只复制列表并进行排序。
void printMemoryUsage()
{
long memory = GC.GetTotalMemory(true);
long mb = 1024 * 1024;
Console.WriteLine("memory: " + memory/mb + " MB" );
}
var r = new Random();
var list = Enumerable.Range(0, 20*1024*1024).OrderBy(x => r.Next()).ToList();
printMemoryUsage();
var lsitCopy = list.OrderBy(x => x);
foreach(var v in lsitCopy)
{
printMemoryUsage();
break;
}
Console.ReadKey();
我得到的结果是:
内存:128 MB
内存:288 MB
但是复制列表和排序消耗的内存更少。
void printMemoryUsage()
{
long memory = GC.GetTotalMemory(true);
long mb = 1024 * 1024;
Console.WriteLine("memory: " + memory/mb + " MB" );
}
var r = new Random();
var list = Enumerable.Range(0, 20*1024*1024).OrderBy(x => r.Next()).ToList();
printMemoryUsage();
var lsitCopy = list.ToList();
printMemoryUsage();
lsitCopy.Sort();
printMemoryUsage();
Console.ReadKey();
结果是:
内存:128 MB
内存:208 MB
内存:208 MB
更多测试表明orderBy消耗的内存是列表大小的两倍。
答案 0 :(得分:0)
当您深入研究内部如何实现这两种方法时,这并不奇怪。看看.NET的Reference Source。
在第二种方法中,您在列表上调用Sort()
方法,将List对象中的内部数组传递到用C#以外的本机代码编写的TrySZSort
方法。垃圾收集器没有工作。
private static extern bool TrySZSort(Array keys, Array items, int left, int right);
现在,在第一种方法中,您正在使用LINQ对可枚举数进行排序。当您调用.OrderBy()
时,实际上是在构建OrderedEnumerable<T>
对象。仅仅调用OrderBy
并不能对列表进行排序。仅在被调用的GetEnumerator
方法枚举时才对其进行排序。当您调用GetEnumerator
或使用ToList
之类的枚举枚举时,foreach
在幕后被隐式调用。
实际上,您对列表进行了两次排序,因为您在此行上对列表进行了一次枚举:
var list = Enumerable.Range(0, 20*1024*1024).OrderBy(x => r.Next()).ToList();
,当您通过foreach
在此行枚举时:
var lsitCopy = list.OrderBy(x => x);
foreach(var v in lsitCopy)
由于这些LINQ方法未使用本机代码,因此它们依赖垃圾回收器来处理。每个类也都创建了一堆对象(例如OrderedEnumerable
用数组的另一个副本创建Buffer<TElement>
)。所有这些对象都消耗RAM。
答案 1 :(得分:0)
我不得不对此做一些研究,并且发现了一些有趣的信息。默认的List.Sort函数执行就地排序(不是第二个副本),但是通过调用Array.Sort来执行某些操作,而Array.Sort最终调用TrySZSort,TrySZSort是经过高度优化的本机非托管CLR函数,用于选择特定的排序该算法基于输入类型,但在大多数情况下执行所谓的自省排序,该合并结合了QuickSort,HeapSort和InsertSort的最佳用例,以实现最大效率。这是在非托管代码中完成的,这意味着它通常更快,更高效。
如果您对钻进兔子洞感兴趣,则Array Sort源为here,而TrySZSort实现为here。但最终,使用非托管代码意味着不参与垃圾收集器,因此使用的内存更少。
OrderBy使用的实现是标准的Quicksort,而OrderedEnumerable实际上创建了排序中使用的键的第二个副本(在您的情况下为唯一字段,但是如果您认为具有单个属性或两个属性的较大类对象(用作排序器),这将导致您所观察到的完全一样,这是额外的用法,等于第二个副本的集合大小。假设您随后将其输入到列表或数组(而不是OrderedEnumerable)中,然后等待或强制进行垃圾回收,则应恢复该内存的大部分。如果您想深入研究Enumerable.OrderBy方法的源,则为here。
答案 2 :(得分:0)
在行上创建的OrderedEnumerable的实现中可以找到使用的额外内存的源
IOrderedEnumerable<int> lsitCopy = list.OrderBy(x => x);
OrderedEnumerable是一个通用实现,可以根据您提供的任何条件对其进行排序,这与List.Sort的实现(仅按值对元素进行排序)明显不同。如果遵循OrderedEnumerable的编码,则会发现它会创建一个buffer,将您的值复制到其中,从而占用了额外的80MB(4 * 20 * 1024 * 1024)内存。额外的40MB(2 * 20 * 1024 * 1024)与structures created关联,以按键对列表进行排序。
要注意的另一件事不仅是OrderBy(x => x)会导致更多的内存使用,它还使用了更多的处理能力,根据我的测试调用Sort比使用OrderBy(x => x快6倍左右)。
List.Sort()方法由本机实现高度优化的方法支持,该方法用于按元素的值对元素进行排序,而Linq OrderBy方法则用途更广,因此对于仅按值对列表进行排序的优化程度较低... < / p>
IOrderedEnumerable<TSource> OrderBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
P.S我建议您停止使用var而不是实际的变量类型,因为它向代码读者隐藏了有关代码实际功能的宝贵信息。我建议开发人员仅将var关键字用于anonymous types
答案 3 :(得分:0)
康纳答案提供了一个线索,说明这里发生了什么。 OrderedEnumerable的实现使其更加清晰。 OrderedEnumerable的GetEnumerator是
public IEnumerator<TElement> GetEnumerator() {
Buffer<TElement> buffer = new Buffer<TElement>(source);
if (buffer.count > 0) {
EnumerableSorter<TElement> sorter = GetEnumerableSorter(null);
int[] map = sorter.Sort(buffer.items, buffer.count);
sorter = null;
for (int i = 0; i < buffer.count; i++) yield return buffer.items[map[i]];
}
}
缓冲区是原始数据的另一个副本。 Map保留订单的映射。因此,如果代码是
// memory_foot_print_1
var sortedList = originalList.OrderBy(v=>v)
foreach(var v in sortedList)
{
// memory_foot_print_2
...
}
在这里,memory_foot_print_2等于memory_foot_print_1 + size_of(originalList)+ size_of(new int [count_of(originalList)])(假设没有GC)
因此,如果originalList是大小为80Mb的整数的列表,则memory_foot_print_2-memory_foot_print_1 = 80 + 80 = 160Mb。并且如果originalList是大小为80Mb的日志列表,则我正在观察的是memory_foot_print_2-memory_foot_print_1 = 80+ 40(地图大小)= 120Mb(假设int-4个字节,longs- 8个字节)。
这导致另一个问题,对于较大的对象使用OrderBy是否有意义。