C# 编译器优化

标签 c# visual-studio compiler-construction

我想知道是否有人可以向我解释编译器到底做了什么让我观察到一个简单方法的性能差异如此之大。

 public static uint CalculateCheckSum(string str) { 
    char[] charArray = str.ToCharArray();
    uint checkSum = 0;
    foreach (char c in charArray) {
        checkSum += c;
    }
    return checkSum % 256;
 }

我正在与一位同事合作,为消息处理应用程序做一些基准测试/优化。在 Visual Studio 2012 中使用相同的输入字符串对此函数进行 1000 万次迭代大约需要 25 秒,但是当使用“优化代码”选项构建项目时,相同的代码在 7 秒内执行相同的 1000 万次迭代。

我非常有兴趣了解编译器在幕后做了什么,以便我们能够看到像这样看似无辜的代码块的性能提高超过 3 倍。

根据要求,这是一个完整的控制台应用程序,它演示了我所看到的内容。

class Program
{
    public static uint CalculateCheckSum(string str)
    {
        char[] charArray = str.ToCharArray();
        uint checkSum = 0;
        foreach (char c in charArray)
        {
            checkSum += c;
        }
        return checkSum % 256;
    }

    static void Main(string[] args)
    {
        string stringToCount = "8=FIX.4.29=15135=D49=SFS56=TOMW34=11752=20101201-03:03:03.2321=DEMO=DG00121=155=IBM54=138=10040=160=20101201-03:03:03.23244=10.059=0100=ARCA10=246";
        Stopwatch stopwatch = Stopwatch.StartNew();
        for (int i = 0; i < 10000000; i++)
        {
            CalculateCheckSum(stringToCount);
        }
        stopwatch.Stop();
        Console.WriteLine(stopwatch.Elapsed);
    }
}

在关闭优化的情况下调试运行我看到 13 秒,我得到 2 秒。

在优化后的发布中运行 3.1 秒和 2.3 秒。

最佳答案

要查看C# 编译器 为您做了什么,您需要查看 IL。如果您想了解这如何影响 JIT 代码,您需要查看 Scott Chamberlain 所描述的 native 代码。请注意,JITted 代码会因处理器架构、CLR 版本、进程的启动方式以及可能的其他因素而异。

我通常会从 IL 开始,然后可能查看 JIT 代码。

使用 ildasm 比较 IL 可能会有些棘手,因为它包含每条指令的标签。以下是经过优化和未经优化(使用 C# 5 编译器)编译的方法的两个版本,删除了无关的标签(和 nop 指令)以使它们尽可能易于比较:

优化

  .method public hidebysig static uint32 
          CalculateCheckSum(string str) cil managed
  {
    // Code size       46 (0x2e)
    .maxstack  2
    .locals init (char[] V_0,
             uint32 V_1,
             char V_2,
             char[] V_3,
             int32 V_4)
    ldarg.0
    callvirt   instance char[] [mscorlib]System.String::ToCharArray()
    stloc.0
    ldc.i4.0
    stloc.1
    ldloc.0
    stloc.3
    ldc.i4.0
    stloc.s    V_4
    br.s       loopcheck
  loopstart:
    ldloc.3
    ldloc.s    V_4
    ldelem.u2
    stloc.2
    ldloc.1
    ldloc.2
    add
    stloc.1
    ldloc.s    V_4
    ldc.i4.1
    add
    stloc.s    V_4
  loopcheck:
    ldloc.s    V_4
    ldloc.3
    ldlen
    conv.i4
    blt.s      loopstart
    ldloc.1
    ldc.i4     0x100
    rem.un
    ret
  } // end of method Program::CalculateCheckSum

未优化

  .method public hidebysig static uint32 
          CalculateCheckSum(string str) cil managed
  {
    // Code size       63 (0x3f)
    .maxstack  2
    .locals init (char[] V_0,
             uint32 V_1,
             char V_2,
             uint32 V_3,
             char[] V_4,
             int32 V_5,
             bool V_6)
    ldarg.0
    callvirt   instance char[] [mscorlib]System.String::ToCharArray()
    stloc.0
    ldc.i4.0
    stloc.1
    ldloc.0
    stloc.s    V_4
    ldc.i4.0
    stloc.s    V_5
    br.s       loopcheck

  loopstart:
    ldloc.s    V_4
    ldloc.s    V_5
    ldelem.u2
    stloc.2
    ldloc.1
    ldloc.2
    add
    stloc.1
    ldloc.s    V_5
    ldc.i4.1
    add
    stloc.s    V_5
  loopcheck:
    ldloc.s    V_5
    ldloc.s    V_4
    ldlen
    conv.i4
    clt
    stloc.s    V_6
    ldloc.s    V_6
    brtrue.s   loopstart

    ldloc.1
    ldc.i4     0x100
    rem.un
    stloc.3
    br.s       methodend

  methodend:
    ldloc.3
    ret
  }

注意事项:

  • 优化后的版本使用了更少的本地人。这可能允许 JIT 更有效地使用寄存器。
  • 优化版本在检查是否再次循环时使用 blt.s 而不是 clt 后跟 brtrue.s (这是一个额外的本地人的原因)。
  • 未优化的版本在返回之前使用额外的本地来存储返回值,大概是为了使调试更容易。
  • 未优化的版本在返回之前有一个无条件分支。
  • 优化后的版本更短,但我怀疑它是否足够短以内联,所以我怀疑这无关紧要。

关于C# 编译器优化,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/21438751/

相关文章:

java - 类型转换在编译器/机器级别做什么?

c++ - LLVM 中的非指针全局变量

xcode - 在 Mac OSX 10.6.8 上安装 f2c

c# - 为什么 servicestack 不能将这个 JSON 反序列化为 C#?

c# - 如何使用 C# 访问 ListBox 中的 TextBlock

c# - 如何捕获 system.xml.xml 异常

visual-studio - 何时使用 `silent` 代码分析严重性?

c# - Apache Ignite .Net 客户端服务器连接

c# - 建议的组合框下拉宽度

c# - 如何将 powershell 集成到 C sharp