CLR 全面透彻解析

Silverlight 4 中的新增功能和改进的性能

Andrew Pardoe

Silverlight 4 的最大变化之一是核心执行引擎迁移到了新版 CLR 上。 从 Microsoft .NET Framework 2.0 直到 .NET Framework 3.5 SP1,.NET Framework 的每个版本均采用相同的 CLR 作为核心。 .NET Framework 4 做了一些改动,某些甚至称得上是翻天覆地的变化,例如,分离出易于下载的客户端配置文件,并通过优化本机二进制文件布局缩短启动时间,不过我们始终采用就地更新,保持了高兼容性。

有了 .NET Framework 4,我们不仅能对 CLR 自身进行较大更改,还保持了对以前各个版本的高兼容性。 Silverlight 4 采用新版 CLR 作为其 CoreCLR 的基础,并将其桌面的所有改进均应用于网络。 其中最显著的运行时增强功能即默认垃圾收集器 (GC) 行为,以及每次执行 Silverlight 程序时不再实时 (JIT) 编译 Silverlight Framework 二进制文件。 我们对基类进行了全面改进,包括改进独立存储、修改 System.IO,以从运行的具提升权限的 Silverlight 应用程序直接访问文件系统。

下面先大致了解一下 CoreCLR GC 的工作原理。

分代 GC

CoreCLR 采用相同的 GC 作为桌面 CLR。 它是分代 GC,意思是其操作基于如下原则:最近分配的对象最有可能在下次收集时成为垃圾。 该方法在小范围内是显而易见的:函数返回后程序不能即刻访问函数局部变量。 该方法通常也适用于较大范围:程序往往会保留在程序执行过程中所存在对象的某些全局状态。

对象通常在最新一代(我们称之为第 0 代)中分配,并在垃圾收集过程中提升到上几代(如果对象存在于收集中),直至到达最老一代(在当前 CLR GC 实现中为第 2 代)。

CLR GC 中还有一代,名为大型对象堆 (LOH)。 大型对象(当前定义为超过 85,000 字节的对象)直接分配到 LOH 中。 该堆与第 2 代同时收集。

如果没有分代 GC,那么 GC 在收集未用内存之前,需要检查整个堆,才能知道哪些内存可访问,哪些内存是垃圾。 有了分代 GC,每次收集时便无需查看整个堆。 由于收集持续时间与要收集的代大小息息相关,因此我们对 GC 进行了优化,降低了第 2 代(和 LOH)的收集频率。 对于小型堆,收集几乎在瞬间完成,堆越大,收集时间越长,第 0 代收集只需数十微秒。

在大多数程序中,第 2 代和 LOH 要比第 0 代和第 1 代大得多,因此检查这些堆的全部内存需要更长时间。 请记住,当 GC 收集第 1 代时始终会收集第 0 代,在收集第 2 代时始终会收集所有堆。 这就是为何第 2 代收集也称为完整 收集的原因。 有关不同堆收集性能的详细信息,请参见 2009 年 10 月的“CLR 全面透彻解析”专栏:msdn.microsoft.com/magazine/ee309515

并发 GC

执行垃圾收集的简单算法是令执行引擎中止所有程序线程,然后 GC 执行其工作。 我们称此收集类型为拦截 收集。 这样一来,GC 会移动不固定的内存,例如,将内存从一代移动到下一代,或压缩稀疏内存段,而程序并不知道已发生了变化。 如果在执行程序线程时移动了内存,则对程序而言,像是内存已损坏。

不过,也有某种垃圾收集工作不会变动内存。 自 CLR 第一版起,我们提供了一种执行并发 收集的 GC 模式。 此模式在整个收集过程中,无需中止程序线程便可执行大部分完整 GC 工作。

有许多功能 GC 无需更改程序可见的任何状态即可完成,例如,GC 可以查找所有程序可访问的内存。 在 GC 检查堆时,程序线程可继续执行。 在执行实际收集之前,GC 只需在检查内存时搜索发生变化的内存,例如,如果程序分配了新的对象,则该对象需要标记为可访问。 最后,GC 要求执行引擎拦截所有线程(就像在非并发 GC 中),并继续完成此时的所有可访问内存。

后台 GC

并发 GC 在大多数情况下都能取得出色的效果,但还有一种情况例外,我们对此做了大幅改进。 如前所述,内存是在最新一代或 LOH 中分配的。 第 0 代和第 1 代位于单个段中,我们称之为临时 段,因为它用于保存短期对象。 临时段填满后,由于临时段中已无空间,因此程序无法再创建新的对象。 这时 GC 需要对临时段执行收集,以释放部分空间,令分配继续。

并发 GC 的问题在于,在进行并发收集的同时,无法执行以上操作。 当程序线程运行时,GC 线程不能移动任何内存(因而无法将旧的对象提升到第 2 代),而且,由于已存在 GC,因而无法启动临时收集。 但是 GC 必须在临时段中释放部分内存,程序才能继续进行。 这样一来便陷入了困境:不得不中止程序线程的原因,不是并发 GC 更改了程序可见的状态,而是因为程序无法分配。 如果并发收集在搜索所有可访问内存时发现临时段已满,它会中止所有线程,并执行拦截压缩。

以上难题正是促使我们开发后台 GC 的动因。 其工作原理类似并发 GC,始终在后台自己的线程上执行大部分完整收集工作。 二者之间的主要差别在于,后台 GC 允许在执行完整收集时进行临时收集。 也就是说,当临时段满时,程序可继续执行。 GC 只是执行临时收集,其他一切照常运行。

后台 GC 对程序延迟的影响十分显著。 在运行后台 GC 时,我们观察到,程序执行中止次数大为减少,因而缩短了持续时间。

后台 GC 是 Silverlight 4 的默认模式,只能用于 Windows 平台,因为 OS X 缺乏 GC 运行于后台或并发模式下所需的某些 OS 支持。

NGen 性能改进

托管语言(如 C# 和 Visual Basic)编译器不会直接生成可在用户计算机上执行的代码。 这些编译器将生成名为 MSIL 的中间语言,然后在程序执行时使用 JIT 编译器编译为可执行代码。

使用 MSIL 从安全性到可移植性都有很多好处,但它与 JIT 编译的代码之间存在两点折衷。 首先,必须先编译大量 .NET Framework 代码,才能编译和执行程序的 Main 函数。 这意味着,用户在程序开始运行之前不得不等待 JIT。 其次,对于用户计算机上执行的每一个 Silverlight 程序,都必须编译所使用的所有 .NET Framework 代码。

NGen 有助于解决这两个问题。 NGen 会在安装时编译 .NET Framework 代码,这样当程序开始执行时代码已经编译好。 采用 NGen 编译的代码一般可由多个程序共享,因而当运行两个或多个 Silverlight 程序时,用户计算机上的工作集将减少。 想要了解有关 NGen 如何改进启动时间和工作集的详细信息,请参见 2006 年 5 月的“CLR 全面透彻解析”专栏:msdn.microsoft.com/magazine/cc163610

.NET Framework 代码占据了 Silverlight 程序的大部分,因此无 NGen 的 Silverlight 2 和 3 在启动时间上存在明显差距。 JIT 编译器在优化和编译每个程序启动路径上的库代码时,所需的时间太长。

在 Silverlight 2 和 3 中,对于此问题的解决方法是不让 JIT 编译器优化代码生成。 代码仍需编译,但由于 JIT 编译器只生成简单代码,因而不需要太少的编译时间。 与传统桌面应用程序相比,为大量 Internet 应用程序 Web 方案编写的大多数程序都很小,而且运行时间短。 更重要的是,这些程序通常都是交互式程序,这意味着其大部分时间都是在等待用户输入。 在 Silverlight 2 和 3 应用中,快速启动远比生成优化代码重要。

在 Silverlight Web 应用程序的发展过程中,我们不断对其改进以保持积极的用户体验。 例如,在 Silverlight 3 中,我们增加了对从桌面安装和运行 Silverlight 应用程序的支持。 通常情况下,相比传统 Web 方案中的交互式小型应用程序,这些应用程序更大、功能更多。 Silverlight 自身添加了许多大计算量的功能,例如,在 Windows 7 中支持触摸输入,以及如 Bing 地图网站中所示的丰富的照片处理功能。 所有这些方案都要求对代码进行优化,以便更有效地执行。

Silverlight 4 提供了出色的启动性能和经优化的代码。 现在,Silverlight 中的 JIT 编译器采用与桌面 .NET 应用程序中同样的优化机制。 由于我们为 Silverlight .NET Framework 程序集启用了 Ngen,因此可以执行优化。 当您安装 Silverlight 时,我们会自动编译 Silverlight 运行时中的所有托管代码,并将这些代码保存在您的硬盘中。 当用户执行您的 Silverlight 程序时,程序无需等待任何 Framework 代码编译,便可开始执行。 还有一点同样重要,我们现在可优化您 Silverlight 程序中的代码,以便程序运行更快,并且我们可以在运行于用户计算机上的多个 Silverlight 程序之间共享 Framework 代码。

在安装过程中,Silverlight 4 为 .NET Framework 程序集创建本机映像。 对于某些应用程序而言,启动性能是唯一重要的性能。 以 Notepad 为例,快速启动很关键,不过一旦您开始键入文字,Notepad 的运行速度便不那么重要(假设其运行速度超过您的键入速度)。 对于此类程序,JIT 编译应用程序启动代码所需的时间可能会导致性能降低。 在 Silverlight 4 中,大多数应用程序的启动要快上 400 ms 到 700 ms,在执行过程中性能最多可提高 60%。

基类库 (BCL) 是托管 API 的核心,现在 Silverlight 4 中的 NGen 可支持该库。 下面来看看 BCL 中有哪些新功能。

BCL 新增功能

Silverlight 4 中许多 BCL 新增强功能也是 .NET Framework 4 的新增功能,这些在相关上下文中已有详述。 此处只简要介绍一下 Silverlight 4 中的新增功能。

可以使用代码约定,在 Silverlight 代码中以内置方式表达前置条件、后置条件和对象不变量。 代码约定可用在代码中更好地表达假设条件,并有助于及早发现错误。 使用代码约定还有其他许多好处。 有关详细信息,请访问 Melitta Andersen 2009 年 8 月的“CLR 全面透彻解析”专栏:msdn.microsoft.com/magazine/ee236408、Code Contracts DevLabs 网站 msdn.microsoft.com/devlabs/dd491992 以及 BCL 团队博客 blogs.msdn.com/bclteam

元组最常用于返回方法的多个值, 常用在函数式语言(如 F#)和动态语言(如 IronPython)中,使用方法像 Visual Basic 和 C# 一样简单。 有关元组设计的详细信息,请参见 Matt Ellis 2009 年 7 月的“CLR 全面透彻解析”专栏:msdn.microsoft.com/magazine/dd942829

Lazy<T> 为延迟初始化对象提供了便捷方法。 应用程序可以使用延迟初始化技术将数据的加载或初始化推迟至首次需要数据时。

Silverlight 4 SDK 在 System.Numerics.dll 中新增了 BigInteger 和 Complex 数值数据类型。 BigInteger 用于表示任意精度的整数,Complex 用于表示带实数和虚数部分的复数。

Enum、Guid 和 Version 现在也像大多数其他 BCL 数据类型一样支持 TryParse,从而可以更有效地根据字符串创建实例而不会引发异常或错误。

Enum.HasFlag 是新增的一个便捷方法,用于轻松检查 Flags 枚举是否设置了标志,从而无需记住如何使用位运算符。

String.IsNullOrWhiteSpace 是用于检查字符串是否为 Null、为空或只包含空白的便捷方法。

String.Concat 和 Join 重载现在采用 IEnumerable<T> 参数。 这些新的 String.Concat 和 Join 重载可将实现 IEnumerable<T> 的任意集合连接在一起,而无需先将集合转换为数组。

Stream.CopyTo 简化了在一行代码中从一个流中读取内容然后写入另一个流的操作。

除上述新功能之外,我们还对独立存储进行了改进,并启用了受信任的 Silverlight 应用程序,以通过 System.IO 直接访问文件系统部分。

独立存储增强功能

独立存储是一个虚拟的文件系统,Silverlight 应用程序可通过它在客户端上存储数据。 有关 Silverlight 中独立存储的详细信息,请参见 2009 年 3 月的“CLR 全面透彻解析”专栏:msdn.microsoft.com/magazine/dd458794

Silverlight 4 中独立存储最显著的改进是性能方面。 自 Silverlight 2 发布以来,我们从开发人员那收到了许多有关独立存储性能的反馈。 在 Silverlight 3 中,我们做了一些改进,大大提高了独立存储的数据读取性能。 在 Silverlight 4 中,我们更进了一步,解决了开发人员在对独立存储写入数据时遇到的性能瓶颈。 总之,独立存储的性能在 Silverlight 4 中有了大幅提高。

另外,开发人员还曾反映,独立存储中没有重命名或复制文件的简单方法。 要重命名文件,不得不手动读取原始文件、创建并写入一个新文件,然后删除原始文件。 重命名目录时也只能采取类似的方法,甚至需要更多代码行,尤其是当要重命名的目录中还包含子目录时。 这种方法虽然可行,但需要编写较多代码,而且也不如直接告知 OS 重命名磁盘上的文件或目录那么有效。

在 Silverlight 4 中,我们在 IsolatedStorageFile 类中增加了一些新方法,调用这些方法后,只需一行代码即可高效执行以上操作:CopyFile、MoveFile 和 MoveDirectory。 我们还增加了以下新方法,以提供有关独立存储中文件和目录的更多信息:GetCreationTime、GetLastAccessTime 和 GetLastWriteTime。

我们在 Silverlight 4 中新增了另一个 API,IsolatedStorageFile.IsEnabled。 在以前的版本中,要确定独立存储是否启用的唯一方式是尝试使用独立存储,然后捕获后续的 IsolatedStorageException,如果独立存储禁用则会引发该异常。 现在,可以使用新的 IsEnabled 静态属性来轻松确定独立存储是否启用。

如今,很多浏览器,如 Internet Explorer、Firefox、Chrome 和 Safari,在浏览历史记录、Cookie 和其他不保留的数据时都支持隐私浏览模式。 因此,当浏览器处于隐私模式下时,Silverlight 4 支持隐私浏览设置,可防止应用程序访问独立存储并将信息存储在您的本地计算机上。 在这种情况下,IsEnabled 属性将返回错误,且任何使用独立存储的企图都将引发 IsolatedStorageException,当用户明确禁用独立存储时同样如此。

文件系统访问

Silverlight 应用程序运行于部分信任的安全沙箱中。 安全沙箱限制了对本地计算机的访问,并对应用程序设置了许多约束,以防止恶意代码造成伤害。 例如,部分信任 Silverlight 应用程序无法直接访问文件系统。 如果应用程序需要在客户端上存储数据,唯一的方法是将数据存储在独立存储中。 要访问范围更大的文件系统,只能通过 OpenFileDialog 或 SaveFileDialog 来实现。

Silverlight 3 增加了在浏览器之外安装和运行应用程序的功能。 这产生了一些有趣的脱机方案,不过此类应用程序仍旧运行在与运行于浏览器内的应用程序同样的沙箱中。 Silverlight 4 允许浏览器外的应用程序将自身配置为运行于提升的信任等级下。 此类受信任的应用程序在安装后可以绕过沙箱的某些限制。 例如,受信任的应用程序可以访问用户文件、不受跨域访问限制地使用网络、绕过用户同意和初始化要求、访问本机 OS 功能等。

用户在安装需要提升的信任等级的应用程序时,正常安装提示将被一条警告信息取代,警告用户该应用程序可以访问用户数据,只应当从受信任的网站进行安装。

受信任的应用程序可以在 System.IO 中使用 API,以直接访问文件系统中的以下用户目录:MyDocuments、MyMusic、MyPictures 和 MyVideos。 目前还不允许对以上目录之外的目录进行文件操作,否则将导致 SecurityException。 对于这些目录,允许执行包括读和写在内的一切文件操作。 例如,受信任的相册应用程序可以直接访问 MyPictures 目录下的所有文件。 受信任的视频编辑应用程序可以将电影保存到 MyVideos 目录中。

由于这些目录的文件系统路径因不同基础 OS 而异,因此在应用程序中不应对这些路径进行硬编码。 Windows 与 Mac OS X 的文件系统路径截然不同,Windows 各个版本之间的路径也可能不同。 为在所有平台上正常工作,应当使用 System.Environment.GetFolderPath 获取这些目录的文件系统路径。 以下代码使用 Environment.GetFolderPath 获取 MyPictures 目录的文件系统路径,并使用 System.Directory.EnumerateFiles 方法在 MyPictures(及其子目录)中查找后缀为 .jpg 的所有文件,然后将每个文件路径添加到 ListBox 中:

if (Application.Current.HasElevatedPermissions) {
  string myPictures = Environment.GetFolderPath(
    Environment.SpecialFolder.MyPictures);
  IEnumerable<string> files = 
    Directory.EnumerateFiles(myPictures, "*.jpg", 
    SearchOption.AllDirectories);
  foreach (string file in files) {
    listBox1.Items.Add(file);
  }
}

以下代码显示了如何从受信任的应用程序,在用户的 MyDocuments 目录中创建一个文本文件:

if (Application.Current.HasElevatedPermissions) {
  string myDocuments = Environment.GetFolderPath(
    Environment.SpecialFolder.MyDocuments);
  string filename = "hello.txt";
  string file = Path.Combine(myDocuments, filename);

  try {
    File.WriteAllText(file, "Hello World!");
  }
  catch {
    MessageBox.Show("An error occurred.");
  }
}

System.IO.Path.Combine 用于将 MyDocuments 的路径与文件名结合在一起,并在二者之间插入基础平台相应的目录分隔符(Windows 采用 \,Mac 采用 /)。 File.WriteAllText 用于创建文件(如果文件已存在,则覆盖),并将语句“Hello World!”写入该文件。

更优性能和更多功能

如上所述,Silverlight 4 中的新版 CLR 对运行时和基类都做了改进。 全新的 GC 行为、Silverlight Framework 程序集中的 Ngen,以及增强的独立存储性能,这一切都表明 Silverlight 4 上的应用程序将启动更快、运行更出色。 BCL 增强功能使应用程序只需更少代码即可执行更多功能,并且,诸如受信任的应用程序可访问文件系统等新功能推动了更多优秀应用程序方案的涌现。

Andrew Pardoe 是 Microsoft 的 CLR 项目经理。其工作领域涉及桌面和 Silverlight 运行时执行引擎的多个方面。您可以通过电子邮件地址与他联系:andrew.pardoe@microsoft.com

Justin Van Patten 是 Microsoft CLR 小组的项目经理,主要从事基类库的研究。您可以通过 BCL 小组博客与他联系:https://blogs.msdn.com/b/bclteam/

衷心感谢以下技术专家对本文的审阅:Surupa Biswas、Vance MorrisonMaoni Stephens