【问题标题】:Are the limits of for loops calculated once or with each loop?for 循环的限制是计算一次还是每个循环计算一次?
【发布时间】:2010-07-30 21:34:11
【问题描述】:

以下循环(12332*324234)中的限制是计算一次还是每次循环运行时计算?

for(int i=0; i<12332*324234;i++)
{
    //Do something!
}

【问题讨论】:

  • C# - For-loop internals的可能重复
  • @Darin,不是真的。这使用了方法/属性,而不是常量。
  • 不好的例子;编译器会在任何找到静态计算的地方优化它们。
  • 附带说明,VB 与 C# 相反。使用 VB,您总是会评估一次循环限制。

标签: c# loops


【解决方案1】:

为此它计算了一次,或者更可能是 0 次。

编译器会为你优化乘法。

但是,如果您有类似的东西,情况并非总是如此。

for(int i=0; i<someFunction();i++)
{
    //Do something!
}

因为编译器并不总是能够看到someFunction 将返回什么。所以即使someFunction每次都返回一个常量值,如果编译器不知道,就无法优化它。

编辑:正如 MainMa 在评论中所说,在这种情况下,您可以通过执行以下操作来消除成本:

int limit = someFunction();
for(int i=0; i<limit ;i++)
{
    //Do something!
}

如果你确定someFunction()的值在循环过程中不会改变。

【讨论】:

  • 是的。例如,对someFunction() 中的数据库进行查询将是一个非常糟糕的主意。您可以通过在循环之前创建一个整数,为其赋值,然后在循环中使用该整数来轻松避免这种情况。
【解决方案2】:

这是 C# 中最常被误解的循环行为之一。

您需要了解以下内容:

循环边界计算,如果 非常量且涉及变量, 属性访问、函数调用或委托调用 将在每个之前重新计算边界值 循环的迭代。

所以,例如:

for( int i = 0; i < 1234*1234; i++ ) { ... }

在这种情况下,表达式1234*1234 是一个编译时间常数,因此不会在每次迭代时重新计算。其实是在编译时计算出来的,换成一个常数。

但是,在这种情况下:

int k = 10;
for( int i = 0; i < k; i++ ) { k -= 1; ... }

k 的值必须在每次迭代时检查。 毕竟它可以改变 .. 在这个例子中确实如此。幸运的是,由于k 只是一个局部变量,因此访问它的成本非常低 - 在许多情况下,它要么保留在本地 CPU 缓存中,要么甚至保存在寄存器中(取决于 JIT 如何处理和发出机器代码)。

如果是这样的情况:

IEnumerable<int> sequence = ...;
for( int i = 0; i < sequence.Count(); i++ ) { ... }

计算sequence.Count() 的成本可能相当昂贵。而且由于它是在循环的每次迭代中进行评估的,因此可以快速累加。

编译器不能优化循环边界表达式中出现的方法或属性的调用,因为它们也可能随着每次迭代而改变。想象一下,如果上面的循环写成:

IEnumerable<int> sequence = ...;
for( int i = 0; i < sequence.Count(); i++ ) {
    sequence = sequence.Concat( anotherItem );
}

显然sequence 在每次迭代中都会发生变化……因此Count() 可能在每次迭代中都不同。编译器不会尝试执行一些静态分析来确定循环边界表达式是否可能是常量......如果不是不可能的话,这将是极其复杂的。相反,它假定如果表达式不是常量,则必须在每次迭代时对其进行计算。

现在,在大多数情况下,计算循环边界约束的成本会相对便宜,因此您不必担心。但是您确实需要了解编译器如何处理这样的循环边界。此外,作为开发人员您需要小心使用具有副作用的属性或方法作为边界表达式的一部分 - 毕竟,这些副作用会在循环的每次迭代中发生。

【讨论】:

    【解决方案3】:

    实际上它不会编译,因为它会溢出,但如果你把它设置为更小的数字并打开 Reflector,你会发现类似这样的东西。

    for (int i = 0; i < 0x3cf7b0; i++)
    {
    
    }
    

    【讨论】:

      【解决方案4】:

      有两种方法可以解释您的问题:

      • 是否在每个循环上计算 12332*324234 的乘积
      • 循环条件中的表达式是否在每个循环上进行评估

      这两个不同的问题的答案是:

      • 不,它实际上是在编译时评估的,因为它涉及两个常量
      • 是的,如有必要,它们是

      换句话说:

      for (int i = 0; i < someString.Length; i++)
      

      如果someString.Length 的评估代价高昂,则每次循环迭代都会产生惩罚。

      【讨论】:

      • 我怀疑 String.Length 是否像 strlen 一样工作。但是,“someString”可能会在每个迭代循环中发生变化。
      【解决方案5】:

      首先,问题中的 for 循环不会编译。但是可以说是

                  for (int  i = 0; i < 20; i++)
              {
                  Console.WriteLine(i);
                  i++;
      
              }
      

      VS

                  for (int  i = 0; i < 10*2; i++)
              {
                  Console.WriteLine(i);
                  i++;
      
              }
      

      IL 代码完全相同。

         .method private hidebysig static void Main(string[] args) cil managed
      {
          .entrypoint
          .maxstack 2
          .locals init (
              [0] int32 i,
              [1] bool CS$4$0000)
          L_0000: nop 
          L_0001: ldc.i4.0 
          L_0002: stloc.0 
          L_0003: br.s L_0016
          L_0005: nop 
          L_0006: ldloc.0 
          L_0007: call void [mscorlib]System.Console::WriteLine(int32)
          L_000c: nop 
          L_000d: ldloc.0 
          L_000e: ldc.i4.1 
          L_000f: add 
          L_0010: stloc.0 
          L_0011: nop 
          L_0012: ldloc.0 
          L_0013: ldc.i4.1 
          L_0014: add 
          L_0015: stloc.0 
          L_0016: ldloc.0 
          L_0017: ldc.i4.s 20
          L_0019: clt 
          L_001b: stloc.1 
          L_001c: ldloc.1 
          L_001d: brtrue.s L_0005
          L_001f: call int32 [mscorlib]System.Console::Read()
          L_0024: pop 
          L_0025: ret 
      }
      

      现在,即使我用循环中的函数替换它,比如

      class Program
      {
          static void Main(string[] args)
          {
              for (int  i = 0; i < Foo(); i++)
              {
                  Console.WriteLine(i);
                  i++;
      
              }
              Console.Read();
          }
      
          private static int Foo()
          {
              return 20;
          }
      

      我得到了这个 IL 代码

          .method private hidebysig static void Main(string[] args) cil managed
      {
          .entrypoint
          .maxstack 2
          .locals init (
              [0] int32 i,
              [1] bool CS$4$0000)
          L_0000: nop 
          L_0001: ldc.i4.0 
          L_0002: stloc.0 
          L_0003: br.s L_0016
          L_0005: nop 
          L_0006: ldloc.0 
          L_0007: call void [mscorlib]System.Console::WriteLine(int32)
          L_000c: nop 
          L_000d: ldloc.0 
          L_000e: ldc.i4.1 
          L_000f: add 
          L_0010: stloc.0 
          L_0011: nop 
          L_0012: ldloc.0 
          L_0013: ldc.i4.1 
          L_0014: add 
          L_0015: stloc.0 
          L_0016: ldloc.0 
          L_0017: call int32 TestBedForums.Program::Foo()
          L_001c: clt 
          L_001e: stloc.1 
          L_001f: ldloc.1 
          L_0020: brtrue.s L_0005
          L_0022: call int32 [mscorlib]System.Console::Read()
          L_0027: pop 
          L_0028: ret 
      }
      

      在我看来是一样的。

      所以在我看来,FOR LOOP 与有限限制没有区别,另一个与计算限制没有区别,最后一个限制来自函数。

      因此,只要代码可编译,您就知道自己在像这样的庞大循环中正在做什么,并且您在处理过程中有足够的内存,我认为它会工作并产生相同的性能。 (在 C# 中)

      【讨论】:

        【解决方案6】:

        正如@Chaos 所说,它不会编译。但如果您使用可表示的表达式(如 100*100),则结果可能会被硬编码。在 Mono 上,CIL 包括:

        IL_0007:  ldloc.0
        IL_0008:  ldc.i4.1
        IL_0009:  add
        IL_000a:  stloc.0
        IL_000b:  ldloc.0
        IL_000c:  ldc.i4 10000
        IL_0011:  blt IL_0007
        

        如您所见,100 * 100 被硬编码为 10000。但是,通常每次都会对其进行评估,如果调用了方法或属性,则可能无法优化掉。

        【讨论】:

          【解决方案7】:

          看起来每次都在计算。从VS2008反汇编。

          0000003b  nop              
                      for (Int64 i = 0; i < (Int64)12332 * (Int64)324234; i++)
          0000003c  mov         qword ptr [rsp+20h],0 
          00000045  jmp         000000000000005E 
                      {
          00000047  nop              
                          bool h = false;
          00000048  mov         byte ptr [rsp+28h],0 
                      }
          0000004d  nop              
                      for (Int64 i = 0; i < (Int64)12332 * (Int64)324234; i++)
          0000004e  mov         rax,qword ptr [rsp+20h] 
          00000053  add         rax,1 
          00000059  mov         qword ptr [rsp+20h],rax 
          0000005e  xor         ecx,ecx 
          00000060  mov         eax,0EE538FB8h 
          00000065  cmp         qword ptr [rsp+20h],rax 
          0000006a  setl        cl   
          0000006d  mov         dword ptr [rsp+2Ch],ecx 
          00000071  movzx       eax,byte ptr [rsp+2Ch] 
          00000076  mov         byte ptr [rsp+29h],al 
          0000007a  movzx       eax,byte ptr [rsp+29h] 
          0000007f  test        eax,eax 
          00000081  jne         0000000000000047 
          

          【讨论】:

            【解决方案8】:

            除了 Klee1 的答案,我写的是完全相同的东西,只是略有不同,以使其更易于阅读:

            for(int i=0, limit = someFunction(); i<limit ;i++)
            {
                //Do something!
            }
            

            someFunction() 仍然只执行一次,并且循环的头部仍然可以舒适地放在 1 行中,而不会让你的队友感到太多困惑。希望! ;-)

            顺便说一句,这也适用于 C++。

            【讨论】:

              【解决方案9】:

              是的,每个循环周期都会计算比较值。

              如果你绝对必须使用 for 循环,这已经很少需要了,afaik 只有一个“好的”循环模板:

              for (int i = first(), last = last(); i != last; ++i)
              {
              // body
              }
              

              还要注意前缀增量。

              【讨论】:

              • 这是被误导的原因有很多。这不是“必要”的问题。 for 循环从来都不是必需的,但有时有最简洁的方式来表达某些东西。并非所有 for 条件都使用方法。事实上,问题中的那个没有。在那些这样做的情况下,有时方法必须每次都被调用,而在其他情况下,性能差异可以忽略不计。最后,前增量和后增量在这里绝对没有实际区别。
              • 我很确定使用此模板的开发人员知道每次必须调用该方法的时间并会对其进行调整。无论如何,使用 Scott Meyers 在 C++ 中引入的“for 模式”是一种很好的风格,并且仍然非常有用。
              • ++i 和 i++ 没有区别,因为编译器会将它们优化为语义相同。
              猜你喜欢
              • 2011-11-08
              • 1970-01-01
              • 1970-01-01
              • 2021-01-31
              • 2023-03-19
              • 2013-09-06
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多