对于那些对我如何进行基准测试感兴趣的人,看看here,我在“循环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;
}
答案 0 :(得分:6)
之前有人说过,在优化方面,x86 JIT比x64 JIT做得更好,看起来就是这种情况下发生的事情。尽管循环执行的内容基本相同,但JITer创建的x64汇编代码根本不同,我认为它可以解释您所看到的速度差异。
两种方法之间的汇编代码在关键内循环中有所不同,称为1000 * N次。这就是我认为的速度差异。
循环1:
000007fe`97d50240 4d8bd1 mov r10,r9 000007fe`97d50243 4983c128 add r9,28h 000007fe`97d50247 4183c004 add r8d,4 ; Loop while j < 1000d 000007fe`97d5024b 4181f8e8030000 cmp r8d,3E8h 000007fe`97d50252 7cec jl 000007fe`97d50240
循环2:
; rax = ret ; ecx = j ; Add 10 to ret 4 times 000007fe`97d50292 48050a000000 add rax,0Ah 000007fe`97d50298 48050a000000 add rax,0Ah 000007fe`97d5029e 48050a000000 add rax,0Ah 000007fe`97d502a4 48050a000000 add rax,0Ah 000007fe`97d502aa 83c104 add ecx,4 ; increment j by 4 ; Loop while j < 1000d 000007fe`97d502ad 81f9e8030000 cmp ecx,3E8h 000007fe`97d502b3 7cdd jl 000007fe`97d50292
您会注意到JIT正在展开内部循环,但是当涉及到指令的数量时,循环中的实际代码差别很大。循环1经过优化,可以使单个add语句为40,其中循环2使4个add语句为10。
我的(狂野)猜测是JITer可以更好地优化变量p
,因为它是在第一个循环的内部范围中定义的。由于它可以检测到p
从未在该循环之外使用并且确实是临时的,因此它可以应用不同的优化。在第二个循环中,您将对在两个循环范围之外定义和使用的变量进行操作,并且x64 JIT中使用的优化规则不会将其识别为可以具有相同优化的相同代码。
答案 1 :(得分: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
这似乎表明性能是一样的。也许你没有热身VM?
答案 2 :(得分:1)
循环应该比Loop2更快,我想到的唯一解释是编译器优化启动并将long p = 0;
for (int j = 0; j < 1000; j++)
{
p++;
}
减少到像long p = 1000;
这样的东西,检查生成的汇编代码会带来清晰度。
答案 3 :(得分: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
loop2 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
答案 4 :(得分: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;
}
}
}
答案 5 :(得分: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;
}
}
}
所以,我的答案是:理由在你的过度复杂的测量系统中。
答案 6 :(得分:0)
外部循环在两种情况下都是相同的,但这阻止了编译器在第二种情况下优化代码。
问题在于变量ret没有被声明为足够接近内循环,所以它不在外循环的主体中。 ret变量在外部循环之外,这意味着它超出了编译器优化器的范围,后者无法通过2个循环优化代码。
然而,变量p在内循环之前被声明,这就是它被优化的原因。