歡迎光臨
每天分享高質量文章

C#編譯器最佳化那點事

作者:波多爾斯基

連結:https://www.cnblogs.com/podolski/p/8987595.html

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

贊(0)

分享創造快樂