我经常遇到我想在我声明它的地方评估查询的情况 . 这通常是因为我需要多次迭代它并且计算起来很昂贵 . 例如:
string raw = "...";
var lines = (from l in raw.Split('\n')
let ll = l.Trim()
where !string.IsNullOrEmpty(ll)
select ll).ToList();
这很好用 . But 如果我不打算修改结果,那么我不妨调用 ToArray()
而不是 ToList()
.
我想知道 ToArray()
是否通过首先调用 ToList()
来实现,因此比调用 ToList()
更节省内存 .
我疯了吗?我应该只是打电话给 ToArray()
- 知道内存不会被分配两次是安全的吗?
15 回答
除非您只需要一个数组来满足其他约束,否则您应该使用
ToList
. 在大多数情况下,ToArray
将分配比ToList
更多的内存 .两者都使用数组进行存储,但
ToList
具有更灵活的约束 . 它需要数组至少与集合中元素的数量一样大 . 如果数组较大,那不是问题 . 但是ToArray
需要将数组的大小精确地调整为元素的数量 .为了满足这个约束,
ToArray
经常比ToList
多做一次分配 . 一旦它有一个足够大的数组,它就会分配一个完全正确大小的数组,并将元素复制回该数组 . 唯一能避免这种情况的是当数组的增长算法恰好与需要存储的元素数量一致时(绝对是少数) .EDIT
有几个人问我在
List<T>
值中有多余的未使用内存的后果 .这是一个有效的问题 . 如果创建的集合是长期存在的,在创建之后永远不会被修改并且很有可能在Gen2堆中登陆,那么你可能最好在前面额外分配
ToArray
.总的来说,虽然我发现这是罕见的情况 . 更常见的是看到很多
ToArray
调用会立即传递给内存的其他短暂使用,在这种情况下ToList
明显更好 .这里的关键是剖析,剖析,然后再详细介绍一些 .
性能差异无关紧要,因为
List<T>
实现为动态大小的数组 . 调用ToArray()
(使用内部Buffer<T>
类来增长数组)或ToList()
(调用List<T>(IEnumerable<T>)
构造函数)将最终成为将它们放入数组并增长数组直到它适合所有数组的问题 .如果您希望具体确认这一事实,请查看Reflector中相关方法的实现 - 您将看到它们归结为几乎完全相同的代码 .
(七年后......)
其他(好)答案集中在将发生的微观性能差异上 .
这篇文章只是一个补充,提到 semantic difference 存在于数组(
T[]
)产生的IEnumerator<T>
与List<T>
返回的数据之间 .通过示例最佳说明:
上面的代码将运行,没有异常并产生输出:
这表明
int[]
返回的IEnumarator<int>
无法跟踪自创建枚举器以来阵列是否已被修改 .请注意,我将局部变量
source
声明为IList<int>
. 通过这种方式,我确保C#编译器不会将foreach
语句优化为等同于for (var idx = 0; idx < source.Length; idx++) { /* ... */ }
循环的内容 . 如果我使用var source = ...;
,这就是C#编译器可能会做的事情 . 在我当前版本的.NET框架中,这里使用的实际枚举器是非公共引用类型System.SZArrayHelper+SZGenericArrayEnumerator
1[System.Int32]` ,但当然这是一个实现细节 .现在,如果我将
.ToArray()
更改为.ToList()
,我只会:随后是 System.InvalidOperationException 爆炸说:
在这种情况下,底层枚举器是public mutable value-type [System.Collections.Generic.List
1+Enumerator[System.Int32]](https://msdn.microsoft.com/en-us/library/x854yt9s.aspx)(在这种情况下,在
IEnumerator<int>框内装箱,因为我使用
IList<int>` ) .In conclusion, 由
List<T>
生成的枚举器会跟踪列表在枚举期间是否更改,而T[]
生成的枚举器则不会 . 因此,在.ToList()
和.ToArray()
之间进行选择时,请考虑这种差异 .人们经常添加一个额外的
.ToArray()
或.ToList()
来绕过一个集合跟踪调查员在整个生命周期内是否被修改过 .(如果有人想知道
List<>
如何跟踪集合是否被修改,则此类中有一个私有字段_version
,每次更新List<>
时都会更改 . )我同意@mquander的观点,即性能差异应该是微不足道的 . 但是,我想确定它的基准,所以我做了 - 而且它是微不足道的 .
每个源数组/ List都有1000个元素 . 所以你可以看到时间和内存的差异可以忽略不计 .
我的结论是:你不妨使用 ToList() ,因为
List<T>
提供的功能比数组更多,除非几个字节的内存对你很重要 .内存将始终分配两次 - 或接近该内存 . 由于无法调整数组大小,因此两种方法都将使用某种机制来收集不断增长的集合中的数据 . (好吧,List是一个不断增长的集合 . )
List使用数组作为内部存储,并在需要时将容量加倍 . 这意味着平均2/3的项目至少重新分配一次,其中一半重新分配至少两次,其中一半重新分配至少三次,依此类推 . 这意味着每个项目平均重新分配1.3次,这不是很大的开销 .
还要记住,如果要收集字符串,集合本身只包含对字符串的引用,字符串本身不会重新分配 .
如果您在
IEnumerable<T>
(例如来自ORM)上使用它,通常首选ToList()
. 如果开头不知道序列的长度,ToArray()
会创建像List这样的动态长度集合,然后将其转换为数组,这需要额外的时间 .Edit :此答案的最后部分无效 . 但是,其余的仍然是有用的信息,所以我会留下它 .
我知道这是一个老帖子,但在有了同样的问题并做了一些研究后,我发现了一些可能值得分享的有趣内容 .
首先,我同意@mquander和他的回答 . 他说性能方面是正确的,两者是相同的 .
但是,我一直在使用Reflector来查看
System.Linq.Enumerable
扩展名称空间中的方法,我注意到了一个非常常见的优化 .只要有可能,
IEnumerable<T>
源将转换为IList<T>
或ICollection<T>
以优化方法 . 例如,请查看ElementAt(int)
.有趣的是,微软选择仅针对
IList<T>
进行优化,而不是IList
. 看起来微软更喜欢使用IList<T>
界面 .System.Array仅实现IList,因此它不会受益于任何这些扩展优化 . 因此,我认为最佳做法是使用.ToList()方法 . 如果您使用任何扩展方法,或将列表传递给另一个方法,则可能会针对IList <T>进行优化 .
根据理想的设计选择,您应该根据
ToList
或ToArray
做出决定 . 如果您想要一个只能通过索引进行迭代和访问的集合,请选择ToArray
. 如果你想在以后添加和删除集合的其他功能没有太多麻烦,那么做一个ToList
(不是真的你不能添加到一个数组,但这通常不适合它) .如果性能很重要,您还应该考虑哪些操作更快 . 实际上,你不会拨打
ToList
或ToArray
一百万次,但可能会对获得的收藏品进行一百万次的处理 . 在这方面[]
更好,因为List<>
是[]
并且有一些开销 . 请参阅此主题以进行效率比较:Which one is more efficient : List<int> or int[]在我不久前的测试中,我发现
ToArray
更快 . 而且我不确定测试有多么偏差 . 但性能差异是如此微不足道,只有当您在循环中运行这些查询数百万次时才能注意到这一点 .一个非常晚的答案,但我认为这将有助于谷歌 .
他们在使用linq创建时都很糟糕 . 它们都实现了相同的代码 resize buffer if necessary .
ToArray
内部使用一个类将IEnumerable<>
转换为数组,方法是分配一个包含4个元素的数组 . 如果这还不够,可以通过创建一个新的数组,将当前大小加倍,并将当前数组复制到该数组,从而使其大小增加一倍 . 最后,它会分配一个新的项目数量 . 如果您的查询返回129元素然后ToArray将进行6次分配和内存复制操作以创建256个元素数组,而不是另一个129要返回的数组 . 对于记忆效率这么多 .ToList执行相同的操作,但它会跳过最后一次分配,因为您可以在将来添加项目 . 列表不关心它是从linq查询创建还是手动创建 .
创建列表更好的内存,但更糟糕的是cpu,因为列表是一个通用的解决方案,每个操作都需要范围检查,除了.net的内部范围检查数组 .
因此,如果您将遍历结果集太多次,那么数组是好的,因为它意味着比列表更少的范围检查,并且编译器通常优化数组以进行顺序访问 .
如果在创建容量参数时指定容量参数,则列表的初始化分配会更好 . 在这种情况下,假设您知道结果大小,它将仅分配一次数组 . linq的
ToList
没有指定提供它的重载,因此我们必须创建扩展方法,创建具有给定容量的列表,然后使用List<>.AddRange
.要完成这个答案,我必须写下面的句子
最后,您可以使用ToArray或ToList,性能不会如此不同(请参阅@EMP的答案) .
您正在使用C# . 如果您需要性能,那么不要担心编写高性能代码,但担心不编写糟糕的性能代码 .
始终以x64为目标,以获得高性能代码 . AFAIK,x64 JIT基于C编译器,并做了一些有趣的事情,如尾递归优化 .
使用4.5,您还可以享受配置文件引导优化和多核JIT .
最后,您可以使用async / await模式更快地处理它 .
我发现人们在这里做的其他基准测试缺乏,所以这里是我的解决方案 . 如果您发现我的方法有问题,请告诉我 .
你可以download the LINQPad Script here .
结果:
调整上面的代码,你会发现:
dealing with smaller arrays时,差异不太明显 .
处理
int
而不是string
时,差异不太明显 .使用大
struct
而不是string
一般需要花费更多时间,但并没有真正改变比率 .这与最高投票答案的结论一致:
除非您的代码经常产生许多大型数据列表,否则您不太可能注意到性能差异 . (每个创建1000个100K字符串列表时只有200ms的差异 . )
ToList()
始终如一地运行得更快,如果您不打算长时间坚持使用结果,那将是更好的选择 .更新
@JonHanna指出,根据
Select
的实现,ToList()
或ToArray()
实现可以提前预测产生的集合大小 . 目前用Where(i => true)
yields very similar results替换上面代码中的.Select(i => i)
,无论.NET实现如何,更有可能这样做 .这是一个古老的问题 - 但是为了绊倒它的用户的利益,还有“Memoizing”Enumerable的替代方案 - 它具有缓存和停止Linq语句的多个枚举的效果,这就是ToArray()即使从不使用列表或数组的集合属性,也会使用ToList() .
Memoize在RX / System.Interactive lib中可用,并在此解释:More LINQ with System.Interactive
(来自Bart De'Smet's blog,如果您正在使用Linq对象很多,这是 highly 建议阅读)
一种选择是添加自己的扩展方法,返回 readonly
ICollection<T>
. 当您不想使用数组/列表的索引属性或从列表中添加/删除时,这可能比使用ToList
或ToArray
更好 .单元测试:
老问题,但新的提问者在任何时候 .
根据System.Linq.Enumerable的来源,
ToList
只返回new List(source),而ToArray
使用new Buffer<T>(source).ToArray()返回T[]
.在 IEnumerable<T> only 对象上运行时,
ToArray
会比ToList
再分配一次内存 . 但是在大多数情况下你不必关心它,因为GC会在需要时进行垃圾收集 .那些质疑这个问题的人可以运行以下代码你自己的机器,你会得到你的答案 .
我在我的机器上得到了这些结果:
由于stackoverflow对答案字符数量的限制,因此省略了Group2和Group3的样本列表 .
如您所见,在大多数情况下使用
ToList
或ToArry
并不重要 .在处理运行时计算的
IEnumerable<T>
对象时,如果计算带来的负载比ToList
和ToArray
的内存分配和复制操作重,则差异无关紧要(C.ToList vs C.ToArray
和S.ToList vs S.ToArray
) .只能在非运行时计算的
IEnumerable<T>
对象(C1.ToList vs C1.ToArray
和S1.ToList vs S1.ToArray
)上观察到差异 . 但绝对差异(<60ms)仍然可以接受一百万个小物件IEnumerable<T>
. 实际上,差异是由Enumerator<T>
的Enumerator<T>
的实施决定的 . 所以,如果你的程序真的非常敏感,你必须 profile, profile, profile !最后你可能会发现瓶颈不是ToList
或ToArray
,而是枚举器的细节 .并且,
C2.ToList vs C2.ToArray
和S2.ToList vs S2.ToArray
的结果表明,在非运行时计算的ICollection<T>
对象上,您实际上不需要关心ToList
或ToArray
.当然,这只是我机器上的结果,这些操作在不同机器上的实际花费时间会不一样,你可以使用上面的代码在你的机器上找到 .
您需要做出选择的唯一原因是,您对
List<T>
或T[]
有特定需求,如@Jeppe Stig Nielsen的答案所述 .对于有兴趣在另一个Linq-to-sql中使用此结果的人
那么无论是否为myListOrArray使用List或Array,生成的SQL都是相同的 . 现在我知道有些人可能会问为什么甚至在这个语句之前进行枚举,但是从IQueryable vs(List或Array)生成的SQL之间存在差异 .
ToListAsync<T>()
是首选 .在实体框架6中,两种方法最终都调用相同的内部方法,但
ToArrayAsync<T>()
最后调用list.ToArray()
,这实现为所以
ToArrayAsync<T>()
有一些开销,因此ToListAsync<T>()
是首选 .