提高LINQ性能

7

我有一个像这样的 Linq 语句:

var records = from line in myfile 
              let data = line.Split(',')
              select new { a=int.Parse(data[0]), b=int.Parse(data[1]) };
var average = records.Sum(r => r.b)!=0?records.Sum(r => r.a) / records.Sum(r => r.b):0;

我的问题是: 在最后一行中,records.Sum(r => r.b)计算了多少次? 当需要计算总和时(在这种情况下,有3个Sum()),LINQ是否每次都要循环所有记录(因此循环3次)?还是聪明地只循环一次并计算所有总和?
编辑1:
1.我想知道是否有办法通过仅一次遍历所有记录来改进它(因为我们只需要在单个循环中执行它时使用普通的for循环)? 2.真的没有必要在我们可以进行求和和平均值之前将所有内容都加载到内存中。当从文件中加载每个元素时,我们肯定可以对每个元素进行求和。是否有任何方法也可以减少内存消耗?
编辑2: 只是为了澄清一下,我以前没有使用过LINQ。使用普通的while / for循环可以实现所有性能要求。但是我尝试通过使用LINQ来提高可读性并减少代码行数。似乎我们无法同时获得两者。

1
@AndyWiesendanger,查询似乎没有针对数据库执行... - Thomas Levesque
2
它将执行三次(如果sum == 0,则执行一次),无论是否针对数据库。 - Rob
6个回答

9

将Twice写成这样,它就变成了一次:

var sum = records.Sum(r => r.b);

var avarage = sum != 0 ? records.Sum(r => r.a)/sum: 0;

1
你正在进行两次迭代,而不是一次。请使用“Aggregate”。 - flindeberg
4
嘿,两次总比三次好,对吧? - moarboilerplate

6

有很多答案,但没有一个能包含您所有的问题。

最后一行中records.Sum(r => r.b)被计算了几次?

三次。

LINQ在需要计算总和时(在本例中为3 Sum()),是否每次都遍历所有记录(因此循环3次)?

是的。

还是它聪明地只遍历一次所有记录并计算所有总和?

不是的。

我想知道是否有任何方法可以通过仅在单个循环中使用普通for循环来仅一次浏览所有记录(因为我们只需要这样做)?

您可以这样做,但需要您及早加载所有数据,这与您的下一个问题相矛盾。

在我们可以进行求和和平均值之前,真的没有必要将所有内容加载到内存中。当从文件加载时,我们当然可以在加载每个元素时对其进行求和。是否有任何方法可以降低内存消耗?

没错。在您的原始帖子中,您有一个名为myFile的变量,并且您正在遍历它并将其放入名为line的本地变量中(即基本上是foreach)。由于您没有显示如何获取myFile数据,因此我假设您正在急切地加载所有数据。

这里是懒加载数据的快速示例:

public IEnumerable<string> GetData()
{
    using (var fileStream = File.OpenRead(@"C:\Temp\MyData.txt"))
    {
        using (var streamReader = new StreamReader(fileStream))
        {
            string line;
            while ((line = streamReader.ReadLine()) != null)
            {                       
                yield return line;
            }
        }
    }
}

public void CalculateSumAndAverage()
{
    var sumA = 0;
    var sumB = 0;
    var average = 0;

    foreach (var line in GetData())
    {
        var split = line.Split(',');
        var a = Convert.ToInt32(split[0]);
        var b = Convert.ToInt32(split[1]);

        sumA += a;
        sumB += b;
    }

    // I'm not a big fan of ternary operators,
    // but feel free to convert this if you so desire.
    if (sumB != 0)
    {
        average = sumA / sumB;
    }
    else 
    {
        // This else clause is redundant, but I converted it from a ternary operator.
        average = 0;
    }
}

@james 这只是一个业务逻辑错误,而不是语法错误。不过,我会进行修正。 - Cameron
谢谢,@Cameron。这实际上是我一开始尝试的方法...然后我尝试使用LINQ来提高可读性并减少代码行数。看来我们不能同时得到两者。 - james
同意。我只是觉得这应该是一个常见的问题,鉴于LINQ是一个非常成熟的工具,所以我期望性能改进也可以使用LINQ来完成。我认为Pedro Mora的解决方案非常接近。 - james
我会接受这个答案,因为它回答了我大部分的问题。对于那些有兴趣使用Linq解决方案并且仍然保持相同性能的人,请参考Pedro Mora的解决方案。 - james
@CodesInChaos,同意。File.ReadLines是惰性加载,但在实践中,我仍然倾向于使用StreamReader,因为它可以允许共享访问,例如Excel打开的文件无法被ReadLines读取... - james
显示剩余4条评论

4

这里需要使用Aggregate而不是Sum,需要进行三次操作。

// do your original selection
var records = from line in myfile 
              let data = line.Split(',')
              select new { a=int.Parse(data[0]), b=int.Parse(data[1]) };
// aggregate them into one record
var sumRec = records.Aggregate((runningSum, next) =>
          { 
            runningSum.a += next.a;
            runningSum.b += next.b;                
            return runningSum;
          });
// Calculate your average
var average = sumRec.b != 0 ? sumRec.a / sumRec.b : 0;

谢谢,@flindeberg。runningSum不是只读的吗?它怎么能被赋值? - james
@james 是正确的。匿名类型的属性是只读的。你要么使用本地变量,要么创建一个强类型。否则,这是最佳答案。 - Cameron
这个解决方案看起来很高效,但我想知道它是不是在执行聚合之前将所有内容都加载到记录中?也就是说,在我们进行求和和平均值之前真的没有必要将所有记录加载到内存中。 - james
@james 这要看情况... 参考我的回答。 - Cameron
@Cameron 完全忽略了匿名类型的只读效果,随意回答了 =/ - flindeberg
@james 正如Cameron所说,这取决于您如何将其加载到myfile中。 - flindeberg

2

每次调用Sum方法都会遍历myfile中的所有行。为了提高性能,请按照以下方式编写:

var records = (from line in myfile 
          let data = line.Split(',')
          select new { a=int.Parse(data[0]), b=int.Parse(data[1]) }).ToList();

因此,它将创建包含所有元素(具有“a”和“b”属性)的列表,然后每次调用Sum方法都将通过此列表进行迭代,而无需拆分和解析数据。 当然,您可以进一步记住Sum方法的结果在某些临时变量中。


为了提高性能,请编写以下代码:如果这是针对数据库执行的,那么这将极大地降低性能。根据可枚举对象的实现方式,在完全内存中可能会更慢。添加 ToList 表示您将所有行加载到内存中,而您只关心 Sum。此外,我认为在数据库中运行时,Sum 比针对 List<> 更有效率。 - Rob
1
但是这个查询似乎是针对文件的!因此,它将提高性能,因为没有可能针对文件内容调用 T-SQL 查询。 - Marcin Iwanowski
你说得对 - 我错了,我已经取消了踩的操作。虽然这样仍然会计算两次“Sum”,但这绝对比原来的代码要好。 - Rob

1

james,我并不是专家,但这是我的想法。我认为它可以被简化为1。也许还有一点代码。记录仍然是一个AnonymousType {int a,int b}的IEnumerable。

*Dynamic是解决它的快速方法。你应该为它编写一个结构。

int sum_a = 0,sum_b = 0;
Func<string[], dynamic> b = (string[] data) => { 
    sum_a += int.Parse(data[0]); 
    sum_b += int.Parse(data[1]);
    return new {a = int.Parse(data[0]),b = int.Parse(data[0]) }; 
};
var records = from line in fileLines 
              let data = line.Split(',')
              let result = b(data)
              select new { a = (int)result.a, b = (int)result.b };
var average = sum_b != 0 ? sum_a / sum_b : 0;

对于其他结构,这很简单。
public struct Int_Int //May be a class or interface for mapping
{
    public int a = 0, b = 0;        
}

那么

int sum_a = 0,sum_b = 0;    
Func<string[], Int_Int> b = (string[] data) => { 
    sum_a += int.Parse(data[0]); 
    sum_b += int.Parse(data[1]);
    return new Int_Int() { a = int.Parse(data[0]), b = int.Parse(data[0]) }; 
};
var records = from line in fileLines
              let data = line.Split(',')
              select b(data);
var average = sum_b != 0 ? sum_a / sum_b : 0;

非常感谢,@Pedro。尽管似乎没有很多人对您的答案进行了投票,但我已经为其点赞。与所有其他答案相比,我认为这是目前最有效的解决方案。它不需要在执行求和之前将所有内容加载到内存中,而且仅对所有记录循环一次! - james
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Pedro Mora

0

SUM 在每次调用时都会获取所有记录,我建议您使用 ToList() --> 你使用过 ToList() 吗?

var records = from line in myfile 
              let data = line.Split(',')
              select new { a=int.Parse(data[0]), b=int.Parse(data[1]) }.ToList();

var sumb = records.Sum(r => r.b);
var average = sumb !=0?records.Sum(r => r.a) / sumb :0;

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接