c#编译器是否优化计数属性? List=。。。 对于(int i=0;i

c#编译器是否优化计数属性? List=。。。 对于(int i=0;i,c#,optimization,compiler-construction,C#,Optimization,Compiler Construction,那么编译器是否知道列表。每次迭代都不必调用Count?你确定吗 List<int> list = ... for(int i = 0; i < list.Count; ++i) { ... } 这两个代码片段似乎完全不同,但这只是因为我们倾向于将for循环与while循环相比较。在这两种情况下,每次迭代都会检查变量的值。在任何一种情况下,该值都可能发生变化 通常,当同一代码的“优化”和“非优化”版本之间的行为实际上不同时,假设编译器优化了某些内容是不安

那么编译器是否知道列表。每次迭代都不必调用Count?

你确定吗

List<int> list = ...

for(int i = 0; i < list.Count; ++i)
{
           ...
}
这两个代码片段似乎完全不同,但这只是因为我们倾向于将
for
循环与
while
循环相比较。在这两种情况下,每次迭代都会检查变量的值。在任何一种情况下,该值都可能发生变化

通常,当同一代码的“优化”和“非优化”版本之间的行为实际上不同时,假设编译器优化了某些内容是不安全的。

C#编译器不会进行任何类似的优化。然而,我相信JIT编译器会对数组(不可调整大小)进行优化,但不会对列表进行优化


列表的count属性可以在循环结构中更改,因此这将是一个不正确的优化。

不,它不会。因为每个步骤都会计算条件。它可能比使用计数进行比较更复杂,并且允许使用任何布尔表达式:

List<int> list = new List<int>();

int i = 0;
while (list.Count <= 100)
{
    list.Add(i++);
}
for(int i=0;new Random().NextDouble()<.5d;i++)
控制台写入线(i);

这取决于计数的具体实现;我从来没有注意到在列表上使用Count属性时有任何性能问题,所以我认为这是可以的

在这种情况下,您可以使用foreach节省一些打字时间

 for(int i = 0; new Random().NextDouble() < .5d; i++)
     Console.WriteLine(i);
List List=newlist(){0};
foreach(列表中的int项)
{ 
// ...
} 

对于所有其他认为“Count”属性可能在循环体中发生更改的评论者:JIT优化让您能够利用正在运行的实际代码,而不是可能发生的最坏情况。总的来说,计数可能会改变。但并非所有代码中都有

因此,在海报的示例中(可能没有任何计数更改),JIT检测循环中的代码没有更改List用于保持其长度的任何内部变量是否不合理?如果它检测到
list.Count
是常量,它不会将变量访问权从循环体中取出吗


我不知道JIT是否能做到这一点。但是我并没有那么快就把这个问题一笔勾销,而是简单地说了声“从不”。如果你看看Dan Tao的例子中生成的IL,你会在循环条件下看到这样一行:

List<int> list = new List<int>(){0};
foreach (int item in list)
{ 
    // ...
} 
callvirt实例int32[mscorlib]System.Collections.Generic.List`1::get_Count()

这是不可否认的证据,证明循环的每次迭代都会调用Count(即get_Count())。

值得注意的是,正如没有人提到过的那样,从这样的循环来看,“Count”属性实际上会做什么,或者它可能会有什么副作用,这是不可否认的

考虑以下情况:

  • 名为“Count”的属性的第三方实现可以执行它希望执行的任何代码。e、 g.返回一个我们所知道的随机数。有了列表,我们可以对它的运行方式更加自信,但是JIT如何区分这些实现呢

  • 循环中的任何方法调用都可能改变Count的返回值(不仅仅是直接对集合进行“添加”,循环中调用的用户方法也可能对集合进行参与)

  • 任何其他碰巧同时执行的线程也可以更改计数值

JIT不能“知道”计数是常数

但是,JIT编译器可以通过内联Count属性的实现(只要它是一个微不足道的实现),使代码运行得更高效。在您的示例中,它很可能被内联到一个变量值的简单测试中,从而避免了每次迭代时函数调用的开销,从而使最终的代码既漂亮又快速。(注意:我不知道JIT是否能做到这一点,只是它能做到。我真的不在乎——请看我答案的最后一句,找出原因)

但是,即使使用内联,值在循环的迭代之间仍然可能会改变,因此每次比较都需要从RAM中读取。如果要将Count复制到局部变量中,JIT可以通过查看循环中的代码来确定局部变量在循环的生命周期内保持不变,那么它可能能够进一步优化它(例如,通过在寄存器中保留常量值,而不必在每次迭代时从RAM中读取)。因此,如果您(作为一名程序员)知道Count在循环的生命周期中是恒定的,那么您可以通过在局部变量中缓存Count来帮助JIT。这为JIT提供了优化循环的最佳机会。(但无法保证JIT会实际应用此优化,因此以这种方式手动“优化”执行时间可能不会有任何差异。如果您的假设(该计数为常数),您也有出错的风险。)或者如果另一个程序员编辑循环的内容,使计数不再是常数,并且他没有发现你的聪明,你的代码可能会中断)

因此,这个故事的寓意是:JIT可以通过内联优化这个案例。即使它现在不这样做,它也可能在下一个C#版本中这样做。您可能无法通过手动“选择”代码来获得任何优势,您可能会改变代码的行为,从而破坏它,或者至少会使代码的未来维护变得更具风险,或者可能会失去未来的JIT增强功能。因此,最好的方法就是按照您现有的方式编写,并在探查器告诉您循环是您的性能瓶颈时对其进行优化

因此,考虑/理解这样的案例很有趣,但最终你并不需要知道。一点点知识可能是一件危险的事情。就让JIT来吧
List<int> list = new List<int>(){0};
foreach (int item in list)
{ 
    // ...
} 
callvirt instance int32 [mscorlib]System.Collections.Generic.List`1<int32>::get_Count()