【问题标题】:How to explain the difference of performance in these 2 simple loops?如何解释这两个简单循环的性能差异?
【发布时间】:2013-04-29 12:28:00
【问题描述】:

对于那些对我如何进行基准测试感兴趣的人,请看here,我在“Loop 1K”方法的构建附近简单地替换/添加了几个方法。

对不起,我忘了说我的测试环境。 .Net 4.5 x64(不要选择 32 位首选)。在 x86 中,这两种方法都需要 5 倍的时间。

Loop2 花费的时间是Loop 的 3 倍。我认为x++ / x+=y 不应该在x 变大时减速(因为它需要 1 或 2 个 cpu 指令)

是由于参考的地方性吗?不过我觉得Loop2里面的变量并不多,应该都差不多吧……

    public long Loop(long testSize)
    {
        long ret = 0;
        for (long i = 0; i < testSize; i++)
        {
            long p = 0;
            for (int j = 0; j < 1000; j++)
            {
                p+=10;
            }
            ret+=p;
        }
        return ret;
    }

    public long Loop2(long testSize)
    {
        long ret = 0;
        for (long i = 0; i < testSize; i++)
        {
            for (int j = 0; j < 1000; j++)
            {
                ret+=10;
            }
        }
        return ret;
    }

更新When, if ever, is loop unrolling still useful?很有用

【问题讨论】:

  • 你应该展示你是如何设置衡量性能的。
  • testSize多少钱
  • 可能是优化问题。在“循环”中,优化器可以安全地假设变量 p 的类型是 int 而不是 long。因此,可以使内循环中的增量更快。
  • 我得到了类似的结果(但我尝试时只需要 2 倍的时间)。我用 testsize = 10000000 进行了尝试,发布版本。循环运行几次试验,使用秒表计时。
  • 我刚刚用这种方法验证了stackoverflow.com/a/1048708/421143 - 这两种方法的结果相似。

标签: c# performance


【解决方案1】:

之前有人说过,在优化方面,x86 JIT 比 x64 JIT 做得更好,看起来这就是在这种情况下发生的情况。尽管循环执行基本相同的事情,但 JITer 创建的 x64 汇编代码根本不同,我认为这是您看到的速度差异的原因。

两种方法的汇编代码在关键内循环不同,调用1000*N次。这就是我认为造成速度差异的原因。

循环 1:

000007fe`97d50240 4d8bd1 mov r10,r9 000007fe`97d50243 4983c128 添加 r9,28h 000007fe`97d50247 4183c004 添加 r8d,4 ; j

循环 2:

; rax = ret ; ecx = j ;加 10 至 ret 4 次 000007fe`97d50292 48050a000000 添加 rax,0Ah 000007fe`97d50298 48050a000000 添加 rax,0Ah 000007fe`97d5029e 48050a000000 添加 rax,0Ah 000007fe`97d502a4 48050a000000 添加 rax,0Ah 000007fe`97d502aa 83c104 添加 ecx,4 ;将 j 增加 4 ; j

您会注意到 JIT 正在展开内部循环,但循环中的实际代码在执行的指令数量方面存在很大差异。循环 1 被优化为创建一个 40 的 add 语句,其中 Loop 2 使 4 个 10 的 add 语句。

我的(疯狂的)猜测是,JITer 可以更好地优化变量p,因为它是在第一个循环的内部范围内定义的。由于它可以检测到p 从未在该循环之外使用并且是真正临时的,因此它可以应用不同的优化。在第二个循环中,您正在对在两个循环范围之外定义和使用的变量进行操作,并且 x64 JIT 中使用的优化规则不会将其识别为可以具有相同优化的相同代码。

【讨论】:

  • 我原以为 x64 Jit 比 x86 更激进。例如。在消除范围检查时,尾递归... x64 肯定更好
  • 我确信这是正确的答案(或接近它)。我发现,如果您将内部循环中使用的增量作为参数传递给方法,那么两种方法都会以几乎相同的速度运行。这支持了这样一种观点,即它是内部循环中的一种优化,并且可能依赖于编译器在编译时知道增量是什么。
【解决方案2】:

我没有看到任何明显的性能差异。使用这个 LinqPad 脚本(包括你的这两种方法):

void Main()
{
    // Warmup the vm
    Loop(10);
    Loop2(10);

    var stopwatch = Stopwatch.StartNew();
    Loop(10 * 1000 * 1000);
    stopwatch.Stop();
    stopwatch.Elapsed.Dump();

    stopwatch = Stopwatch.StartNew();
    Loop2(10 * 1000 * 1000);
    stopwatch.Stop();
    stopwatch.Elapsed.Dump();
}

打印出来(在 LinqPad 中);

00:00:22.7749976
00:00:22.6971114

当颠倒Loop/Loop2调用的顺序时,结果类似:

00:00:22.7572688
00:00:22.6758102

这似乎表明性能是相同的。也许您没有预热虚拟机?

【讨论】:

  • 尝试 x64 构建,我忘了在 x86 中它们是相似的。而 LinqPad 是默认的 x86(我试过了)
  • @colinfang,我刚刚尝试使用明确指定 x64 的控制台应用程序。它的表现完全相同。您是否尝试过我的代码作为您自己的控制台应用程序?
  • 我确实用过你的。你要截屏吗?
  • @KirkWoll 22 秒?您正在测试的是发布版本还是调试版本?您是否在调试器之外运行它?我在我的电脑上运行了你的代码,只用了 2.5 秒。我怀疑我的电脑比你的快 8 倍,所以我怀疑你没有安排发布版本。
  • 你说得对,我没有使用发布版本。现在我分别得到 6.1917275 和 6.1968686。
【解决方案3】:

Loop 应该比 Loop2 快,我想到的唯一解释是编译器优化启动并将 long p = 0; for (int j = 0; j < 1000; j++) { p++; } 减少到类似于 long p = 1000; 的东西,检查生成的汇编代码会带来清晰。

【讨论】:

  • 这将提供大约 1000 倍的加速。
  • 我将p++ 更改为p+=10,问题仍然存在。我认为编译器不够聪明,无法优化 +=10 ......我使用 ILSPY 在 C# 中反编译,没有找到优化。
【解决方案4】:

通过查看 IL 本身,loop2 应该更快(而且在我的计算机上更快)

循环IL

.method public hidebysig 
instance int64 Loop (
    int64 testSize
) cil managed 
{
// Method begins at RVA 0x2054
// Code size 48 (0x30)
.maxstack 2
.locals init (
    [0] int64 'ret',
    [1] int64 i,
    [2] int64 p,
    [3] int32 j
)

IL_0000: ldc.i4.0
IL_0001: conv.i8
IL_0002: stloc.0
IL_0003: ldc.i4.0
IL_0004: conv.i8
IL_0005: stloc.1
IL_0006: br.s IL_002a
// loop start (head: IL_002a)
    IL_0008: ldc.i4.0
    IL_0009: conv.i8
    IL_000a: stloc.2
    IL_000b: ldc.i4.0
    IL_000c: stloc.3
    IL_000d: br.s IL_0019
    // loop start (head: IL_0019)
        IL_000f: ldloc.2
        IL_0010: ldc.i4.s 10
        IL_0012: conv.i8
        IL_0013: add
        IL_0014: stloc.2
        IL_0015: ldloc.3
        IL_0016: ldc.i4.1
        IL_0017: add
        IL_0018: stloc.3

        IL_0019: ldloc.3
        IL_001a: ldc.i4 1000
        IL_001f: blt.s IL_000f
    // end loop

    IL_0021: ldloc.0
    IL_0022: ldloc.2
    IL_0023: add
    IL_0024: stloc.0
    IL_0025: ldloc.1
    IL_0026: ldc.i4.1
    IL_0027: conv.i8
    IL_0028: add
    IL_0029: stloc.1

    IL_002a: ldloc.1
    IL_002b: ldarg.1
    IL_002c: blt.s IL_0008
// end loop

IL_002e: ldloc.0
IL_002f: ret
} // end of method Program::Loop

循环2 IL

.method public hidebysig 
instance int64 Loop2 (
    int64 testSize
) cil managed 
{
// Method begins at RVA 0x2090
// Code size 41 (0x29)
.maxstack 2
.locals init (
    [0] int64 'ret',
    [1] int64 i,
    [2] int32 j
)

IL_0000: ldc.i4.0
IL_0001: conv.i8
IL_0002: stloc.0
IL_0003: ldc.i4.0
IL_0004: conv.i8
IL_0005: stloc.1
IL_0006: br.s IL_0023
// loop start (head: IL_0023)
    IL_0008: ldc.i4.0
    IL_0009: stloc.2
    IL_000a: br.s IL_0016
    // loop start (head: IL_0016)
        IL_000c: ldloc.0
        IL_000d: ldc.i4.s 10
        IL_000f: conv.i8
        IL_0010: add
        IL_0011: stloc.0
        IL_0012: ldloc.2
        IL_0013: ldc.i4.1
        IL_0014: add
        IL_0015: stloc.2

        IL_0016: ldloc.2
        IL_0017: ldc.i4 1000
        IL_001c: blt.s IL_000c
    // end loop

    IL_001e: ldloc.1
    IL_001f: ldc.i4.1
    IL_0020: conv.i8
    IL_0021: add
    IL_0022: stloc.1

    IL_0023: ldloc.1
    IL_0024: ldarg.1
    IL_0025: blt.s IL_0008
// end loop

IL_0027: ldloc.0
IL_0028: ret
} // end of method Program::Loop2

【讨论】:

  • IL 分析并不是全部。可以将许多不同的 IL 组合成相同的高效结果。
  • 优化器无法进一步加快处理速度是一个(负面)惊喜。
  • @AxelKemper 大多数优化是由 JIT 编译器完成的,而不是由 C# 编译器完成的。
【解决方案5】:

我可以在我的系统上确认这个结果。

我的测试结果是:

x64 Build

00:00:01.1490139 Loop
00:00:02.5043206 Loop2

x32 Build

00:00:04.1832937 Loop
00:00:04.2801726 Loop2

这是在调试器之外运行的 RELEASE 构建。

using System;
using System.Diagnostics;

namespace Demo
{
    internal class Program
    {
        private static void Main()
        {
            new Program().test();
        }

        private void test()
        {
            Stopwatch sw = new Stopwatch();

            int count = 10000000;

            for (int i = 0; i < 5; ++i)
            {
                sw.Restart();
                Loop(count);
                Console.WriteLine(sw.Elapsed + " Loop");
                sw.Restart();
                Loop2(count);
                Console.WriteLine(sw.Elapsed + " Loop2");
                Console.WriteLine();
            }
        }


        public long Loop(long testSize)
        {
            long ret = 0;
            for (long i = 0; i < testSize; i++)
            {
                long p = 0;
                for (int j = 0; j < 1000; j++)
                {
                    p++;
                }
                ret += p;
            }
            return ret;
        }

        public long Loop2(long testSize)
        {
            long ret = 0;
            for (long i = 0; i < testSize; i++)
            {
                for (int j = 0; j < 1000; j++)
                {
                    ret++;
                }
            }
            return ret;
        }
    }
}

【讨论】:

    【解决方案6】:

    我已经进行了自己的测试,但没有发现任何显着差异。试试看:

    using System;
    using System.Diagnostics;
    
    namespace ConsoleApplication1
    {
        class Program
        {
            static void Main(string[] args)
            {
                Stopwatch sw = new Stopwatch();
                while (true)
                {
                    sw.Start();
                    Loop(5000000);
                    sw.Stop();
                    Console.WriteLine("Loop: {0}ms", sw.ElapsedMilliseconds);
                    sw.Reset();
    
                    sw.Start();
                    Loop2(5000000);
                    sw.Stop();
                    Console.WriteLine("Loop2: {0}ms", sw.ElapsedMilliseconds);
                    sw.Reset();
    
                    Console.ReadLine();
                }
            }
    
            static long Loop(long testSize)
            {
                long ret = 0;
                for (long i = 0; i < testSize; i++)
                {
                    long p = 0;
                    for (int j = 0; j < 1000; j++)
                    {
                        p++;
                    }
                    ret += p;
                }
                return ret;
            }
    
            static long Loop2(long testSize)
            {
                long ret = 0;
                for (long i = 0; i < testSize; i++)
                {
                    for (int j = 0; j < 1000; j++)
                    {
                        ret++;
                    }
                }
                return ret;
            }
        }
    
    }
    

    所以,我的回答是:原因在于您过于复杂的测量系统。

    【讨论】:

    • 尝试 x64 构建,我忘了在 x86 中它们是相似的。
    【解决方案7】:

    外循环在这两种情况下是相同的,但这就是阻止编译器在第二种情况下优化代码的原因。

    问题是变量 ret 没有声明为离内循环足够近,所以它不在外循环的主体中。 ret 变量在外循环之外,这意味着它超出了编译器优化器的范围,编译器优化器无法通过 2 个循环优化代码。

    但是变量 p 是在内循环之前声明的,这就是它得到很好优化的原因。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2023-02-07
      • 1970-01-01
      • 1970-01-01
      • 2021-04-03
      • 1970-01-01
      • 2010-11-13
      相关资源
      最近更新 更多