6

这是不言自明的代码(执行十亿次操作):

int k = 0;

Stopwatch sw = new Stopwatch();
sw.Start();
for (int a = 0; a < 1000; a++)
    for (int b = 0; b < 1000; b++)
        for (int c = 0; c < 1000; c++)
            k++;

sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds);

sw = new Stopwatch();
sw.Start();

for (int a = 0; a < 1000; a++)
    for (int b = 0; b < 1000; b++)
        for (int c = 0; c < 1000; c++)
            ; // NO-OP

sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds);

结果(至少在我的计算机上)大约(以毫秒为单位)

2168
2564

第二个总是大约半秒。

增加一个变量十亿次的运行时间怎么可能比执行相同次数的无操作运行时间更长?

编辑:这仅在调试时发生。发布正确地做到了这一点,第一个持续时间更长,至少在我的电脑上。正如评论中所指出的,即使在 RELEASE 构建中,也有人遇到过这个问题。但是在产生这种效果的 DEBUG 上会发生什么?

4

3 回答 3

4

问题正如 Azodious 提到的,您不能使用调试模式来测量时间,因为它不准确。

打开发布模式后,我得到以下数字:

递增k:445

编号:402

IL递增版本中还有 4条指令:

IL_0001:  ldc.i4.0    
IL_0002:  stloc.0     
IL_0003:  ldc.i4.0    
IL_0004:  stloc.1     
IL_0005:  br.s        IL_003B
IL_0007:  ldc.i4.0    
IL_0008:  stloc.2     
IL_0009:  br.s        IL_0029
IL_000B:  ldc.i4.0    
IL_000C:  stloc.3     
IL_000D:  br.s        IL_0017
IL_000F:  ldloc.0     
IL_0010:  ldc.i4.1    
IL_0011:  add         
IL_0012:  stloc.0     
IL_0013:  ldloc.3     
IL_0014:  ldc.i4.1    
IL_0015:  add         
IL_0016:  stloc.3     
IL_0017:  ldloc.3     
IL_0018:  ldc.i4      E8 03 00 00 
IL_001D:  clt         
IL_001F:  stloc.s     04 
IL_0021:  ldloc.s     04 
IL_0023:  brtrue.s    IL_000F
IL_0025:  ldloc.2     
IL_0026:  ldc.i4.1    
IL_0027:  add         
IL_0028:  stloc.2     
IL_0029:  ldloc.2     
IL_002A:  ldc.i4      E8 03 00 00 
IL_002F:  clt         
IL_0031:  stloc.s     04 
IL_0033:  ldloc.s     04 
IL_0035:  brtrue.s    IL_000B
IL_0037:  ldloc.1     
IL_0038:  ldc.i4.1    
IL_0039:  add         
IL_003A:  stloc.1     
IL_003B:  ldloc.1     
IL_003C:  ldc.i4      E8 03 00 00 
IL_0041:  clt         
IL_0043:  stloc.s     04 
IL_0045:  ldloc.s     04 
IL_0047:  brtrue.s    IL_0007

-verisonNOP有相同数量的分支,但少一个add

IL_0001:  ldc.i4.0    
IL_0002:  stloc.0     
IL_0003:  ldc.i4.0    
IL_0004:  stloc.1     
IL_0005:  br.s        IL_0037
IL_0007:  ldc.i4.0    
IL_0008:  stloc.2     
IL_0009:  br.s        IL_0025
IL_000B:  ldc.i4.0    
IL_000C:  stloc.3     
IL_000D:  br.s        IL_0013
IL_000F:  ldloc.3     
IL_0010:  ldc.i4.1    
IL_0011:  add         
IL_0012:  stloc.3     
IL_0013:  ldloc.3     
IL_0014:  ldc.i4      E8 03 00 00 
IL_0019:  clt         
IL_001B:  stloc.s     04 
IL_001D:  ldloc.s     04 
IL_001F:  brtrue.s    IL_000F
IL_0021:  ldloc.2     
IL_0022:  ldc.i4.1    
IL_0023:  add         
IL_0024:  stloc.2     
IL_0025:  ldloc.2     
IL_0026:  ldc.i4      E8 03 00 00 
IL_002B:  clt         
IL_002D:  stloc.s     04 
IL_002F:  ldloc.s     04 
IL_0031:  brtrue.s    IL_000B
IL_0033:  ldloc.1     
IL_0034:  ldc.i4.1    
IL_0035:  add         
IL_0036:  stloc.1     
IL_0037:  ldloc.1     
IL_0038:  ldc.i4      E8 03 00 00 
IL_003D:  clt         
IL_003F:  stloc.s     04 
IL_0041:  ldloc.s     04 
IL_0043:  brtrue.s    IL_0007

这些是在没有优化的情况下编译的,因为我想看看到底发生了什么。

实际上,它们之间的唯一区别是:

IL_0012:  stloc.0     
IL_0013:  ldloc.3     
IL_0014:  ldc.i4.1    
IL_0015:  add  

简单地说:你得到奇怪的数字,因为你处于调试模式。

于 2012-09-18T07:32:43.107 回答
1

除了测试错误的代码之外,您犯的一个核心错误是假设您测量了增量运算符的成本。你没有,你测量了 for() 循环的成本。这比增量需要更多的 cpu 周期。

for() 循环的一个问题是 CPU 被迫分支,跳回到循环的开头。现代CPU不太喜欢分支,它们被优化为顺序执行代码。管道的副作用,核心架构实现细节旨在使处理器快速执行代码。一个分支可能会迫使处理器刷新管道,从而丢掉大量被证明是无用的工作。在 cpu 设计中分配了大量资源,以减少必须冲洗管道的成本。一个核心部分是分支预测器,它试图预先猜测分支将走向哪条路,以便它可以用可能的指令填充管道被执行。猜错是非常昂贵的。如果你的 for() 循环足够长,你就不必为此担心太多。

现代处理器的另一个问题是它们对分支目标的对齐非常敏感。换句话说,循环开始的指令的地址。如果它未对齐,而不是位于可被 4 或 8 整除的地址,则预取单元需要额外的周期来开始解码正确的指令。这是抖动需要处理的实现细节,它可能必须插入额外的 NOP 指令才能使指令对齐。x86 抖动不执行优化,x64 抖动执行。

对齐问题的一个可观察到的副作用是交换两段代码可能会影响您的测量。

基准代码是现代 CPU 上的一次危险冒险,通过分析代码的合成版本观察到的实际代码的可能性并不大。15% 或更少的差异没有统计学意义。

于 2012-12-08T18:54:18.113 回答
0

我跑了 3 次,输出是:

3786

3252


3800

3256


3840

3255

因此,如果您是根据在调试模式下收集的统计数据做出决定,请不要这样做。

调试模式将大量数据附加到代码中,以在调试期间帮助调试器。

于 2012-09-18T07:15:39.410 回答