C#编译器优化那点事【分分快三计划】

作者:编程技术

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

请记住,任何高级的语言最多只能使用CLR全部特性一个子集。而IL汇编语言允许开发人员获取CLR的所有特性。因此,如果你真的想使用被你的编程语言隐藏掉的CLR特性,你可以在IL汇编器中编写或者使用另一种编程语言来完成你需要的CLR功能。 

CoreFX(.NET Core Libraries)

CoreFX主要包含数个公共库,例如 System.Collections, System.IO, System.Xml等。CoreFX是 .NET Standard Library 的实现,同样的.NET Framework 4.6.3也是基于.NET Standard Library的实现。它们目前都是基于.NET Standard Library1.6版本,具体见下表:

分分快三计划 2

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

当JIT编译器(JITCompiler)函数被调用时,它会知道正在调用的是那个方法,以及该方法是由那个类型定义的。JIT编译器函数随后会在被调用方法所定义的程序集中的元函数内搜索其IL代码的位置。JIT编译器接着验证这些IL代码并将编译成本地CPU指令。本地CPU指令会被保存在动态分配的内存快中。然后JIT编译器将前面内部数据结构中被调用方法的地址替换成包含本地CPU指令的内存块地址。最后,JIT编译器会跳转到该内存块中的代码上。这里的代码就是Writeline方法(含有一个String类型参数的版本)当这些代码执行完,它将返回到Main函数中,Main函数会接着继续执行下面的代码。

中间层是公共库(CoreFX),实现了.NET Standard Library ,囊括了常用系统级操作例如(文件、网络等)。

结语

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

 

总结

本节介绍了.NET Core的构成体系,包括新增的多个编译器以及遵循.NET Standard Library的CoreFX,总体来说.NET Core较之前的.NET Framework 从性能和开发效率上都有很大的提升。关键是首次实现了.NET的完全跨平台能力的基础技术栈。

.NET Core 基于跨平台能力,并没有将与 GUI 高度相关的 API 移植到 .NET Core 内,因此像是 Windows Forms 或是 Windows Presentation Foundation (WPF) 并未移植到 .NET Core。.NET Core 支持控制台应用程序 (Console Application) 以及类库 (Class Library) 类型的项目。

不过微软在其 Universal Windows Platform (UWP) 开发平台使用了 .NET Core,并且利用 .NET Native 技术将其性能提升至十分接近原生码的速度。

ASP.NET Core 则以控制台应用程序驱动其托管环境 Kestrel Server 以支持 ASP.NET Core 程序的运行。


作者:帅虫哥 出处:

参考链接

【1】

【2】

【3】

【4】

【5】

【6】

【7】

optimize only

首先需要了解c#代码编译的过程,如下图:
分分快三计划 3
图片来自

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

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

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

 

CoreCLR & CoreRT

CoreCLR 和 CoreRT 都是.NET Core的运行时(Runtime),
它们提供了与.NET Framework CLR 类似的核心功能(内存管理、程序集加载、安全性、异常、线程管理等),可由面向于运行时的所有语言使用。

CoreRT 和 CoreCLR 不同的是,CoreRT 提供了一套
AOT 的机制,可以将.NET Core程序编译成原生代码,不依赖 .NET 运行时而运行在宿主机器上。除此之外两个运行时大部分功能代码是共享的,比如GC。AOT的优化带来不少好处:

  • 编译后生成一个单文件,包含所有的依赖,包括 CoreRT,无需安装Framework

  • 启动时是机器码,不需要生成机器码,也不要加载JIT编译器

  • 可以使用其他优化编译器,包括 LLILC ,IL to CPP

CoreRT有两个方式生成机器码,第一个使用是直接编译IL成机器码,默认情况下,RyuJIT 作为一个 AOT 编译器将IL编译成机器码,另一个方式是将C#代码编译成C 代码,然后调用对应平台的C 编译器优化编译成机器码。

使用 RyuJIT 编译成机器码

dotnet restore
dotnet build --native --ilcpath <repo_root>bin
ProductWindows_NT.x64.Debugpackagingpublish1

编译生成 C 代码

dotnet restore
dotnet build --native --cpp --ilcpath <repo_root>binProductWindows_NT.x64.Debugpackaging
publish1 --cppcompilerflags /MTd

CoreRT也有不足之处,它需要为不同平台编译一次;但凡事有但是,它允许工程师可以不发布到不想支持的平台(比如某游戏仅支持桌面,不支持手机)。

注:这两个命名在.NET Core RC2 版本中均无法使用,按照官方说法是在当前版本中已经移除这个命令了,具体等6月27日正式版发出后才知道最后的情况

- 跳转简化

代码如下:
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

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

 

.NET Core 代码开发、部署、运行过程

分分快三计划 4

从上图可以看到使用JIT编译和使用AOT编译源代码并运行程序是两种不同的流程。

如果使用JIT编译器部署程序时只需要将程序打包为IL的assemblies,在方法第一次执行前编译器将IL编译为目标机机器码(Native code),而AOT编译会在编译时将源代码直接编译为目标机机器码。

AOT将源代码编译为机器码,拥有如下特性:

  • 用静态代码替换反射,例如如果一个值类型(value type)没有重写 ValueType.Equals 的equals的方法,默认情况判断相等,会使用反射找到filedinfo以确定type是否相等,然后再比较value是否相等。而在AOT编译中由于替换了反射因此只能比较value是否相等。

  • 依赖的第三方类库以及.NET Libraries均打包至最终编译的程序中。

  • 打包后的程序运行在一个精简版的运行时上(CoreRT)主要包含垃圾回收器,而运行时也会打包在app文件中。

  • 虽然编译时会替换反射代码,但遇动态反射代码无能为力,运行时若遇动态反射调用则会因找不到对应的元数据及实现而抛出异常。解决办法是编译前配置运行时指令文件(Runtime directive file)指定需要用到的程序集。

- 从未使用变量

代码如下:
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)也消失了。
所以,整个没有使用的变量,在设置为优化的时候,就直接消失了,就像从来没有写过一样。

Note:如果应用程序是在x86版本的windows上或者是WoW64上跑的,JIT编译器会产生x86指令。如果应用程序是在x64版本的windows或者Itanlum版本的windows上跑,JIT编译器会分别产生x64或者IA64指定。

前文介绍了.NET Core 在整个.NET 平台所处的地位,以及与.NET Framework的关系(原文链接),本文将详细介绍.NET Core 框架的构成和各模块主要功能,以及如何实现跨平台。

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

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

 

  • 简析 .NET Core 构成体系
    • Roslyn 编译器
    • RyuJIT 编译器
    • CoreCLR & CoreRT
    • CoreFX(.NET Core Libraries)
    • .NET Core 代码开发、部署、运行过程
    • 总结

- 简单分支检查

如果swtich写了两个以上的相同条件,或者分支明显无法访问到,都会弹出提示。  
![](https://images2018.cnblogs.com/blog/616093/201805/616093-20180501011749912-993405991.png)

●JIT编译器能监测到一个应用程序是否运行在一些新型的CPU(奔腾4CPU)上运行,并产生利用这些新型CPU提供的特殊指令的本地代码。而非托管应用程序通常被编译为面向具有最小通用功能集合的CPU平台,一般会避免使用新型CPU提供的特殊指令。

RyuJIT 编译器

在程序运行中需要执行某一个方法,首先需要将已经编译好的IL转换本机的机器码,而这个任务就交给了RyuJIT。它是新一代JIT编译器,第一次实现了AMD64的架构,RyuJIT能够比JIT64(上一代编译器)更快地生成代码,以提高程序运行效率(测试详情链接)。

- 临时变量消除

一些临时变量(中间变量)会被简化消除。代码如下:
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型比较变量消失了。

如果宿主机器只有一个CPU,那么对于该段代码,JIT编译器将不会产生任何CPU执行。在这种情况下,针对宿主机器的本地代码机会得到更好的调整;代码将变得更小,执行速度也会更快

在CoreFx下是运行时环境,.NET Core 包含了两种运行时(CoreCLR、CoreRT),CoreCLR是一种基于即时编译程序(Just in time compiler,JIT)的运行时,它使用了跨平台开源的编译器RyuJIT,而CoreRT是使用提前编译器(Ahead of time compiler,AOT)的运行时,它既可以使用RyuJIT来实现AOT编译也可以使用其他的AOT编译器。由于AOT提前编译IL成了机器码,在移动设备上也具有更好的启动速度和节能性。

- 常量计算

在写程序的时候,有时能看见代码下面划了一道红波浪线,那就是编译器动态检查。常量计算,就是这样,编译器会计算常量,帮助判断其他错误。  
![](https://images2018.cnblogs.com/blog/616093/201805/616093-20180501011736735-1016278688.png)

 

Roslyn 编译器

Roslyn编译器用于将C#或VB.NET代码编译为程序集(assembly),它的编译过程是一个管道式的处理过程一共包含4个步骤,具体过程见下图。

分分快三计划 5

A. Parser(解析)

根据语法对源代码进行解析。

B. Declaration (声明)

为代码生成元数据(metadata),元数据是一个数据表的集合,描述了在当前代码中定义的数据类型和成员,同时也描述了引用的类型及成员。

C. Bind(绑定)

将生成的IL代码与描述它的元数据绑定在一起,生成托管模块(managed module)。

D. Emit(生成)

将一个或多个托管模块合并生成程序集(assembly)。

- 分支简化

代码如下:
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

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

●在应用程序运行时,CLR能够分析评估代码的执行情况,并有选择的重新将IL代码编译为本地代码。根据观察到的执行模式,被编译的代码可以被重新组织以提高分支预测的成功率。当前的CLR并不支持这个功能,但是将来的版本会的。

分分快三计划 6

- 空指令删除

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

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

 

最后还要提到一个开源的跨平台源代码编译器Roslyn,它有别于刚才两个编译器,JIT和AOT编译器主要用于将IL编译成本机机器码,而Roslyn是将C# 或 VB.NET 代码编译成程序中间语言(intermediate language,IL)。

延伸阅读:.NET中的优化(转载自)

在.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)选项推荐(不仅仅是提示)即时编译器将一个方法内联。

在执行一个方法的时候,它的IL首先要转化成当地的CPU指令。这是CLR中(JIT--just-in-time)编译器的工作。

上图描述了 .NET Core的系统构成,最上层是应用层,是开发基于UI应用的框架集,包括了ASP.NET Core(用于创建web app),和 UWP(用于创建Windows10 app)。

- 空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代码段)。

 

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

 分分快三计划 7

局限

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

分分快三计划 9

optimize- and optimize

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

 

- 使用未赋值变量

不多说,看图。  
![](https://images2018.cnblogs.com/blog/616093/201805/616093-20180501011804349-1464238631.png)

 

- 未使用变量

不多说明,直接看图。  
![](https://images2018.cnblogs.com/blog/616093/201805/616093-20180501012150677-566406144.png)

 

选择/optimize-,由C#编译器产生的未优化的IL代码包含了很多无操作(no-operation NOP)的指令,并分支跳转到下一行代码。这些指令是为了能够在调试的时候使用Visual Stidio中的编辑-继续功能,还有一些额外的指令也能够使断点调试(比如一些控制流程的指令:for , while ,do ,if ,else, try ,catch 还有finally 语法块)更方便。当生产优化的IL代码,在C#编译器将通过删除多余的NOP和分支指令。此外,某些功能可能无法正常工作的评价时,调试器内进行。但是,IL代码比较小,使产生的EXE / DLL文件更小,使我们这些想了解IL是如何产生的更容易。

分分快三计划 10

你还要知道CLR的JIT编译器可以像非托管C 编译器后端一样优化本地代码。另外,产生优化代码可能花费更多的时间,但是经过优化后的代码的执行效率显得会更高。

如前所述,托管程序集同时包含元数据和IL。 IL是由Microsoft在咨询了一些商业和学术上的语言编译器作者后创建的一种独立于CPU的机器语言。 IL是一种比大多数CPU机器语言更高层次的语言。IL可以访问和操纵的对象类型,创建和初始化对象,调用对象的虚方法,操作数组元素。它甚至能抛出和捕获异常的错误处理。你可以认为IL是一种面向对象的机器语言。 

 

JIT编译器将本地存储在动态内存之中。这以为这当应用程序关闭时,编译生成的本地代码将被丢弃。这样,如果我们以后再次运行该应用程序,或者同时运行该应用程序的两个不同实例,JIT编译器需要再次将同样的IL代码编译成本地指令。

 

重点:我觉得这种语序在编程语言间方便切换和高度集成的能力是CLR一个非常厉害的功能(an awesome feature of the CLR)。不行的是,许多开发人员经常会护士这个特性。我们知道,C#和Visual Basic善于进行I/O操作,而APL在高级工程和金融计算时则非常突出。通过CLR,我们可以用C#来编写应用程序的I/O部分,然后再用APL来编写应用程序的工程计算部分,CLR提供的语言之间的继承能力是前所未有的,这也是的混合编程值得很多开发项目考虑。

 

 

了解CLR提供了哪些功能的唯一途径就是阅读有关CLR的文档。本书重点讲述CLR的一些特性,以及它们在C#语言中哪些是提供的,哪些是没有提供的。我想很多其他的书籍和文章都会从一门语言的角度去探讨CLR,并且大多数开发人员也倾向于相信CLR仅提供了他选所选择的语言所展现的功能。如果大家选额的语言能够实现想做的事情,那么这种理解也并非坏事,虽然有些混乱。

 

当Main方法第一次调用writeline,JITCompiler函数将被调用,该函数负责将一个方法的IL代码编译成本地CPU指令。因为IL代码是被“即时(just-in-time)”编译的,所以CLR的这一部分通常被称作JITter或者JIT编译器。

 

1.4 Executing Your Assembly’s Code 执行程序集代码

你可能不会相信,托管应用程序有可能在性能方面超过非托管应用程序,但很多人(包括我)都认为这是有可能的。这里有很多原因。举例来说,当JIT编译器在运行时将IL代码编译成本地代码,编译器对于执行环境的了解要多余非托管编译器。下面是托管代码有可能超过非托管代码的一些地方:

 

有两个C#编译器选项会影响代码的优化:optimize和debug。下面的表格将向我们展示我们这些选项在 C#编译器生成IL代码、和JIT编译器生成本地代码的质量上的影响。

 

图1-4展示了一个方法第一次被调用时的情况

在Main方法执行之前,CLR会检查Main中代码引用到的所有类型。这会导致CLR分配一个内部的数据结构,该数据结构用于管理对所引用到的类型的访问。在图1-4中,Main方法只引用了一个Console类型,CLR将会为此分配一个单独的内部结构。在这个内部数据结构中,Console类型中的每个方法都有一个条目(entry),每个条目中将保存有一个方法实现代码的地址。当初始化这个结构时,CLR将把每一个条目设置为CLR内部的一个没有正式记录的函数,我们暂且成该阐述为JITCompiler。

通常,开发人员会使用一门高级语言编程,如C#和C / CLI中,或Visual Basic。这些高级语言的编译器产生IL。然而,正如任何其他机器语言,IL可以用汇编语言编写,微软也确实提供了IL汇编器,ILAsm.exe。微软还提供了IL反汇编,ILDasm.exe。 

如果你经验告诉你CLR的JIT编译器不能为应用程序提供期望的性能,那么你可以使用NGen.exe工具,它是和.NET框架SDK一起发布的一个工具。该工具可以将一个程序集中的所有的IL代码转化为本地代码,并将结果代码保存在磁盘上的一个文件中。在运行时程序集被加载的时候,CLR将自动检查是否有该程序集存在,如果存在,CLR将加载预编译的代码,不再需要额外的运行时编译。注意NGen.exe对于实际执行环境的假设肯定比较保守,因此NGen.exe产生的代码将不如JIT编译器产生的代码那样高度优化。

对于大多数的应用程序,JIT编译引起的损失是微不足道的。而且,大部分的也能够用程序经常反复调用同一个方法。这样,在应用程序执行时,这些方法引起的也只是一次性能损失。而且通常方法内部执行所花费的时间要比方法调用本身索花费的时间要多的多。

 

对于那些有非托管C或C 背景的开发者,可能会对这里的一些性能差别有所顾虑。毕竟,非托管代码是针对某一特性CPU平台所变异的,当他们被调用是,这些代码便会立即执行。而在托管环境中,代码的编译要经过两个阶段才能完成。首先,编译器要所秒源代码,把它编译成IL代码,但是要执行这些IL代码,它们还必须在运行时被编译成本地CPU指令,这项工作通常需要分配更多的时间。

 

相信我,我也是从C/C 过来的,我对这些额外的开销也非常关心,也有过相当的怀疑。无可争议,出现在运行时的第二阶段编译不仅会损伤系统性能,还要分配额外的动态内存。但是微软针对心跟那个已经做了很多工作,开销已经被降到最低。

当你在Visual Studio中创建一个新的C#项目,该项目的调试(Debug)配置是/optimize-和/debug:full选项,发布(Release)的配置/optimize 和/debug:pdbonly。

这样,一个方法只有在被首次调用时才会产生一些性能损失。所有对该方法后续的调用都将以本地代码做全速执行,因为本地代码不再需要验证和编译。

 

●JIT编译器能监测到正在运行的机器上某些总是返回错误的布尔测试。例如,考虑有如下代码的一个方法if (numberOfCPUs > 1){........}

此外,只有当你选择了/debug( /full/pdbonly)编译器产生一个程序数据库(PDB)文件。PDB文件可以帮助调试器查找本地变量,并将IL指令映射到源代码。在/debug:full选择下,JIT编译器被告知将会调试程序集,JIT编译器会找到每句IL指令的本地代码。您可以使用Visual Studio的JIT调试器(JIT Debugger)的调试功能来调试。没有了/debug:full,JIT编译器不会这么做,默认情况下,不会跟踪IL到本机代码信息,使得JIT编译器运行更快一点,使用的内存更少些。如果您启动Visual Studio调试器的进程,它迫使JIT编译器跟踪IL到本地代码的信息(无论/ debug开关),除非你关闭VisualStudio中的在加载模块禁止JIT优化(只限于托管)选项。

 

现在,Main第二次调用WriteLine。由于writeLine已经被验证以及编译过了,所以这次将直接调用到内存快,完全跳过了JIT编译器函数。WriteLine执行完后,同样返回到Main。

 

本文由分分快三计划发布,转载请注明来源

关键词: 分分快三计划 C# CLR via C# NET CLR