C# 为什么Linq的速度要慢得多?

C# 为什么Linq的速度要慢得多?,c#,performance,linq,C#,Performance,Linq,我试着用Linq和非Linq方法做同样的事情,发现Linq的速度要慢很多(~3000x) 为什么呢 林奇道: for (int i = 0; i < totalElements; i += stepSize) { var currentBlock = testList .Skip(i) .Take(stepSize); result.Add(currentBlock.Sum()); } result.ToList(); for(int

我试着用Linq和非Linq方法做同样的事情,发现Linq的速度要慢很多(~3000x)

为什么呢

林奇道:

for (int i = 0; i < totalElements; i += stepSize)
{
    var currentBlock = testList
        .Skip(i)
        .Take(stepSize);

    result.Add(currentBlock.Sum());
}

result.ToList();
for(int i=0;i
非Linq方式:

for (int i = 0; i < totalElements; i += stepSize)
{
    var currentBlock = testList.GetRange(i, stepSize);

    result2.Add(currentBlock.Sum());
}

result2.ToList();
for(int i=0;i
结果:

  • 方法:Linq,时间:26667ms,元素:1000000,步长:100

  • 方法:GetRange,所用时间:9毫秒,元素:1000000,步长:100

按要求提供完整的源代码:

static void Main(string[] args)
{
    var totalElements = 1000000;
    var testList = new List<int>(totalElements);
    var rand = new Random();

    // Initialize the list to random integers between 1 and 1000
    for (int i = 0; i < totalElements; i++)
    {
        testList.Add(rand.Next(1, 1000));
    }

    var result = new List<int>();
    var stepSize = 100;
    var stp = new Stopwatch();

    stp.Start();
    for (int i = 0; i < totalElements; i += stepSize)
    {
        var currentBlock = testList
            .Skip(i)
            .Take(stepSize);

        result.Add(currentBlock.Sum());
    }

    result.ToList();
    stp.Stop();

    Console.WriteLine($"Method: Linq, Time taken: {stp.ElapsedMilliseconds} ms, Elements: {totalElements}, Step Size: {stepSize}");

    stp.Reset();

    var result2 = new List<int>();
    stp.Start();

    for (int i = 0; i < totalElements; i += stepSize)
    {
        var currentBlock = testList.GetRange(i, stepSize);

        result2.Add(currentBlock.Sum());
    }

    result2.ToList();
    stp.Stop();

    Console.WriteLine($"Method: GetRange, Time taken: {stp.ElapsedMilliseconds} ms, Elements: {totalElements}, Step Size: {stepSize}");
}
static void Main(字符串[]args)
{
var totalElements=1000000;
var testList=新列表(totalElements);
var rand=new Random();
//将列表初始化为1到1000之间的随机整数
对于(int i=0;i
问题在于
跳过
的工作原理,这与
GetRange
截然不同<代码>跳过
始终从枚举开始,这意味着您正在执行以下操作:

Iteration #1: Skip 0
Iteration #2: Skip 1 * step
Iteration #3: Skip 2 * step
Iteration #4: Skip 3 * step
Iteration #5: Skip 4 * step
....
Iteration #1.000: Skip 9.999 * step
如果您对1.000.000个元素进行数学运算,并在
100
中执行
步骤,您将得到:

sum = 1 + 2 + 3 + .... + 9.999 = 9.999 * (9.999 + 1) / 2 = 49.995.000
total elements skipped: 49.995.000 * 100 = 4.999.500.000
因此,您的Linq版本有一个惊人的
4.999.500.000
不必要的迭代


这里的一个好问题是:
Skip
为什么没有针对
source
实现
IList
的情况进行优化,因为很明显,这是可能的。

问题是
Skip
如何工作,这与
GetRange
有根本不同<代码>跳过
始终从枚举开始,这意味着您正在执行以下操作:

Iteration #1: Skip 0
Iteration #2: Skip 1 * step
Iteration #3: Skip 2 * step
Iteration #4: Skip 3 * step
Iteration #5: Skip 4 * step
....
Iteration #1.000: Skip 9.999 * step
如果您对1.000.000个元素进行数学运算,并在
100
中执行
步骤,您将得到:

sum = 1 + 2 + 3 + .... + 9.999 = 9.999 * (9.999 + 1) / 2 = 49.995.000
total elements skipped: 49.995.000 * 100 = 4.999.500.000
因此,您的Linq版本有一个惊人的
4.999.500.000
不必要的迭代

这里的一个好问题是:
Skip
为什么没有针对
source
实现
IList
的情况进行优化,因为很明显,这是可能的。

GetRange使用Skip()。它总是从一开始就开始枚举。您想要的是一个函数,它可以将您的序列划分为块,而不需要对序列进行超出实际需要的迭代

这意味着,如果您只需要第一个块,那么函数的迭代次数不应该超过这个块,如果我需要第九个块之后的第十个块,那么它就不应该开始迭代

这个扩展功能怎么样

public static IEnumerable<IEnumerable<Tsource>> ToChuncks<TSource>(
    this IEnumerable<TSource> source, int chunkSize)
{
    while (source.Any())                 // while there are elements left
    {   // still something to chunk
        // yield return a chunk
        yield return source.Take(chunkSize); // return a chunk of chunkSize

        // remove the chunk from the source
        source = source.Skip(chunkSize);     // skip the returned chunk
    }
}
公共静态IEnumerable ToChuncks(
此IEnumerable源,int chunkSize)
{
while(source.Any())//还有元素时
{//还是有东西要拼吗
//返回一大块
yield返回source.Take(chunkSize);//返回chunkSize的块
//从源代码中删除块
source=source.Skip(chunkSize);//跳过返回的块
}
}
此函数重复检查源序列中是否还有剩余内容。如果是这样,它将返回一个数据块并从源代码中删除该数据块

这样,整个源代码最多迭代两次:一次是迭代块中的元素,一次是迭代块。

GetRange使用Skip()。它总是从一开始就开始枚举。您想要的是一个函数,它可以将您的序列划分为块,而不需要对序列进行超出实际需要的迭代

这意味着,如果您只需要第一个块,那么函数的迭代次数不应该超过这个块,如果我需要第九个块之后的第十个块,那么它就不应该开始迭代

这个扩展功能怎么样

public static IEnumerable<IEnumerable<Tsource>> ToChuncks<TSource>(
    this IEnumerable<TSource> source, int chunkSize)
{
    while (source.Any())                 // while there are elements left
    {   // still something to chunk
        // yield return a chunk
        yield return source.Take(chunkSize); // return a chunk of chunkSize

        // remove the chunk from the source
        source = source.Skip(chunkSize);     // skip the returned chunk
    }
}
公共静态IEnumerable ToChuncks(
此IEnumerable源,int chunkSize)
{
while(source.Any())//还有元素时
{//还是有东西要拼吗
//返回一大块
yield返回source.Take(chunkSize);//返回chunkSize的块
//从源代码中删除块
source=source.Skip(chunkSize);//跳过返回的块
}
}
此函数重复检查源序列中是否还有剩余内容。如果是这样,它将返回一个数据块并从源代码中删除该数据块


这样,您的完整源代码最多迭代两次:一次是迭代块中的元素,另一次是迭代块。

仅用很小的片段很难判断或进一步研究。请提供答案。我不认为这是一般“linq性能”问题的重复。26秒对9毫秒是极端的。有些奇怪的事情正在发生,可能是在我们看不到的代码中。@JonSkeet我已经添加了完整的源代码