• C#编译器优化那点事


    使用C#编写程序,给最终用户的程序,是需要使用release配置的,而release配置和debug配置,有一个关键区别,就是release的编译器优化默认是启用的。
    优化代码开关即optimize开关,和debug开关一起,有以下几种组合。

    在Visual Sutdio中新建一个C#项目时,
    项目的“调试”(Debug)配置的是/optimize-和/debug:full开关,
    而“发布”(Release)配置指定的是/optimize+和/debug:pdbonly开关

    optimize-/+决定了编译器是否优化代码,optimize-就是不优化了,但是通常,有一些基本的“优化”工作,无论是否指定optimize+,都会执行。

    optimize- and optimize+

    该项功能主要用于动态语义分析,帮助我们更好地编写代码。

    • 常量计算

    在写程序的时候,有时能看见代码下面划了一道红波浪线,那就是编译器动态检查。常量计算,就是这样,编译器会计算常量,帮助判断其他错误。

    • 简单分支检查

    如果swtich写了两个以上的相同条件,或者分支明显无法访问到,都会弹出提示。

    • 未使用变量

    不多说明,直接看图。

    • 使用未赋值变量

    不多说,看图。

    局限

    使用变量参与计算,随便写一个算式,就可以绕过一些检查,虽然我们看来是明显有问题的。

    optimize+ only

    首先需要了解c#代码编译的过程,如下图:

    图片来自http://www.cnblogs.com/rush/p/3155665.html

    C# compiler将C#代码生成IL代码的就是所谓的编译器优化。先说重点。
    .NET的JIT机制,主要优化在JIT中完成,编译器optimize只做一点简单的工作。(划重点)

    探究一下到底干了点啥吧,以下是使用到的工具。

    Tools:
    Visual studio 2017 community targeting .net core 2.0
    IL DASM(vs自带)

    使用IL DASM可以查看编译器生成的IL代码,这样就能看到优化的作用了。IL代码的用途与机制不是本文的重点,不明白的同学可以先去看看《C# via CLR》(好书推荐)。

    按照优化的类型进行了简单的分类。

    • 从未使用变量

    代码如下:

    using System;
    using System.Threading.Tasks;
    
    namespace CompileOpt
    {
        class Program
        {
            static void Main(string[] args)
            {
                int x = 3;
                Console.WriteLine("sg");
            }
        }
    }
    

    未优化的时候

    .method private hidebysig static void  Main(string[] args) cil managed
    {
      .entrypoint
      // 代码大小       15 (0xf)
      .maxstack  1
      .locals init (int32 V_0)
      IL_0000:  nop
      IL_0001:  ldc.i4.3
      IL_0002:  stloc.0
      IL_0003:  ldstr      "sg"
      IL_0008:  call       void [System.Console]System.Console::WriteLine(string)
      IL_000d:  nop
      IL_000e:  ret
    } // end of method Program::Main
    

    使用优化开关优化之后:

    .method private hidebysig static void  Main(string[] args) cil managed
    {
      .entrypoint
      // 代码大小       11 (0xb)
      .maxstack  8
      IL_0000:  ldstr      "sg"
      IL_0005:  call       void [System.Console]System.Console::WriteLine(string)
      IL_000a:  ret
    } // end of method Program::Main
    

    .locals init (int32 V_0)消失了(局部变量,类型为int32)
    ldc.i4.3(将3推送到堆栈上)和stloc.0(将值从堆栈弹出到局部变量 0)也消失了。
    所以,整个没有使用的变量,在设置为优化的时候,就直接消失了,就像从来没有写过一样。

    • 空try catch语句

    代码如下:

    using System;
    using System.Threading.Tasks;
    
    namespace CompileOpt
    {
        class Program
        {
            static void Main(string[] args)
            {
                try
                {
    
                }
                catch (Exception)
                {
                    Console.WriteLine(DateTime.Now);
                }
    
                try
                {
    
                }
                catch (Exception)
                {
                    Console.WriteLine(DateTime.Now);
    
                }
                finally
                {
                    Console.WriteLine(DateTime.Now);
    
                }
            }
        }
    }
    

    未优化

    .method private hidebysig static void  Main(string[] args) cil managed
    {
      .entrypoint
      // 代码大小       74 (0x4a)
      .maxstack  1
      IL_0000:  nop
      .try
      {
        IL_0001:  nop
        IL_0002:  nop
        IL_0003:  leave.s    IL_001a
      }  // end .try
      catch [System.Runtime]System.Exception 
      {
        IL_0005:  pop
        IL_0006:  nop
        IL_0007:  call       valuetype [System.Runtime]System.DateTime [System.Runtime]System.DateTime::get_Now()
        IL_000c:  box        [System.Runtime]System.DateTime
        IL_0011:  call       void [System.Console]System.Console::WriteLine(object)
        IL_0016:  nop
        IL_0017:  nop
        IL_0018:  leave.s    IL_001a
      }  // end handler
      IL_001a:  nop
      .try
      {
        .try
        {
          IL_001b:  nop
          IL_001c:  nop
          IL_001d:  leave.s    IL_0034
        }  // end .try
        catch [System.Runtime]System.Exception 
        {
          IL_001f:  pop
          IL_0020:  nop
          IL_0021:  call       valuetype [System.Runtime]System.DateTime [System.Runtime]System.DateTime::get_Now()
          IL_0026:  box        [System.Runtime]System.DateTime
          IL_002b:  call       void [System.Console]System.Console::WriteLine(object)
          IL_0030:  nop
          IL_0031:  nop
          IL_0032:  leave.s    IL_0034
        }  // end handler
        IL_0034:  leave.s    IL_0049
      }  // end .try
      finally
      {
        IL_0036:  nop
        IL_0037:  call       valuetype [System.Runtime]System.DateTime [System.Runtime]System.DateTime::get_Now()
        IL_003c:  box        [System.Runtime]System.DateTime
        IL_0041:  call       void [System.Console]System.Console::WriteLine(object)
        IL_0046:  nop
        IL_0047:  nop
        IL_0048:  endfinally
      }  // end handler
      IL_0049:  ret
    } // end of method Program::Main
    

    优化开关开启:

    .method private hidebysig static void  Main(string[] args) cil managed
    {
      .entrypoint
      // 代码大小       19 (0x13)
      .maxstack  1
      .try
      {
        IL_0000:  leave.s    IL_0012
      }  // end .try
      finally
      {
        IL_0002:  call       valuetype [System.Runtime]System.DateTime [System.Runtime]System.DateTime::get_Now()
        IL_0007:  box        [System.Runtime]System.DateTime
        IL_000c:  call       void [System.Console]System.Console::WriteLine(object)
        IL_0011:  endfinally
      }  // end handler
      IL_0012:  ret
    } // end of method Program::Main
    

    很明显可以看到,空的try catch直接消失了,但是空的try catch finally代码是不会消失的,但是也不会直接调用finally内的代码(即还是会生成try代码段)。

    • 分支简化

    代码如下:

    using System;
    using System.Threading.Tasks;
    
    namespace CompileOpt
    {
        class Program
        {
            static void Main(string[] args)
            {
                int x = 3;
                if (x == 3)
                    goto LABEL1;
                else
                    goto LABEL2;
                LABEL2: return;
                LABEL1: return;
            }
        }
    }
    

    未优化的情况下:

    .method private hidebysig static void  Main(string[] args) cil managed
    {
      .entrypoint
      // 代码大小       22 (0x16)
      .maxstack  2
      .locals init (int32 V_0,
               bool V_1)
      IL_0000:  nop
      IL_0001:  ldc.i4.3
      IL_0002:  stloc.0
      IL_0003:  ldloc.0
      IL_0004:  ldc.i4.3
      IL_0005:  ceq
      IL_0007:  stloc.1
      IL_0008:  ldloc.1
      IL_0009:  brfalse.s  IL_000d
      IL_000b:  br.s       IL_0012
      IL_000d:  br.s       IL_000f
      IL_000f:  nop
      IL_0010:  br.s       IL_0015
      IL_0012:  nop
      IL_0013:  br.s       IL_0015
      IL_0015:  ret
    } // end of method Program::Main
    

    优化:

    .method private hidebysig static void  Main(string[] args) cil managed
    {
      .entrypoint
      // 代码大小       5 (0x5)
      .maxstack  8
      IL_0000:  ldc.i4.3
      IL_0001:  ldc.i4.3
      IL_0002:  pop
      IL_0003:  pop
      IL_0004:  ret
    } // end of method Program::Main
    

    优化的情况下,一些分支会被简化,使得调用更加简洁。

    • 跳转简化

    代码如下:

    using System;
    using System.Threading.Tasks;
    
    namespace CompileOpt
    {
        class Program
        {
            static void Main(string[] args)
            {
                goto LABEL1;
                LABEL2: Console.WriteLine("234");
                Console.WriteLine("123");
                return;
                LABEL1: goto LABEL2;
            }     
        }
    }
    

    未优化:

    .method private hidebysig static void  Main(string[] args) cil managed
    {
      .entrypoint
      // 代码大小       32 (0x20)
      .maxstack  8
      IL_0000:  nop
      IL_0001:  br.s       IL_001c
      IL_0003:  nop
      IL_0004:  ldstr      "234"
      IL_0009:  call       void [System.Console]System.Console::WriteLine(string)
      IL_000e:  nop
      IL_000f:  ldstr      "123"
      IL_0014:  call       void [System.Console]System.Console::WriteLine(string)
      IL_0019:  nop
      IL_001a:  br.s       IL_001f
      IL_001c:  nop
      IL_001d:  br.s       IL_0003
      IL_001f:  ret
    } // end of method Program::Main
    

    优化后:

    .method private hidebysig static void  Main(string[] args) cil managed
    {
      .entrypoint
      // 代码大小       21 (0x15)
      .maxstack  8
      IL_0000:  ldstr      "234"
      IL_0005:  call       void [System.Console]System.Console::WriteLine(string)
      IL_000a:  ldstr      "123"
      IL_000f:  call       void [System.Console]System.Console::WriteLine(string)
      IL_0014:  ret
    } // end of method Program::Main
    

    一些多层的标签跳转会得到简化,优化器就是人狠话不多。

    • 临时变量消除

    一些临时变量(中间变量)会被简化消除。代码如下:

    using System;
    using System.Threading.Tasks;
    
    namespace CompileOpt
    {
        class Program
        {
            static void Main(string[] args)
            {
                for (int i = 0; i < 3; i++)
                {
                    Console.WriteLine(i);
                }
                for (int i = 0; i < 3; i++)
                {
                    Console.WriteLine(i + 1);
                }
            }
        }
    }
    

    只显示最关键的变量声明部分,未优化的代码如下:

    .method private hidebysig static void  Main(string[] args) cil managed
    {
      .entrypoint
      // 代码大小       54 (0x36)
      .maxstack  2
      .locals init (int32 V_0,
               bool V_1,
               int32 V_2,
               bool V_3)
      IL_0000:  nop
    

    优化后:

    .method private hidebysig static void  Main(string[] args) cil managed
    {
      .entrypoint
      // 代码大小       39 (0x27)
      .maxstack  2
      .locals init (int32 V_0,
               int32 V_1)
      IL_0000:  ldc.i4.0
    

    很显然,中间的bool型比较变量消失了。

    • 空指令删除

    看第一个例子,很明显,代码中没有了nop字段,程序更加紧凑了。

    编译器版本不同,对应的优化手段也不尽相同,以上只列出了一些,应该还有一些没有讲到的,欢迎补充。

    延伸阅读:.NET中的优化(转载自http://blog.jobbole.com/84712/

    在.NET的编译模型中没有链接器。但是有一个源代码编译器(C# compiler)和即时编译器(JIT compiler),源代码编译器只进行很小的一部分优化。比如它不会执行函数内联和循环优化。

    从优化能力上来讲RyuJIT和Visual C++有什么不同呢?因为RyuJIT是在运行时完成其工作的,所以它可以完成一些Visual C++不能完成的工作。比如在运行时,RyuJIT可能会判定,在这次程序的运行中一个if语句的条件永远不会为true,所以就可以将它移除。RyuJIT也可以利用他所运行的处理器的能力。比如如果处理器支持SSE4.1,即时编译器就会只写出sumOfCubes函数的SSE4.1指令,让生成打的代码更加紧凑。但是它不能花更多的时间来优化代码,因为即时编译所花的时间会影响到程序的性能。

    在当前控制托管代码的能力是很有限的。C#和VB编译器只允许使用/optimize编译器开关打开或者关闭优化功能。为了控制即时编译优化,你可以在方法上使用System.Runtime.Compiler­Services.MethodImpl属性和MethodImplOptions中指定的选项。NoOptimization选项可以关闭优化,NoInlining阻止方法被内联,AggressiveInlining (.NET 4.5)选项推荐(不仅仅是提示)即时编译器将一个方法内联。

    结语

    话说整点这个东西有点什么用呢?
    要说是有助于更好理解.NET的运行机制会不会有人打我...
    说点实际的,有的童鞋在写延时程序时,timer.Interval = 10 * 60 * 1000,作为强迫症患者,生怕这么写不好,影响程序执行。但是,这种写法完全不会对程序的执行有任何影响,我认为还应该推荐,因为增加了程序的可读性,上面的代码段就是简单的10分钟,一看就明白,要是算出来反而可读性差。另外,分支简化也有助于我们专心依照业务逻辑去编写代码,而不需要过多考虑代码的分支问题。其他的用途各位看官自行发挥啦。

  • 相关阅读:
    移动app测试浅析
    Instruments 使用指南
    手机APP测试的几大点
    Python之路【目录】
    Delphi TreeView – 自动展开树形结构
    Delphi TreeView – 自动给标题上加图片
    Delphi 发送邮件 通过Office Outlook
    Delphi Create(nil), Create(self), Create(Application)的区别
    Delphi StringReplace – 替换字符函数
    Delphi MessageBox
  • 原文地址:https://www.cnblogs.com/podolski/p/8987595.html
Copyright © 2020-2023  润新知