C# [优化此]:减慢LINQ到对象查询
我有一个问题困扰着我;它被封装为一个新的查询操作符,我制作了两个版本,试图看看哪一个性能更好。两者的表现都很糟糕 第一次尝试;陈述式风格C# [优化此]:减慢LINQ到对象查询,c#,linq,performance,optimization,linq-to-objects,C#,Linq,Performance,Optimization,Linq To Objects,我有一个问题困扰着我;它被封装为一个新的查询操作符,我制作了两个版本,试图看看哪一个性能更好。两者的表现都很糟糕 第一次尝试;陈述式风格 public static IEnumerable<IEnumerable<α>> Section<α>(this IEnumerable<α> source, int length) { return source.Any() ? source.Take(length).Cons(sou
public static IEnumerable<IEnumerable<α>> Section<α>(this IEnumerable<α> source, int length)
{
return source.Any()
? source.Take(length).Cons(source.Skip(length).Section(length))
: Enumerable.Empty<IEnumerable<α>>();
}
公共静态IEnumerable部分(此IEnumerable源代码,int-length)
{
返回source.Any()
?源、取(长度)、反(源、跳(长度)、段(长度))
:Enumerable.Empty();
}
第二次尝试:命令式“收益回报”风格
公共静态IEnumerable部分(此IEnumerable源代码,int-length)
{
var fst=源。获取(长度);
var rst=源。跳过(长度);
收益率;
if(rst.Any())
foreach(第一节中的变截面(长度))
收益率回归段;
}
事实上,第二次尝试在可读性、合成性和速度方面都更差
有没有关于如何优化这个的线索?只要有可能,我会尝试在操作符中只对源进行一次迭代。如果源代码类似于
Reverse()
操作符的结果,则调用Any
,Take
和Skip
可能会导致很多糟糕的性能
操作员想做什么还不完全清楚,但如果您不需要多次阅读源代码就可以做到,这可能会很有帮助,尽管这在很大程度上取决于输入的内容。如果我正确理解您的问题,您正在尝试构建一个惰性的枚举器实现,该枚举器将较大的项集合拆分为较小的可枚举项集合 例如,一个一百万个数字的序列可以分成“部分”,每个部分只产生100个数字,而你希望所有的数字都是惰性的,即在产生之前不要收集100个项目到一个列表中 首先,您的尝试将在集合上重复多次,这是不好的,因此会出现性能问题 如果你想建立一个纯懒的实现,你应该考虑以下问题:
- 您只想在基础集合上迭代一次
- 您应该返回重复使用基础枚举数的枚举数
- 您需要处理返回的部分没有完全枚举(例如,调用代码只需要这100项中的前50项)
编辑:在讨论我过于简单的解决方案之前,这里有一些注意事项:
- 您无法保存每个节以备以后使用,即无法执行以下操作:
以获取节数组collection.Sequence(10).ToArray()
- 不能对每个部分进行多次枚举,因为这样做会更改隐藏的底层数据结构
这是一个过于简单的实现,我很确定这里存在bug,所以您可能想看看如何为EdgeCase实现大量的单元测试:
using System;
using System.Collections.Generic;
using System.Linq;
namespace ConsoleApplication20
{
class SectionEnumerable<T> : IEnumerable<T>
{
private readonly IEnumerator<T> _Enumerator;
public SectionEnumerable(IEnumerator<T> enumerator, int sectionSize)
{
_Enumerator = enumerator;
Left = sectionSize;
}
public IEnumerator<T> GetEnumerator()
{
while (Left > 0)
{
Left--;
yield return _Enumerator.Current;
if (Left > 0)
if (!_Enumerator.MoveNext())
break;
}
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public int Left { get; private set; }
}
static class SequenceExtensions
{
public static IEnumerable<IEnumerable<T>> Section<T>(this IEnumerable<T> collection, int sectionSize)
{
if (collection == null)
throw new ArgumentNullException("collection");
if (sectionSize < 1)
throw new ArgumentOutOfRangeException("sectionSize");
using (IEnumerator<T> enumerator = collection.GetEnumerator())
{
while (enumerator.MoveNext())
{
SectionEnumerable<T> enumerable = new SectionEnumerable<T>(enumerator, sectionSize);
yield return enumerable;
for (int index = 0; index < enumerable.Left; index++)
if (!enumerator.MoveNext())
yield break;
}
}
}
}
class Program
{
static void Main(string[] args)
{
var sequence = Enumerable.Range(0, 100);
var sections = sequence.Section(10);
foreach (var section in sections)
{
Console.WriteLine(
String.Join(", ",
section.Take(5).ToArray().Select(i => i.ToString()).ToArray()));
}
Console.ReadLine();
}
}
}
您应该进行单元测试的内容:
- 空输入集合不生成节
- 一个元素数量刚好合适的集合只生成一个节
- 包含多个节大小元素(即10、20、30等,节大小为5或10的元素数)的集合不会在所有预期的元素之后生成空节
- 它实际上是惰性的,如果在前10个元素的部分上进行枚举,但只在第二个部分的前5个元素上进行枚举,则只在基础集合的前15个元素上进行枚举
public static IEnumerable<IEnumerable<T>> Section<T>(
this IEnumerable<T> source, int length)
{
return source
.Select((x, i) => new { Value = x, Group = i / length })
.GroupBy(x => x.Group, y => y.Value);
}
公共静态IEnumerable节(
此IEnumerable源(整数长度)
{
返回源
.Select((x,i)=>new{Value=x,Group=i/length})
.GroupBy(x=>x.Group,y=>y.Value);
}
还有一种不使用linq的方法,比第二种方法快得多:
public static IEnumerable<IEnumerable<a>> Section<a>(this IEnumerable<a> source, int length)
{
var enumerator = source.GetEnumerator();
var continueLoop = true;
do
{
var list = new List<a>();
var index = 0;
for (int i = 0; i < length; i++)
{
if (enumerator.MoveNext())
{
list.Add(enumerator.Current);
index++;
}
else
{
continueLoop = false;
break;
}
}
if (list.Count > 0)
{
yield return list;
}
} while (continueLoop);
}
公共静态IEnumerable部分(此IEnumerable源代码,int-length)
{
var枚举器=source.GetEnumerator();
var continueLoop=true;
做
{
var list=新列表();
var指数=0;
for(int i=0;i0)
{
收益回报表;
}
}while(continueLoop);
}
我怀疑您遇到的问题与以下事实有关:枚举最终结果至少是一个O(n^2)操作,可能更糟;我脑子里还没想清楚
为什么呢?好吧,假设你有[1,2,3,4,5,6],你把它分解成你认为的{{1,2},{3,4},{5,6}
你不是这么做的。事实上,你已经把它分成{取前两个,取前两个并丢弃它们,然后取下下两个,取前两个并丢弃,然后取下两个并丢弃它们,然后取下第三个两个}
注意每一步是如何重新计算结果的?这是因为数组可能在调用枚举之间发生变化。LINQ的设计宗旨是让您随时了解最新的结果;您编写的查询
public static IEnumerable<IEnumerable<T>> Section<T>(
this IEnumerable<T> source, int length)
{
return source
.Select((x, i) => new { Value = x, Group = i / length })
.GroupBy(x => x.Group, y => y.Value);
}
public static IEnumerable<IEnumerable<a>> Section<a>(this IEnumerable<a> source, int length)
{
var enumerator = source.GetEnumerator();
var continueLoop = true;
do
{
var list = new List<a>();
var index = 0;
for (int i = 0; i < length; i++)
{
if (enumerator.MoveNext())
{
list.Add(enumerator.Current);
index++;
}
else
{
continueLoop = false;
break;
}
}
if (list.Count > 0)
{
yield return list;
}
} while (continueLoop);
}
public static IEnumerable<α> Take<α>(this IEnumerator<α> iterator, int count)
{
for (var i = 0; i < count && iterator.MoveNext(); i++)
yield return iterator.Current;
}
public static IEnumerable<IEnumerable<α>> Section<α>(this IEnumerator<α> iterator, int length)
{
var sct = Enumerable.Empty<α>();
do
{
sct = iterator.Take(length).ToArray();
if (sct.Any())
yield return sct;
}
while (sct.Any());
}
public static IEnumerable<IEnumerable<α>> Section<α>(this IEnumerable<α> source, int length)
{
using (var iterator = source.GetEnumerator())
foreach (var e in iterator.Section(length))
yield return e;
}
public static class IEnumerableExtensions
{
public static IEnumerable<List<T>> InSetsOf<T>(this IEnumerable<T> source, int max)
{
List<T> toReturn = new List<T>();
foreach(var item in source)
{
toReturn.Add(item);
if (toReturn.Count == max)
{
yield return toReturn;
toReturn = new List<T>();
}
}
if (toReturn.Any())
{
yield return toReturn;
}
}
}
[TestFixture]
public class When_asked_to_return_items_in_sets
{
[Test]
public void Should_return_the_correct_number_of_sets_if_the_input_contains_a_multiple_of_the_setSize()
{
List<string> input = "abcdefghij".Select(x => x.ToString()).ToList();
var result = input.InSetsOf(5);
result.Count().ShouldBeEqualTo(2);
result.First().Count.ShouldBeEqualTo(5);
result.Last().Count.ShouldBeEqualTo(5);
}
[Test]
public void Should_separate_the_input_into_sets_of_size_requested()
{
List<string> input = "abcdefghijklm".Select(x => x.ToString()).ToList();
var result = input.InSetsOf(5);
result.Count().ShouldBeEqualTo(3);
result.First().Count.ShouldBeEqualTo(5);
result.Last().Count.ShouldBeEqualTo(3);
}
}