List<int> list = ...
for(int i = 0; i < list.Count; ++i)
{
...
}
编译器是否知道list.Count不必每次迭代都被调用?
答案 0 :(得分:22)
你确定吗?
List<int> list = new List<int> { 0 };
for (int i = 0; i < list.Count; ++i)
{
if (i < 100)
{
list.Add(i + 1);
}
}
如果编译器缓存了上面的Count
属性,list
的内容将为0和1.如果没有,则内容将是从0到100的整数。
现在,这对你来说似乎是一个人为的例子;但是这个呢?
List<int> list = new List<int>();
int i = 0;
while (list.Count <= 100)
{
list.Add(i++);
}
看起来好像这两个代码段完全不同,但这只是因为我们倾向于考虑for
循环与while
循环的方式。在任何一种情况下,每次迭代都会检查变量的值。在任何一种情况下,这个价值都可以改变。
通常,当相同代码的“优化”和“非优化”版本之间的行为实际上不同时,假设编译器优化某些内容是不安全的。
答案 1 :(得分:9)
C#编译器不会像这样进行任何优化。但是,JIT编译器对数组进行了优化,我认为(不可调整大小),但不适用于列表。
List的count属性可以在循环结构中更改,因此这将是一个不正确的优化。
答案 2 :(得分:4)
值得注意的是,正如其他人没有提到过的那样,从这样的循环中看不出“Count”属性实际会做什么,或者它可能带来什么副作用。
考虑以下情况:
名为“Count”的属性的第三方实现可以执行它希望的任何代码。例如为我们所知道的所有人返回一个随机数。使用List我们可以对它的运行方式更有信心,但JIT如何区分这些实现呢?
循环中的任何方法调用都可能会改变Count的返回值(不只是直接在集合上直接“添加”,但循环中调用的用户方法也可能在集合中聚会)
恰好同时执行的任何其他线程也可以更改Count值。
JIT无法“知道”Count是不变的。
但是,JIT编译器可以通过内联 Count属性的实现来提高代码的运行效率(只要它是一个简单的实现)。在您的示例中,可能会内联到对变量值的简单测试,避免每次迭代时函数调用的开销,从而使最终代码变得更好,更快。 (注意:我不知道JIT 是否会这样做,只是它可以。我真的不在乎 - 看我回答的最后一句话为什么)
但即使使用内联,该值仍可能在循环的迭代之间发生变化,因此仍需要从RAM中读取每次比较。如果要将Count复制到局部变量中,并且JIT可以通过查看循环中的代码来确定局部变量将在循环的生命周期内保持不变,那么它可以进一步优化它(例如,通过保持常量寄存器中的值,而不是每次迭代时必须从RAM中读取它。因此,如果您(作为程序员)知道 Count将在循环的生命周期中保持不变,您可以通过在局部变量中缓存Count来帮助JIT。这为JIT提供了优化循环的最佳机会。 (但是不能保证JIT实际上会应用这种优化,因此手动“优化”这种方式对执行时间没有任何影响。如果你的假设(Count是常数)不正确,你也会冒错误的风险或者如果另一个程序员编辑循环的内容,那么你的代码可能会破坏,这样Count就不再是常数,而且他没有发现你的聪明。)
故事的寓意是:JIT可以通过内联来优化这种情况。即使它现在不这样做,也可以使用下一个C#版本。您可能无法通过手动“优化”代码获得任何优势,并且您可能会改变其行为并因此破坏它,或至少使您的代码的未来维护更具风险,或者可能会失去未来的JIT增强功能。因此,最好的方法是以您拥有的方式编写它,并在您的探查器告诉您循环是您的性能瓶颈时优化它。
因此,恕我直言,考虑/理解这样的案例很有意思,但最终你并不需要知道。一点点知识可能是一件危险的事情。让JIT做它的事情,然后分析结果,看它是否需要改进。
答案 3 :(得分:3)
如果你看一下为Dan Tao的例子生成的IL,你会在循环的条件下看到这样的一行:
callvirt instance int32 [mscorlib]System.Collections.Generic.List`1<int32>::get_Count()
这是不可否认的证据,即每次迭代循环都会调用Count(即get_Count())。
答案 4 :(得分:1)
对于所有其他评论者,他们说'Count'属性可能在循环体中发生变化:JIT优化让你可以利用正在运行的实际代码,而不是最坏的情况可能发生。一般来说,伯爵可能会改变。但它并不是所有的代码。
所以在海报的例子中(可能没有任何计数更改),JIT检测到循环中的代码不会改变List用于保持其长度的内部变量是不合理的吗?如果它检测到list.Count
是常量,那么它不会将该变量访问提升出循环体吗?
我不知道JIT是否这样做。但我并没有那么快就把这个问题简单地解决掉了“从不”。
答案 5 :(得分:0)
不,它没有。因为条件是在每一步计算的。它可能比仅与count的比较更复杂,并且允许任何布尔表达式:
for(int i = 0; new Random().NextDouble() < .5d; i++)
Console.WriteLine(i);
http://msdn.microsoft.com/en-us/library/aa664753(VS.71).aspx
答案 6 :(得分:0)
这取决于Count的具体实现;我从来没有注意到在List上使用Count属性有任何性能问题所以我认为没问题。
在这种情况下,您可以使用foreach节省一些打字。
List<int> list = new List<int>(){0};
foreach (int item in list)
{
// ...
}