【问题标题】:Does the c# compiler optimizes Count properties?c# 编译器是否优化了 Count 属性?
【发布时间】:2011-03-18 16:40:14
【问题描述】:
List<int> list = ...

for(int i = 0; i < list.Count; ++i)
{
           ...
}

那么编译器是否知道 list.Count 不必每次迭代都被调用?

【问题讨论】:

  • 它调用了一个变量,就像你使用int count = list.Count 并使用它一样。我对你的问题感到困惑?
  • @Nathan:它实际上是在调用一个属性,这实际上是一个方法调用——而不是一个变量获取。这将由 JIT 来优化(通常不是 C# 编译器),但它不会发生。
  • 好吧,使用这样的 for 循环,它可能必须这样做,因为您可以修改集合。
  • @Reed:我希望 JIT 确实优化了 getter/setter 的方法调用;我知道 Java 会这样做
  • @BlueRaja:在某些情况下确实如此,但并不总是对其进行优化。 Java 也一样...

标签: c# optimization compiler-construction


【解决方案1】:

值得注意的是,因为没有其他人提到过,通过查看这样的循环无法知道“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 完成它的工作,然后分析结果以查看它是否需要改进。

【讨论】:

    【解决方案2】:

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

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

    我不知道 JIT 是否这样做。但我不会这么快就将这个问题说成是“从不”。

    【讨论】:

    • 这可能非常困难,因为您可能在某个其他线程上引用了列表,然后在您处于当前线程的循环中时对其进行修改。当然,这会导致许多其他问题,但核心问题是列表是可变的,因此您不能保证 Count 从一个迭代到另一个迭代的状态。即使您忽略线程也是如此(抖动是否应该递归检查每个方法调用以检查副作用?)
    • 是的,可能很难确定。我的帖子的重点是有时,这很容易。当它很容易时,它可能会得到优化。至于线程,如果在遍历时 List 没有被锁保护,那么即使是未优化的版本也是 bug。
    • 没有人说“从不”。 OP 问:“编译器是否知道 list.Count 不必每次迭代都调用?”我回答说:“你确定吗?”在我看来,OP 认为 Count 属性 不会 改变是理所当然的;我的意图是挑战这个假设。
    【解决方案3】:

    如果您查看为 Dan Tao 的示例生成的 IL,您会在循环条件处看到如下一行:

    callvirt instance int32 [mscorlib]System.Collections.Generic.List`1<int32>::get_Count()
    

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

    【讨论】:

    • 我的 x86 组装技能非常生疏,但对 JIT 结果的调查显示“调用 FFFFFFFFF6BA91F0”可能表明调用了 get_Count()
    • 不,JIT 编译器将其转换为直接 CPU 寄存器加载。它被称为“内联”。小属性 getter 总是在 Release 版本中内联。你得看看优化过的机器码才能看到。
    • 是的,IL 并不是实际发生的最终决定。
    【解决方案4】:

    这取决于 Count 的具体实现;我从未注意到在 List 上使用 Count 属性有任何性能问题,所以我认为没问题。

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

    List<int> list = new List<int>(){0};
    foreach (int item in list)
    { 
        // ...
    } 
    

    【讨论】:

    • 除非你在循环体的某处使用索引iforeach 无法完成向每个元素添加一个这样简单的事情。
    • OP 没有提到向每个元素添加一些东西?
    【解决方案5】:

    不,它没有。因为条件是在每一步上计算的。它可以比仅仅与计数进行比较更复杂,并且允许任何布尔表达式:

     for(int i = 0; new Random().NextDouble() < .5d; i++)
         Console.WriteLine(i);
    

    http://msdn.microsoft.com/en-us/library/aa664753(VS.71).aspx

    【讨论】:

      【解决方案6】:

      你确定吗?

      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++);
      }
      

      这两个代码 sn-ps 似乎完全不同,但这只是因为我们倾向于考虑for 循环与while 循环的方式。在任何一种情况下,每次迭代都会检查变量的值。无论哪种情况,该值都可能发生变化。

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

      【讨论】:

      • IIRC,第一个示例无法编译,您无法在迭代列表时修改列表。
      • @Neil,这将是枚举而不是按索引访问?
      • @Neil:这是 OP 所犯的错误,认为 for 循环与迭代相同。在实践中它可能总是以这种方式使用,但这并不是它真正的定义。它本质上是一个while 循环。您可能会想到foreach,根据定义,它 一个枚举。这将引发异常,因为如果在调用之间修改了列表,List&lt;T&gt;.Enumerator.MoveNext 将引发 InvalidOperationException。无论如何,上面的for 循环确实可以编译;自己试试看。
      • 我可能在想foreach
      • @Neil:是的,完全可以理解。顺便说一句,即使是这种情况(如果从foreach 中修改List&lt;T&gt;,则会引发异常)特定于List&lt;T&gt;.Enumerator 类型;它不是所有集合的固有特征。 (可能只是 BCL 中的所有集合,但不是 all 集合。)IEnumerator&lt;T&gt; 实现必须专门编码以在 MoveNext 上引发异常;如果不是,那么在枚举集合的同时修改集合本身并没有什么违法行为。无论哪种方式,代码肯定会编译
      【解决方案7】:

      C# 编译器不做任何这样的优化。然而,JIT 编译器优化了数组,我相信(不可调整大小),但不适用于列表。

      List 的 count 属性可以在循环结构中发生变化,因此这是不正确的优化。

      【讨论】:

        猜你喜欢
        • 2014-04-18
        • 1970-01-01
        • 2015-06-16
        • 1970-01-01
        • 1970-01-01
        • 2018-07-24
        • 2014-05-23
        • 1970-01-01
        • 2014-08-29
        相关资源
        最近更新 更多