在linq foreach中调用方法 - 有多少开销?

7

我打算用Linq替换很多内联的foreach,并在这样做的过程中创建新的方法,例如:

当前代码:

foreach(Item in List)
{
    Statement1
    Statement2
    Statement3
}

想法:

List.Foreach(Item => Method(Item))

很明显,Method() 包含 Statement1..3。

这是一个好习惯吗?还是调用方法数千次会降低性能?我的列表有10,000-100,000个元素。


4
除你之外,没有人知道你对性能降低的容忍度是多少。编写两种代码实现方式,分别测试它们的性能表现,然后你就会知道答案了。 - Eric Lippert
4个回答

12

嗯,首先你可以使用方法组转换使得ForEach语句更加高效。

List.ForEach(Method);

这样就减少了一级间接性。

但是,我个人认为这不是一个好主意。第一种方法更易读,并且可能表现得和第二种方式差不多。使用 List<T>.ForEach 的优势是什么?

Eric Lippert 在一篇精彩的博客文章中更详细地讨论了这个问题。如果你已经有一个要对每个元素执行的委托,那么我会使用List<T>.ForEach,但我不会为了这个目的引入委托和额外的方法。

就效率而言,我不指望会看到很大的区别。第一种形式可能表现得更好,因为它没有委托调用的间接性 - 但如果 ForEach 中的迭代循环利用了对 List<T> 的内部数据结构的访问权,则第二种形式可能更有效。我非常怀疑你会注意到任何一种方式。当然,如果你真的在意,你可以尝试进行测量。


6
也许你不知道,曾经有某人对枚举List<T>的各种方法进行了相当广泛的性能比较。希望这有所帮助。 :) http://msmvps.com/blogs/jon_skeet/archive/2006/01/20/foreachperf.aspx - Ani
2
@Ani:天啊,我完全忘记了这件事。刚刚用.NET 4重新运行了代码,“language foreach”版本似乎有所改进。 - Jon Skeet
1
什么是“方法组转换”,它如何帮助? - Ian Ringrose
1
@Ian:这是从方法名称到委托的转换。Lambda表达式最终会创建一个额外的方法来调用“Method”。方法组转换允许像button.Click += ClickHandler;这样的操作。 - Jon Skeet
关于“方法组转换”,感谢您。我本以为C#编译器自己会优化它! - Ian Ringrose
更新了@JonSkeet的foreach性能博客文章链接:https://codeblog.jonskeet.uk/2006/01/20/foreachperf/ - Roald

2

如果你考虑更改的原因是循环体中的三个语句太复杂,那么我可能会使用普通的foreach,但重构循环体为一个方法:

foreach(var item in List) 
  Method(item);

如果正文中的代码不复杂,那么我同意Jon的观点,没有使用ForEach的好理由(它不会以任何方式使代码更易读/声明性)。
我通常不喜欢使用“类似于LINQ”的结构来在LINQ查询的末尾进行命令式处理。我认为使用foreach更清楚地表明你已经完成了数据查询,现在正在进行一些处理。

1

我完全同意Jon Skeet的回答。但是既然我们正在讨论ForEach的性能,我有一些额外的东西要补充到你的问题中。请注意,如果您的语句1~3与彼此无关,则:

foreach(Item in List) 
{ 
   DoSomething();
   DoAnotherThing(); 
   DoTheLastThing();
} 

上面的代码可能比下面的代码性能差:
foreach(Item in List) 
{ 
   DoSomething();
} 
foreach(Item in List) 
{ 
   DoAnotherThing(); 
} 
foreach(Item in List) 
{ 
   DoTheLastThing();
} 

后者的代码需要多循环两次,但性能更好的原因是,当它不断调用DoSomething()数千次时,一些必要的变量始终在CPU寄存器中处于“热”状态。访问这些变量的成本非常低。另一方面,如果它在调用DoSomthing()之后立即调用DoAnotherThing(),那么已经在CPU寄存器中的DoSomething()变量将会冷却下来。在下一个循环中,访问这些变量需要更高的成本。


我想知道在那个列表中需要有多少项才能从CPU寄存器“热”变量中获得好处。 - Paul C

0

我一直认为,你应该首先为可读性编写代码,因为编译器和CLR在优化方面做得非常出色。如果你发现通过基准测试,这段代码可以更快地执行,那么可以考虑其他选项。

例如,for循环比foreach()更快,因为它们使用数组偏移量,在CLR内部进行了优化。

但是,List.ForEach()肯定会执行foreach(),所以你只是把工作交给了另一个方法,而不是自己去做。

严格来说,引入更多的方法调用实际上会在第一次通过时减慢代码的速度,因为CLR将在调用方法时JIT编译方法,尽管对该方法的后续调用不会这样做。

因此,我的建议是坚持编写易读的代码,如果你能证明这是系统的瓶颈,再从那里开始。


1
我不确定你是从哪里得到了这样一个观点,即CLR在优化方面做得非常出色。优化并不容易,特别是对于像多线程和副作用(C#包括了这些)这样的问题,我的经验表明CLR在消除开销方面做得还不错,但是你不应该指望它能够实际上重新排列代码以获得更快的“等效”变体。编写可读性强的代码,因为性能往往并不重要,而不是因为你希望从优化器中期待奇迹。 - Eamon Nerbonne
我认为目标有两个方面:减少代码量以获得性能优势。我仍然会首先考虑可读性,无论是重构还是编写更多的代码行来帮助可维护性。但在尝试优化之前,请先证明它是瓶颈。 - Dominic Zukiewicz

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