C#编译器优化

Jes*_*ter 15 c# compiler-construction visual-studio

我想知道是否有人可以向我解释一下编译器可能正在为我做些什么来观察一个简单方法的性能差异.

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

我正在与一位同事一起为消息处理应用程序做一些基准测试/优化.在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);
    }
}
Run Code Online (Sandbox Code Playgroud)

在调试中运行优化关闭我看到13秒,我得到2秒.

在发布中运行,优化时间为3.1秒和2.3秒.

Jon*_*eet 7

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

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

比较IL使用ildasm可能有点棘手,因为它包含每个指令的标签.以下是使用和不使用优化(使用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
Run Code Online (Sandbox Code Playgroud)

未优化

  .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
  }
Run Code Online (Sandbox Code Playgroud)

注意事项:

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


Ric*_*ove 6

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

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

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

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

反射器的开源替代品?

  • 请注意,您不需要Reflector(免费试用之外的付费产品) - ildasm可以很好地完成工作. (3认同)
  • [ILSpy](http://ilspy.net/)是Reflector的免费替代品,也可以反编译为C#或IL. (3认同)
  • @ScottChamberlain:不是*编译器*优化标志.编译器的输出是IL.由编译器标志*引起的任何更改必须*存在于IL中.JIT如何优化事物是另一回事. (2认同)

Sco*_*ain 5

我不知道它在做什么优化,但是我可以向您展示如何找到自己的优化方法。

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

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

(点击查看大图) 在此处输入图片说明

  • 查看JITted代码不会显示* compiler *输出中的差异。在这种情况下,IMO,您评论但被删除的答案是正确的。该标志指向编译器,而编译器的输出为IL。 (2认同)