.net 为什么在C#中(result>>8)& hfm比((op1 ^ result ^ op2)>>8)& hfm慢?

dm7nw8vv  于 2023-03-20  发布在  .NET
关注(0)|答案(2)|浏览(121)

我把这段简单的代码输入到LINQPad 7中,看看哪段代码更快。我惊讶地发现第二个计时循环一直都很慢?有人能解释一下为什么吗?

void Main()
{
  const byte hfm = 0b0001_0000;
  
  UInt16 operand1 = 0x2;
  UInt16 operand2 = 0xFFFE;
  UInt32 result = (UInt32)(operand1 + operand2);
  UInt32 hf;
  var timer = new Stopwatch();
  
  timer.Start();
  for (var i = 0; i < 500_000_000; i++)
  {
    hf = (((operand1 ^ result ^ operand2) >> 8) & hfm);
  }
  timer.Stop();
  timer.Elapsed.Dump("(((operand1 ^ result ^ operand2) >> 8) & hfm)");

  timer.Restart();
  for (var i = 0; i < 500_000_000; i++)
  {
    hf = (result >> 8) & hfm;
  }
  timer.Stop();
  timer.Elapsed.Dump("(result >> 8) & hfm");
}

结果非常一致。第二段(更短更简单)代码运行速度慢了0. 03秒。

(((operand1 ^ result ^ operand2) >> 8) & hfm)
00:00:01.1278598

(result >> 8) & hfm
00:00:01.1612988

如果我从两个代码中删除“& hfm”,第二个代码保持相同的时间,但第一个代码比第二个代码慢约0.002秒;如果我从两个代码中删除“〉〉8”,第一个代码比第二个代码快约0.01秒。
根据LINQPad,为两个代码块生成的IL为
hf = (((operand1 ^ result ^ operand2) >> 8) & hfm);

IL_0022  ldloc.0     // operand1
IL_0023  ldloc.2     // result
IL_0024  xor  
IL_0025  ldloc.1     // operand2
IL_0026  xor  
IL_0027  ldc.i4.8  
IL_0028  shr.un  
IL_0029  ldc.i4.s  10  // 16
IL_002B  and  
IL_002C  stloc.3     // hf

hf = (result >> 8) & hfm;

IL_0073  ldloc.2     // result
IL_0074  ldc.i4.8  
IL_0075  shr.un  
IL_0076  ldc.i4.s  10  // 16
IL_0078  and  
IL_0079  stloc.3     // hf

看起来第二个代码块肯定会更快...

UPDATE原来LINQPad是在编译调试代码(这是有道理的)当我把(Edit/Preferences/Query)改为“Compile for release..."时,两个代码块的时序非常相似。

h4cxqtbf

h4cxqtbf1#

这两个表达式之间的性能差异很可能是由于执行代码的处理器的微架构细节。现代CPU具有复杂的流水线、多个执行单元和许多优化,这些都可能导致非直观的性能特征。
把CPU想象成高科技厨房里的超高效厨师,以 lightning 般的速度处理各种任务,你给我们厨师的两份菜谱(两个表情),食材和烹饪步骤都不一样。
第一道食谱(((operand1 ^ result ^ operand2) >> 8) & hfm)的原料和步骤都比较多,但我们的星星厨师是多任务处理高手。他们可以同时切洋葱、拌沙拉和翻煎饼!这就像CPU能够交叉执行多条指令并并行执行它们一样。因此,尽管第一道食谱看起来更复杂,但我们熟练的厨师可以毫不费力地完成它!
另一方面,第二道食谱(result >> 8) & hfm的原料和步骤较少,但它们需要完成的顺序却让我们的主厨慢了下来。这就像用一条腿跳着玩杂耍一样--不是不可能,但可能需要更多的时间才能做好!
要点是,你不能总是根据表面的复杂程度来判断一个配方(或表达式)。就像我们的厨师一样,CPU是处理任务的大师,有时候,由于指令级并行的魔力,看起来更复杂的表达式可能最终会更快。
不过,请记住,执行时间的差异是微小的,就像你在10加仑的汤里加了多少盐一样,它是存在的,但可能不会对事情的总体规划产生巨大的影响。

lsmepo6l

lsmepo6l2#

确保使用适当的工具和技术进行基准测试。首先在发布模式下运行基准测试代码。
还可以考虑使用BenchmarkDotNet-一个领先的工具(微)基准测试.NET代码,它有很多有用的功能,也可以防止至少一些常见的错误(例如,它警告使用调试模式和/或运行附加的调试器)。
根据我的实验,这两种方法显示出相似的运行时间:

public class CalcBench
{
    const byte hfm = 0b0001_0000;
  
    UInt16 operand1 = 0x2;
    UInt16 operand2 = 0xFFFE;
    private UInt32 result;

    public CalcBench()
    {
        result = (UInt32)(operand1 + operand2);
    }
        
    [Benchmark]
    public UInt32 First()
    {
        UInt32 hf = 0;
        for (int i = 0; i < 10_000; i++)
            hf = (((operand1 ^ result ^ operand2) >> 8) & hfm);
        return hf;
    }

    [Benchmark]
    public UInt32 Second()
    {
        UInt32 hf = 0;
        for(int i=0; i < 10_000; i++)
         hf = (result >> 8) & hfm;
        return hf;
    }
}

在我的计算机上:
| 方法|平均值|错误|标准差|
| - ------|- ------|- ------|- ------|
| 第一次|2.376美国|0.0129美国|0.0121美国|
| 第二次|2.365美国|0.0083美国|0.0074美国|
请注意,对于如此接近的微基准测试结果,尽管所有库都试图缓解,但仍有许多事情可能会干扰结果(例如,我在笔记本电脑上运行,这可能会限制CPU,另一个进程也可能会争夺资源,等等)

相关问题