9

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

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

Loop2需要 3 倍的时间Loop。我认为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;
    }

更新如果有的话,循环展开何时仍然有用?有用

4

7 回答 7

6

之前已经说过几次,在优化方面,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 < 1000d 时循环
000007fe`97d5024b 4181f8e8030000 cmp r8d,3E8h
000007fe`97d50252 7cec jl 000007fe`97d50240

循环 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 < 1000d 时循环
000007fe`97d502ad 81f9e8030000 cmp ecx,3E8h
000007fe`97d502b3 7cdd jl 000007fe`97d50292

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

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

于 2013-05-05T18:30:16.820 回答
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

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

于 2013-05-05T17:41:42.913 回答
1

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

于 2013-05-05T17:33:10.110 回答
1

通过查看 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
于 2013-05-05T17:48:45.680 回答
1

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

我的测试结果是:

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;
        }
    }
}
于 2013-05-05T18:12:26.040 回答
0

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

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

}

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

于 2013-05-05T17:44:50.353 回答
0

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

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

然而,变量 p 是在内循环之前声明的,这就是为什么它得到了很好的优化。

于 2013-05-05T22:45:38.403 回答