【问题标题】:C# Compiler OptimizationsC# 编译器优化
【发布时间】:2014-02-21 17:00:38
【问题描述】:

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

 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 秒。

【问题讨论】:

  • 几个问题;你在Release 模式下运行吗?您是否使用Stopwatch 进行计时?
  • 冒险猜测它可能会将foreach 循环变成for 循环,这将防止它不得不从数组切换到枚举。有关这方面的一些讨论,请参阅 stackoverflow.com/questions/365615/…
  • 坐下来等待 Eric Lippert 提供明确的、无需猜测的答复。
  • @JesseCarter:我强烈建议您独立运行它(例如作为控制台应用程序),而不是从单元测试运行器中运行。删除尽可能多的无关位。
  • 您也可以发布您的输入字符串吗?或者更理想的是,一个简短但完整的程序可以展示差异。

标签: c# visual-studio compiler-construction


【解决方案1】:

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

【讨论】:

    【解决方案2】:

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

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

    更新: Dotnet Reflector 可通过http://www.red-gate.com/products/dotnet-development/reflector/获取。

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

    Open Source Alternatives to Reflector?

    【讨论】:

    • 只看 IL 是不够的,您需要查看 JITed 输出才能看到应用的大部分优化。
    • @ScottChamberlain:不是由 compiler 优化标志。编译器的输出是 IL。由于编译器标志而导致的任何更改必须出现在 IL 中。 JIT 如何优化事物是另一回事。
    • 请注意,您不需要 Reflector(免费试用后的付费产品)- ildasm 可以很好地完成这项工作。
    • ILSpy 是 Reflector 的免费替代品,也可以反编译为 C# 或 IL。
    【解决方案3】:

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

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

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

    (点击查看大图)

    【讨论】:

    • 查看 JITted 代码不会显示 compiler 输出的差异。在这种情况下,您评论但被删除的答案是正确的,IMO。 flag是给编译器的,编译器的输出是IL。
    • @JonSkeet 但是,如果我错了,请纠正我,优化的 IL 将更有可能被 JIT 进一步优化,因为它更“JIT 友好”导致更大的变化IL 代码和 JIT 代码。
    • 是的,JIT 会进一步优化。但是 OP 说他们对 compiler 正在做什么感兴趣 - 因此查看 compiler 输出而不是 JIT 是有意义的输出。 (JIT 将取决于各种事情。)
    • @JonSkeet:OP 确实询问了编译器,但听起来(对我而言)好像他假设所有优化都发生在此处。如果编译器输出的 IL 更容易被 JIT 优化,那么编译器的差异只是等式的一半。
    • 在我对这个问题的解释中,OP 更感兴趣的是总运行时间从 25 秒减少到 7 秒,这将涉及查看 IL 优化的性能增益和JITed 代码。编辑:什么@EdS。说:)
    猜你喜欢
    • 1970-01-01
    • 2012-02-09
    • 1970-01-01
    • 2010-12-13
    • 2012-04-11
    • 2012-12-11
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多