C#编译器优化

时间:2014-01-29 17:50:25

标签: 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秒。

在Release中运行优化时间为3.1秒和2.3秒。

3 个答案:

答案 0 :(得分:7)

要查看 C#编译器为您做什么,您需要查看IL。如果你想看看它如何影响JITted代码,你需要查看Scott Chamberlain所描述的本机代码。请注意,JITted代码将根据处理器体系结构,CLR版本,流程的启动方式以及可能的其他内容而有所不同。

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

使用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(这是其中一个额外本地人的原因)。
  • 未经优化的版本在返回之前使用额外的本地存储返回值,可能是为了使调试更容易。
  • 未经优化的版本在返回之前有一个无条件分支。
  • 优化版本较短,但我怀疑它的内容足够短,所以我怀疑这是无关紧要的。

答案 1 :(得分:6)

为了更好地理解,您应该查看生成的IL代码。

编译程序集,然后复制它并使用优化再次编译。然后打开.net反射器中的两个程序集,并比较编译的IL的差异。

更新: Dotnet Reflector可在http://www.red-gate.com/products/dotnet-development/reflector/

获得

更新2: IlSpy似乎是一个很好的开源替代品。 http://ilspy.net/

Open Source Alternatives to Reflector?

答案 2 :(得分:5)

我不知道它正在做什么优化,但我可以告诉你如何找到自己。

首先构建优化的代码并在没有附加调试器的情况下启动它(如果连接了调试器,JIT编译器将生成不同的代码)。运行您的代码,以便您知道该部分至少输入一次,以便JIT编译器有机会处理它,并在Visual Studio中转到Debug->Attach To Process...。从新菜单中选择正在运行的应用程序。

在你想知道的地点放一个断点,让程序停止,一旦停止,转到Debug->Windows->Dissasembly。这将向您展示JIT创建的已编译代码,您将能够检查它正在做什么。

(点击查看大图) enter image description here