C#編譯器優化那點事

使用C#編寫程序,給最終用戶的程序,是須要使用release配置的,而release配置和debug配置,有一個關鍵區別,就是release的編譯器優化默認是啓用的。
優化代碼開關即optimize開關,和debug開關一塊兒,有如下幾種組合。
html

在Visual Sutdio中新建一個C#項目時,
項目的「調試」(Debug)配置的是/optimize-和/debug:full開關,
而「發佈」(Release)配置指定的是/optimize+和/debug:pdbonly開關c#

optimize-/+決定了編譯器是否優化代碼,optimize-就是不優化了,可是一般,有一些基本的「優化」工做,不管是否指定optimize+,都會執行。ide

optimize- and optimize+

該項功能主要用於動態語義分析,幫助咱們更好地編寫代碼。函數

  • 常量計算

    在寫程序的時候,有時能看見代碼下面劃了一道紅波浪線,那就是編譯器動態檢查。常量計算,就是這樣,編譯器會計算常量,幫助判斷其餘錯誤。
    工具

  • 簡單分支檢查

    若是swtich寫了兩個以上的相同條件,或者分支明顯沒法訪問到,都會彈出提示。
    性能

  • 未使用變量

    很少說明,直接看圖。
    優化

  • 使用未賦值變量

    很少說,看圖。
    spa

侷限

使用變量參與計算,隨便寫一個算式,就能夠繞過一些檢查,雖然咱們看來是明顯有問題的。
.net

optimize+ only

首先須要瞭解c#代碼編譯的過程,以下圖:

圖片來自http://www.cnblogs.com/rush/p/3155665.htmldebug

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分鐘,一看就明白,要是算出來反而可讀性差。另外,分支簡化也有助於咱們專心依照業務邏輯去編寫代碼,而不須要過多考慮代碼的分支問題。其餘的用途各位看官自行發揮啦。

相關文章
相關標籤/搜索