什么时候用哪个?

时间:2011-05-19 12:09:20

标签: c# .net .net-4.0 parallel-processing iteration

编辑其他选项以及下面稍微扩展的问题。

考虑一个类体的这个人为的抽象例子。它演示了执行“for”迭代的四种不同方法。

private abstract class SomeClass
{
    public void someAction();
}

void Examples()
{
    List<SomeClass> someList = new List<SomeClass>();

    //A. for
    for (int i = 0; i < someList.Count(); i++)
    {
        someList[i].someAction();
    }

    //B. foreach
    foreach (SomeClass o in someList)
    {
        o.someAction();
    }

    //C. foreach extension
    someList.ForEach(o => o.someAction());

    //D. plinq
    someList.AsParallel().ForAll(o => o.someAction());

编辑:从答案和研究中添加一些选项。

    //E. ParallelEnumerable
    ParallelEnumerable.Range(0, someList.Count - 1)
        .ForAll(i => someList[i].someAction());

    //F. ForEach Parallel Extension
    Parallel.ForEach(someList, o => o.someAction());

    //G. For Parallel Extension
    Parallel.For(0, someList.Count - 1, i => someList[i].someAction())
}

我的问题分为两部分。我错过了一些重要的选择吗?考虑可读性但主要是性能,哪个选项是最佳选择?

请说明SomeClass实施的复杂性或Count的{​​{1}}是否会影响此选择。

编辑:有了这样令人眼花缭乱的选项,我不希望我的代码被选择所破坏。要在我的问题中添加第三部分,如果我的列表可以是任何长度,我应该默认为并行选项吗?

作为一个稻草人。我怀疑,考虑到多处理器架构的普遍性,someList的所有实现和SomeClass选项someList的所有长度都将提供最佳的平均性能。我没有做过任何测试来证明这一点。

注意:并行扩展将需要使用//E. ParallelEnumerable命名空间。

7 个答案:

答案 0 :(得分:6)

选项A仅对实现索引的序列有意义,并且只对具有O(1)查找时间的序列执行。通常,我会使用foreach和变体,除非你有特殊的逻辑。

另请注意,for (int i = 1; i < list.Count; i++)之类的“特殊逻辑”可以使用Linq扩展方法实现:foreach(var item in sequence.Skip(1))

所以,一般比B更喜欢B.

对于C:如果他们不习惯功能风格,这可能会让其他开发人员感到困惑。

关于D:这取决于很多因素。我想对于简单的计算,你不想这样做 - 如果循环体需要一段时间来计算,你只会真正受益于并行化。

答案 1 :(得分:3)

你错过了:

Parallel.ForEach(someList, o => o.someAction())
Parallel.For(0, someList.Length, i => someList[i].someAction())

答案 2 :(得分:3)

IL告诉我们for循环是最有效的。没有状态机可以担心。

for 会产生以下内容

IL_0036:  br.s        IL_0048
IL_0038:  ldloc.0     
IL_0039:  ldloc.1     
IL_003A:  callvirt    System.Collections.Generic.List<UserQuery+SomeClass>.get_Item
IL_003F:  callvirt    UserQuery+SomeClass.someAction
IL_0044:  ldloc.1     
IL_0045:  ldc.i4.1    
IL_0046:  add         
IL_0047:  stloc.1     
IL_0048:  ldloc.1     
IL_0049:  ldloc.0     
IL_004A:  call        System.Linq.Enumerable.Count
IL_004F:  blt.s       IL_0038

IL_0051:ret

此处为 foreach 生成的IL显示了正在运行的状态机。 LINQ版本和ForEach产生类似的输出。

IL_0035:  callvirt    System.Collections.Generic.List<UserQuery+SomeClass>.GetEnumerator
IL_003A:  stloc.3     
IL_003B:  br.s        IL_004B
IL_003D:  ldloca.s    03 
IL_003F:  call        System.Collections.Generic.List<UserQuery+SomeClass>.get_Current
IL_0044:  stloc.1     
IL_0045:  ldloc.1     
IL_0046:  callvirt    UserQuery+SomeClass.someAction
IL_004B:  ldloca.s    03 
IL_004D:  call        System.Collections.Generic.List<UserQuery+SomeClass>.MoveNext
IL_0052:  brtrue.s    IL_003D
IL_0054:  leave.s     IL_0064
IL_0056:  ldloca.s    03 
IL_0058:  constrained. System.Collections.Generic.List<>.Enumerator
IL_005E:  callvirt    System.IDisposable.Dispose
IL_0063:  endfinally  
IL_0064:  ret   

我没有做过任何测试,但我认为这是一个安全的假设。

话虽如此,但并不意味着 关键字应始终使用。这一切都取决于你的风格,你的团队风格,或者如果那段代码你的写作确实需要每个CPU周期,你就可以开始。

我认为我不会将AsParallel()与for,foreach或lambda等效项进行比较。你使用AsParallel()拆分CPU密集型任务或阻塞操作,你不会只是迭代“普通”集合。

答案 3 :(得分:1)

就性能而言,我认为其中一种效果最好。

  //A. for
    for (int i = 0; i < someList.Count(); i++)
    {
        someList[i].someAction();
    }

 //D. plinq
    someList.AsParallel().ForAll(o => o.someAction());

虽然在A的情况下,我宁愿不每次都做someList.Count()。

就性能而言,

forforeach相比表现更好。 D可以比A好,但这取决于场景。如果你在某个列表中有一些大数据,Parallelism可能会有所帮助,但如果你的数据很少,可能会造成额外负担

答案 4 :(得分:1)

一般来说,我会选择与我正在进行的逻辑匹配。如果我循环整个列表都使用foreach但是如果我循环遍历一个子集我使用for循环。另外,如果要在循环中修改集合,则必须使用for循环。

我所知道的唯一另一个尚未说明的选项是手动执行foreach正在执行的操作,如果您需要将枚举数的状态维护在其创建范围之外,这将非常有用。

using(var myEnum = aList.GetEnumerator()){
    while(myEnum.MoveNext()){
        myEnum.Current.SomeAction();
    }
}

答案 5 :(得分:0)

for(int i = 0...)要使用此方法,您必须拥有一个可以逐个访问每个元素的数组。

foreach (SomeClass o in someList)此语法可用于可枚举类,即实现IEnumerable的类。 IEnumerable有一个方法GetEnumerator(),它知道如何浏览集合的每个元素。现在,上面的数组实现了IEnumerable。它知道如何枚举集合的方式是你如何定义它。但是,并非所有可以使用foreach语法的IEnumerable类都可以使用第一种方法,因为并非所有集合都提供对每个元素的访问。考虑以下函数(没有测试它):

public IEnumerable<int> GetACoupleOfInts()
{
yield return 1;
yield return 2;
}

}

此方法允许您使用foreach构造,因为运行时知道如何枚举GetACoupleInts()的值,但不允许for构造。

someList.ForEach(o => o.someAction()); - 按照我理解的方式,这个lambda将被转换为与foreach (SomeClass o in someList)相同的表达式

someList.AsParallel().ForAll(o => o.someAction()); - 在决定是否使用PLINQ时,你必须决定“果汁是否值得挤压”。如果someAction()中的工作量是微不足道的,那么运行时尝试组织并发操作中的所有数据的开销将过多,并且您最好连续执行此操作。

tl; dr - 前三个可能会导致相同的调用并且对性能没有实际影响,尽管它们在框架内具有不同的含义。第四种选择在使用前需要更多考虑。

答案 6 :(得分:0)

除了(C),它似乎向后看,我可以想到你可能想要使用其他每一个的情况。此外,根据您的操作,您还可以将标准LINQ投入到混音中。例如,如果您的循环只使用列表项来创建其他对象。

 (E) var someOtherCollection = someList.Select( l => transform(l) );

对于选项(A),如果您需要知道列表中的位置以及使用该项目。选项(B)或(E)将是我通常使用的。如果列表很大并且操作可以并行化(项目之间没有或可管理的依赖关系),则选项(D)是有意义的。

由于你使用的是通用列表,所以除了(E)之外都是O(N)。 Count()应该是O(1)操作,因为它在内部保存在变量中。在其他可枚举类型上,您需要知道如何构造数据结构。如果您不知道集合的类型,我会在索引实现上使用foreach实现或LINQ,因为集合可能没有编入索引,并且可能会将枚举变为O(N 2 )操作。