循环作为for循环比while循环快得多,但是当语句更改时,while循环会更快。发生了什么?

4

出于好奇,我测试了一下使用for循环和while循环进行同样操作是否有区别。为什么在我的电脑上(AMD Phenom II X6 1090T @ 3.20GHz)while循环比for循环慢了2-2.5秒呢?它们不是做同样的事情吗?你是否得到类似的结果?

此外,当我把循环中的x = null;语句替换成空语句时,while循环速度会明显加快。这是怎么回事呢?

当然,迭代次数非常高,但差异还是相当显著的,对吧?

static void Main(string[] args)
{
    String x;
    const Int64 FIVE_BN = 5000000000;
    Int64 i = 0;

    DateTime start = DateTime.Now;
    for (; FIVE_BN > i; i++)
        x = null; //Replace with only ; in both loops and the for loop is faster
    Console.Out.WriteLine(FIVE_BN.ToString() + " times (for): " + (DateTime.Now - start));

    i = 0;

    start = DateTime.Now;
    while(FIVE_BN > i++)
        x = null; //Replace with only ; in both loops and the for loop is faster
    Console.Out.WriteLine(FIVE_BN.ToString() + " times (while): " + (DateTime.Now - start));

    Console.Read();
    return;
}

2
不要在基准测试中使用DateTime - 应使用专为此类用途设计的Stopwatch类。 - Oded
也许你的编译器进行了一些优化。 - Basile Starynkevitch
1
只是一些指针,尝试使用Stopwatch进行性能计时,并进行多次运行以过滤掉冷启动被JIT编译的开销(基本上忽略第一次运行)。 - Adam Houldsworth
感谢提供计时器提示。使用计时器后,无论何种情况下for循环速度都更快。但是即使如此,空语句的差异仍然超过1秒。 - David S.
3个回答

11

虽然这只是一种微小的优化,不会成为性能瓶颈。但有趣的是,实际上这两者是不同的,有趣的是,当你提取方法时,使用VS2010运行两个循环时,我得到以下结果:

private static String forLoop(ref Int64 i)
{
    String x;

    for (; FIVE_BN > i; i++)
        x = null; //Replace with only ; in both loops and the for loop is faster
    return x;
}

private static void whileloop(ref String x, ref Int64 i)
{
    while (FIVE_BN > i++)
        x = null; //Replace with only ; in both loops and the for loop is faster
}

这十分有趣……它表明这两个函数确实是不同的。

现在,当我们用 ; 替换循环中的逻辑时,我们得到了以下抽取方法:

private static Int64 forLoopShort(Int64 i)
{

    for (; FIVE_BN > i; i++)
        ; //Replace with only ; in both loops and the for loop is faster
    return i;
}

private static Int64 whileLoopShort(Int64 i)
{

    while (FIVE_BN > i++)
        ; //Replace with only ; in both loops and the for loop is faster
    return i;
}

这就解释了为什么使用这种配置时循环基本上运行相同。

要想知道它们在内联时(而不是提取到方法中时)有什么不同,我们需要查看优化后的CLR代码长什么样子(尽管优化器实际上可能会消除两个函数之间的任何显著差异)。这是留给以后编辑的事情。

编辑:

CIL显示了区别:

For循环具有.maxstack 2,而while循环具有.maxstack 4,否则由于while的递增发生在循环开始时,而for操作发生在循环结束时,因此操作顺序有一点点不同(将循环内容更改为Console.WriteLine(i),可以看到While循环将从1打印,但For循环将从0打印(尽管两个循环都执行相同数量的循环迭代)。

当循环内容只是;时,两个循环在CIL中都短了2行,以下行被删除(对于两个循环都是如此):

IL_0006:  ldnull
IL_0007:  stloc.0

然而当我们使用发布版本进行构建时,代码会非常不同:

x = null;;在任何一个循环中的区别都是无关紧要的,因为优化器已经注意到该值永远不会变成非null。

优化后的for循环和while循环之间的区别如下:

CIL for 循环:

IL_0000:  ldc.i4.0
IL_0001:  conv.i8
IL_0002:  stloc.0
IL_0003:  br.s       IL_000a
IL_0005:  ldloc.0
IL_0006:  ldc.i4.1
IL_0007:  conv.i8
IL_0008:  add
IL_0009:  stloc.0
IL_000a:  ldc.i8     0x12a05f200
IL_0013:  ldloc.0
IL_0014:  bgt.s      IL_0005
IL_0016:  ret

关于CIL中的while循环:

IL_0000:  ldc.i4.0
IL_0001:  conv.i8
IL_0002:  stloc.0
IL_0003:  ldc.i8     0x12a05f200
IL_000c:  ldloc.0
IL_000d:  dup
IL_000e:  ldc.i4.1
IL_000f:  conv.i8
IL_0010:  add
IL_0011:  stloc.0
IL_0012:  bgt.s      IL_0003
IL_0014:  ret

我们可以看到,优化后的while循环比for循环快2个操作,但是它使用更多的堆栈空间。

这两者之间的区别似乎完全与i++出现的位置有关。

确实,通过创建一个新的方法来确认这一点:

private static void forLoopVeryShort()
{
    string x;
    Int64 i = 0;

    for (; FIVE_BN > i++;)
        ; //Replace with only ; in both loops and the for loop is faster
}

无论是在发布版还是调试版中编译生成的这个for方法的CIL代码与while循环的代码完全相同。

这就是它们之间的区别。当它们执行完全相同的行为时,for循环和while循环的表现也完全相同。你提到的差异完全是由于在调试模式下运行代码,而JIT并不总是像发布版代码优化器一样高效。

我喜欢这个问题,我从中学到了东西,希望其他人也能受益。+1


哇!很有努力的样子。非常有趣。期待任何修改 :) - David S.
就是这样,这就是为什么这两个循环执行起来会有所不同。 - Seph
非常好...就像其他答案中所提到的,这只是一个微小的优化,我们可能不需要费心去寻找,但感谢您实际上回答了问题:)我喜欢深入了解事情,它突然间看起来并不神秘。 - David S.

8
您可能会想要使用cordbg(并仔细启用所有JIT优化)来查看生成的本机代码,以确定为什么会发生这种情况...但为什么要麻烦呢?在实际代码中,差异不会很大,因为您将在循环中执行实际工作。
在我看来,微调完全不现实的代码是没有意义的练习。即使对实际代码进行微调,除非您已经验证了这是瓶颈,否则通常也不会有成效。

如果由于循环内的代码而导致差异波动,那么这种差异在实际情况中甚至可能不存在,更不用说成为性能问题了。 - Adam Houldsworth
你说的可能是对的。如果在真正工作时差异仍然显著,我会开始担心这个问题。 - David S.
当我实际查看生成的CIL指令时,很容易明白为什么这两个循环实际上执行不同。 - Seph

3

提示 - 无法再现:

使用以下指令:

5000000000 times (for): 00:00:15.0488608
5000000000 times (while): 00:00:12.7107270

只需使用一个 ;
5000000000 times (for): 00:00:15.0558611
5000000000 times (while): 00:00:12.7297281

我正在发布模式下运行,不在调试器中等。

这可能与框架有关(我使用的是4.0.30319.488,x64),也可能与CPU有关(我使用的是Intel i7 4x2.67GHz(加上HT)),但我第一个猜测是测试的运行方式。


你说得对,我检查了一下,似乎根据我运行测试的方式,甚至是哪种循环先运行的顺序,结果都大相径庭。这种差异似乎是由其他因素引起的,而不是理论代码本身。 - David S.
确保代码已经通过JIT编译器运行一次,并与在发布模式下运行并进行优化相结合,可以显著提高执行时间。+1 - Seph

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接