2016 年 3 月

第 31 卷,第 3 期

编译器 - 使用后台 JIT 的托管的按配置优化

作者 Hadi Brais | 2016 年 3 月

一般来说,编译器能够正常执行性能优化。也就是说,无论在运行时实际执行的是什么代码,优化都可以提升性能。例如,通过循环展开实现矢量化。此优化将转换一个循环,不对单组操作数上的循环主体执行单个操作(如添加不同数组中存储的两个整数),而是同时对多组操作数执行同一操作(添加四对整数)。

另一方面,编译器还试探性地执行极其重要的优化。这意味着,编译器不确定这些优化后的代码能否真正更好地在运行时执行。寄存器分配和函数内联这两个最重要的优化都属于此类(可能属于所有类别)。您可以在执行这类优化时通过一次或多次运行应用并在记录所执行的代码的同时为其提供典型用户输入,从而帮助编译器做出更好的决定。

已收集的有关应用执行情况的信息称为配置文件。然后,编译器可以使用此配置文件使它的一部分优化更为高效,这通常能够大大提高速度。这种技术称为“按配置优化 (PGO)”。如果您编写的代码可读性强、易于维护、采用了好的算法、最大程度地扩大了数据访问局部性、最大程度地减少了对锁的争用,并且启用了所有可能的编译器优化,但仍对产生的性能不满意,应该使用此技术。通常而言,可以使用 PGO 提升代码的其他特性,而不仅仅是性能。然而,本文中讨论的技术只能用于提升性能。

在之前的一篇文章 (msdn.com/magazine/mt422584) 中,我已经详细讨论了 Microsoft Visual C++ 编译器中的本机 PGO。我为那篇文章的读者带来了一些很棒的消息。使用托管的 PGO 更为简单。我在本文中讨论的后台 JIT 功能(也称为多核 JIT)尤为简单。但这是一篇高级的文章。三年前,CLR 团队撰写了一篇介绍性博客文章 (bit.ly/1ZnIj9y)。Microsoft .NET Framework 4.5 和所有更高版本中均支持后台 JIT。

托管的 PGO 技术分为三种:

  • 使用 Ngen.exe 将托管的代码编译为二进制代码(此过程称为 preJIT),然后使用 Mpgo.exe 生成表示常见使用方案的配置文件,这些使用方案可用于优化二进制代码的性能。这与本机 PGO 非常类似。我将这个技术称为静态 MPGO。
  • 为中间语言 (IL) 方法首次执行 JIT 编译时,生成的已检测二进制代码将记录有关要执行方法的那些部分的运行时信息。然后,使用该内存中的配置文件重新为 IL 方法执行 JIT 编译,从而生成高度优化的二进制代码。这也类似于本机 PGO,只是一切均发生在运行时。我将这个技术称为动态 MPGO。
  • 使用后台 JIT,在实际首次执行 IL 方法之前对其智能地执行 JIT 编译,从而尽可能多地隐藏 JIT 的开销。理想情况下,首次调用某个方法时,可能已对该方法执行 JIT 编译,无需等待 JIT 编译器来编译方法。

有趣的是,所有这些技术都已引入 .NET Framework 4.5,并且更高版本也同样支持这些技术。静态 MPGO 只适用于由 Ngen.exe 生成的本机映像。与之相反,动态 MPGO 只适用于 IL 方法。尽可能使用 Ngen.exe 生成本机映像,并使用静态 MPGO 对映像进行优化,因为这个技术相对简单,同时又能够大大提高速度。第三个技术即后台 JIT,这个技术与前两个技术完全不同,因为它不会提升生成的二进制代码的性能,而是减少 JIT 编译开销,因此,这个技术可以与前两个技术中的任意一个协同使用。然而,单独使用后台 JIT 有时会非常有用,可以将应用启动或特定的常见使用方案的性能提升多达 50%,这已经很棒了。本文只重点关注后台 JIT。在下一节中,我将讨论 JIT 编译 IL 方法的传统方式,以及它是如何影响性能的。然后,我还会讨论后台 JIT 的工作方式,工作原理以及如何正确使用它。

传统 JIT

对于 .NET JIT 编译器的工作方式,鉴于介绍这个流程的文章已经非常之多,您可能已经有了一些基本了解。但是,在讨论后台 JIT 之前,我想重新以更详细、精确(不是非常精确)的方式重新审视这个问题,这样一来,您就可以轻松地理解下一节的内容,并透彻理解这个功能。

我们来看图 1 中的示例。T0 是主线程。线程的绿色部分表示线程正在执行应用代码,并且是全速运行。我们假设 T0 正在通过已经过 JIT 编译的方法执行(最上面的绿色部分),下一个指令是调用 IL 方法 M0。由于这是第一次执行 M0,并且它已在 IL 中表示,因此,必须将其编译为处理器可以执行的二进制代码。因此,在执行调用指令时,将调用称为 JIT IL 存根的函数。此函数最终调用 JIT 编译器,对 M0 的 IL 代码进行 JIT 编译,并返回已生成的二进制代码的地址。此项操作与应用本身无关,由 T0 的红色部分表示,代表它是一个开销。幸运的是,存储 JIT IL 存根地址的内存位置包含相应的二进制代码的地址,这样一来,对同一函数的后续调用能够全速运行。

执行托管代码时的传统 JIT 开销
图 1 执行托管代码时的传统 JIT 开销

现在,从 M0 返回之后,将执行已经过 JIT 编译的一些其他代码,并随之调用 IL 方法 M1。与 M0 一样,将调用 JIT IL 存根,该存根会依次调用 JIT 编译器来编译方法,并返回二进制代码的地址。从 M1 返回之后,将执行更多二进制代码,然后开始运行 T1 和 T2 这两个线程。现在,有趣的事情发生了。

执行已经过 JIT 编译的方法之后,T1 和 T2 将调用 IL 方法 M3,由于之前从未调用过 M3,因此,此方法需要进行 JIT 编译。JIT 编译器在内部维护经过 JIT 编译的所有方法的列表。有一个针对每个 AppDomain 和一个共享代码的列表。此列表受锁的保护,同时每个元素也会由自身的锁进行保护,这样一来,就可以同时安全地对多个线程执行 JIT 编译。此时,某个线程(假设为 T1)将对方法执行 JIT 编译,并在 T2 空闲时将时间花费在执行与应用无关的操作(即仅仅因为非常空闲而等待某个锁)上,直到 M3 的二进制代码可用。同时,T0 将编译 M2。线程完成对方法的 JIT 编译后,将使用二进制代码的地址替换 JIT IL 存根的地址,从而释放锁,并执行方法。请注意,T2 将最终唤醒,并仅执行 M3。

图 1 的绿色线条显示由这些线程执行的其余代码。这表示应用正在全速运行。即使新线程 T3 才开始运行,它需要执行的所有方法也都已经过 JIT 编译,因此,它仍可以全速运行。这样所导致的的性能会非常接近本机代码性能。

大体而言,每个红色区段的持续时间主要取决于对方法进行 JIT 编译所花费的总时间,进而依赖于方法的大小和复杂程度。这个范围可以跨越从几微秒到几十微秒(不包括加载任何所需程序集或模块的时间)。如果启动应用需要首次执行的方法数少于 100 个,这当然不成问题。但是,如果需要首次执行几百个或几千个方法,可能会对所有这些生成的红色区段产生重大影响,尤其是如果对某个方法执行 JIT 编译花费的时间相当于执行该方法的时间,将导致运行缓慢程度达到两位数的百分比。例如,如果某个应用在启动时需要执行 1000 个不同的方法,平均 JIT 时间为 3 微秒,那么它将需要花费 3 秒完成启动过程。这是一个大问题。这对业务不利,因为会令客户不满意。

请注意,同时对同一方法执行 JIT 编译的线程可能有多个。也可能会首次尝试 JIT 编译失败,但第二次执行成功。最后,还可能对已经过 JIT 编译的方法再次执行了 JIT 编译。然而,所有这些情况都不在本文涵盖的范围之内,您在使用后台 JIT 时无需注意这些问题。

后台 JIT

上一节讨论的 JIT 编译开销无法避免,也不能显著减少。要执行 IL 方法,您需要对其进行 JIT 编译。然而,您可以更改引起此开销的时间。关键是,与其等待首次调用 IL 方法以进行 JIT 编译,还不如在早些时候对该方法执行 JIT 编译,这样一来,在调用该方法时,就已经生成了二进制代码。如果您操作正确,图 1 中所示的所有线程都将显示为绿色,并全速运行,就像执行的是 NGEN 本机映像一样,甚至具有更好的性能。但要实现这一点,需要解决两个问题。

第一个问题,如果您要对某个方法执行必需的 JIT 编译,将由哪个线程来执行 JIT 编译? 不难看到,解决这个问题最好的方法是安排一个后台运行的专用线程,这样就能尽快对方法执行 JIT 编译。如果至少有两个可用的核心(通常能够实现),这个解决方法是可行的,因此,重复执行应用代码可以隐藏 JIT 编译开销。

第二个问题: 在首次调用某个方法之前,如何知道下一个要进行 JIT 编译的方法? 请记住,通常情况下,每个方法都包含条件方法调用,因此您无法仅仅对可能要调用的所有方法执行 JIT 编译,也无法对要执行 JIT 编译的下一个方法进行推测性选择。JIT 后台线程很可能会迅速落后于应用线程。这时,配置文件将发挥作用。您首先执行应用启动和常见使用方案,并单独记录每个方案中执行了 JIT 编译的方法,以及执行 JIT 编译的顺序。然后,可以将应用与记录的配置文件一同发布,这样一来,应用在用户计算机上运行时,与时钟时间相关的 JIT 编译开销将降至最低(这是用户感知时间和性能的方式)。这个功能称为后台 JIT,您可以自行轻松地使用它。

在上一节中,您看到了 JIT 编译器在不同线程中如何对不同的方法并行执行 JIT 编译。就技术而言,这个传统的 JIT 已经实现了多核。遗憾的是,MSDN 文档将这个功能称为多核 JIT,这是基于“至少两个内核”的要求,而未遵循功能本身的典型特征,这通常令人困惑。我使用了“后台 JIT”这个名称,因为这是我希望介绍的一个特征。PerfView 对这个功能提供了内置支持,使用了“后台 JIT”这个名称。请注意,“多核 JIT”是 Microsoft 早期在开发过程中使用的名称。在本节的剩余部分中,我将介绍在对代码应用此技术时必须执行的操作,以及这个技术如何更改了传统 JIT 的模式。我还会向您介绍在将后台 JIT 用于您自己的应用时,如何使用 PerfView 衡量后台 JIT 的优势。

要使用后台 JIT,您需要告知运行时要放置配置文件的位置(为每个触发大量 JIT 编译的方案均分配一个)。您还需要告知运行时具体要使用的配置文件,以便运行时可以读取配置文件,从而确定要在后台线程上编译的方法。当然,在关联的使用方案启动之前,需要完全执行此操作。

要指定放置配置文件的位置,可以调用在 mscorlib.dll 中定义的 System.Runtime.Profile­Optimization.SetProfileRoot 方法。此方法如下所示:

public static void SetProfileRoot(string directoryPath);

唯一的参数 directoryPath 用于指定要读取其中所有配置文件或者要向其中写入所有配置文件的文件夹的目录。同一 App­Domain 中,只有对此方法的首次调用起作用,任何其他调用都可以忽略(但不同的 AppDomain 可以使用同一路径)。此外,如果计算机内核少于两个,则可以忽略对 SetProfileRoot 的所有调用。这个方法执行的唯一操作是在内部变量中存储指定目录,以便以后在需要时随时使用。在初始化期间,通常由流程的可执行文件 (.EXE) 调用此方法。共享库不应调用此方法。您可以在应用运行期间,在调用 ProfileOptim­ization.StartProfile 方法之前随时调用此方法。另一种方法如下所示:

public static void StartProfile(string profile);

应用即将完成执行您希望对其优化性能的路径(例如启动)时,调用此方法并向其传递配置文件的文件名和扩展名。如果文件不存在,将在具有您使用 SetProfileRoot. 指定的文件夹中的指定名称的文件中记录并存储配置文件。这个过程称为“配置文件录制”。 如果指定的文件存在且包含有效的后台 JIT 配置文件,后台 JIT 将在专用后台线程 JIT 编译方法中起作用,按照配置文件内容进行选择。这个过程称为“配置文件播放”。 执行配置文件时,也会记录应用呈现的行为,同时也会替换同一输入配置文件。

不支持在不记录的情况下执行配置文件;但这种不支持的情况是临时的。您可以多次调用 StartProfile,指定适用于不同执行路径的不同配置文件。如果在使用 SetProfileRoot 初始化配置文件根目录之前已调用此方法,则此方法将不起作用。另外,如果指定的参数完全无效,那么两种方法均不起作用。实际上,这些方法不会引发任何异常或返回错误代码,不会以令人不快的方式影响应用的行为。这两个方法都是线程安全的,类似于框架中的所有其他静态方法。

例如,如果您希望提升启动性能,可以在主函数中调用这两个方法作为第一个步骤。如果您希望提升特定使用方案的性能,在用户预期初始化该方案时调用 StartProfile,并在早先的任何时候调用 SetProfileRoot。请记住,AppDomain 中的所有操作都发生在本地。

以上是您在代码中使用后台 JIT 需要执行的全部操作。操作非常简单,尽管尝试去操作,不必担心是否有用。然后,可以检验速度提升效果,决定是否可以持续使用。如果速度提升至少 15%,则可以继续使用。否则,由您自己来决定。接下来我将详细说明后台 JIT 的工作方式。

每次调用 StartProfile 时,会在正在执行代码的 AppDomain 的上下文中执行以下操作:

  1. 包含配置文件(如果存在)的文件的所有内容都将被复制到内存中。然后文件将关闭。
  2. 如果这是第一次成功调用 StartProfile,说明已在运行后台 JIT 线程。在这种情况下,将终止此线程,并创建一个新的后台线程。然后,StartProfile 线程将返回到调用方。
  3. 这一步骤在后台 JIT 线程中发生。配置文件已经过分析。记录过的方法按连续记录的顺序尽可能快地进行 JIT 编译。这是配置文件执行过程中的一个步骤。

对于后台线程而言,操作已经完成。如果已对所有记录的方法完成 JIT 编译,将会在无提示的情况下终止。在分析或对方法执行 JIT 编译过程中,如果出现任何问题,线程将在无提示的情况下终止。如果对方法执行 JIT 编译所需的程序集或模块未加载,将不会进行加载,因此,也不会对该方法执行 JIT 编译。设计后台 JIT 的目的是尽可能不更改程序的行为。加载某个模块时,会执行其构造函数。另外,如果无法找到某个模块,将调用使用 System.Reflection.Assembly.ModuleResolve 事件注册的回调。因此,如果后台线程加载某个模块的时间早于预期,这些函数的行为可能会更改。这同样适用于使用 System.AppDomain.AssemblyLoad 事件注册的回调。由于后台 JIT 不加载所需的模块,因此,它可能无法编译大量已记录的方法,这样的话,优势就非常有限。

您可能想知道,为什么不创建多个后台线程来为更多方法执行 JIT 编译? 首先,这些线程属于计算密集型线程,因此,它们可能与应用线程不相上下。其次,线程越多,意味着线程同步争用也越多。第三,对方法执行了 JIT 编译,但不会由任何应用线程调用,这种情况也不是没有可能。相反,可能对方法进行未在配置文件中记录过的或在由多核线程执行 JIT 编译之前的首次调用。由于存在这些问题,创建多个后台线程可能不是非常有利。然而,CLR 团队可能会在以后(尤其是放宽对加载模块的限制后)实现这个想法。现在,来讨论一下应用线程(包括配置文件记录过程)中会出现什么情况。

图 2 显示图 1 中所示的同一示例,只是后台启用了 JIT。也就是说,后台线程按照该顺序对方法 M0、M1、M3 和 M2 执行 JIT 编译。请注意后台线程与应用线程 T0、T1、T2 和 T3 竞争的过程。后台线程需要为每个方法执行 JIT 编译,然后再由任何线程首次调用该方法以实现其用途。下面的讨论假设存在 M0、M1 和 M3,但没有 M2。

显示与图 1 对比的后台 JIT 优化的示例
图 2 显示与图 1 对比的后台 JIT 优化的示例

T0 要调用 M0 时,后台 JIT 线程已对 M0 执行了 JIT 编译。然而,方法地址尚未修补,仍指向 JIT IL 存根。后台 JIT 线程本应修补此方法地址,但为了在以后确定方法是否已被调用,因而未进行修补。此信息由 CLR 团队用于评估后台 JIT。因此,将调用 JIT IL 存根,JIT IL 存根获知已在后台线程上编译此方法。它需要做的只是修补地址,并执行方法。请注意此线程上消除 JIT 编译开销的方法。在 T0 上调用 M1 时,采用同样的方法处理 M1。在 T1 上调用 M3 时,也采用同样的方法处理 M3。但是,当 T2 调用 M3 时(请参考图 1),T1 快速修补方法地址,因此它直接调用方法的实际二进制代码。然后,T0 调用 M2。然而,后台 JIT 线程尚未完成对该方法进行的 JIT 编译,因此 T0 等待对该方法的 JIT 锁定。为方法执行 JIT 编译后,T0 将唤醒并调用该方法。

我还没有介绍在配置文件中记录方法的过程。应用线程调用后台 JIT 线程未曾开始 JIT 编译(或者由于方法不在配置文件中而不会为其执行 JIT 编译)的方法也是完全可能的。我按照下面的算法编译了在应用线程调用未经过 JIT 编译的静态或动态 IL 方法时所执行的步骤:

  1. 获取一个存在方法的 AppDomain 的 JIT 列表锁。
  2. 如果一些其他应用线程已生成二进制代码,则可以释放 JIT 列表锁,并转到步骤 13。
  3. 如果没有元素,则将新元素添加到表示方法的 JIT 工作线程的列表。如果元素已存在,则其引用计数将增加。
  4. 释放 JIT 列表锁。
  5. 获取方法的 JIT 锁。
  6. 如果一些其他应用线程已生成二进制代码,则转到步骤 11。
  7. 如果后台 JIT 不支持该方法,则跳过此步骤。目前,后台 JIT 仅支持通过静态方式发出的 IL 方法,这些方法在未使用 System.Reflection.Assembly.Load 进行加载的程序集中定义。现在,如果支持该方法,可以检查该方法是否已由后台 JIT 线程执行 JIT 编译。如果是,请记录方法并转到步骤 9。否则,执行下一个步骤。
  8. 为方法执行 JIT 编译。JIT 编译器检查方法的 IL,确定所有所需类型,确保已加载所有所需程序集,并且已创建所有所需的类型对象。如果出现任何问题,将引发异常。这一步骤会产生大部分开销。
  9. 将 JIT IL 存根的地址替换为方法的实际二进制代码的地址。
  10. 如果应用线程(而非后台 JIT 线程)对方法执行了 JIT 编译,则存在一个活动的后台 JIT 记录程序,并且后台 JIT 支持该方法;方法将记录在内存配置文件中。对方法执行 JIT 编译的顺序将保留在配置文件中。请注意,不会记录生成的二进制代码。
  11. 释放方法 JIT 锁。
  12. 安全地减少使用列表锁的方法的引用计数。如果减少为零,表明元素已删除。
  13. 执行方法。

出现以下任一情况时,后台 JIT 记录过程将终止:

  • 因任何原因未加载与后台 JIT 管理程序关联的 AppDomain。
  • 在同一 AppDomain 中再次调用了 StartProfile。
  • 应用线程中对方法执行 JIT 编译的速度变得较慢。这表示应用已达到稳定状态,极少需要 JIT 编译。在此之后,后台 JIT 将不再对经过 JIT 编译的任何方法执行任何操作。
  • 已达到其中一个记录限制。模块的最大数为 512,方法的最大数为 16,384,记录最长持续时间为 1 分钟。

记录过程终止后,已记录的内存配置文件将转储到指定文件。这样,下次在应用运行时,会选取那个反映上一次运行期间由应用展示的行为的配置文件。正如我前面提到的,配置文件始终会被覆盖。如果您想要保留当前配置文件,必须先手动制作配置文件的副本,然后再调用 StartProfile。配置文件的大小通常不会超过几十 KB。

在结束本节内容之前,我想谈谈如何选择配置文件根目录。对于客户端应用,您可以指定特定于用户的目录或与应用相关的目录,具体取决于您是希望对不同用户执行多组不同的配置文件,还是对所有用户执行一组配置文件。对于 ASP.NET 和 Silverlight 应用,您可以使用与应用相关的目录。实际上,从 ASP.NET 4.5 和 Silverlight 4.5 开始,后台 JIT 都已默认启用,并且配置文件随应用一同存储。运行时将按您已调用主方法中的 SetProfileRoot 和 StartProfile 的方式执行操作,因此您无需执行任何操作就能使用此功能。但是,如前所述,您仍然可以调用 StartProfile。您可以按 .NET 博客文章“提升应用启动性能的简单解决方案”(bit.ly/1ZnIj9y) 中所述,在 Web 配置文件中将 profileGuided­Optimizations 标记设置为 None,从而关闭自动后台 JIT。此标记只能获取一个其他值 (All),这个值可以实现后台 JIT(默认)。

运行中的后台 JIT

后台 JIT 是一个 Windows 事件跟踪 (ETW) 提供程序。也就是说,它可以向 ETW 使用者(如 Windows 性能记录器和 PerfView)报告与此功能相关的大量事件。您可以通过这些事件来诊断后台 JIT 中出现的任何低效问题或故障。尤其是可以确定在后台线程编译的方法数量以及对这些方法执行 JIT 编译的总时间。您可以从 bit.ly/1PpJUpv 下载 PerfView(无需安装,只需解压和运行)。我将使用以下简单代码进行演示:

class Program {
  const int OneSecond = 1000;
  static void PrintHelloWorld() {
    Console.WriteLine("Hello, World!");
  }
  static void Main() {
    ProfileOptimization.SetProfileRoot(@"C:\Users\Hadi\Desktop");
    ProfileOptimization.StartProfile("HelloWorld Profile");
    Thread.Sleep(OneSecond);
    PrintHelloWorld();
  }
}

在主函数中,调用 SetProfileRoot 和 StartProfile 以设置后台 JIT。如果将线程置于休眠状态大约一秒,则会调用 PrintHelloWorld 方法。此方法只是调用 Console.WriteLine,然后返回。将此代码编译为 IL 可执行文件。请注意,Console.WriteLined 不需要 JIT 编译,因为在计算机上安装 .NET Framework 的过程中已使用 NGEN 对其进行编译。

使用 PerfView 启动并剖析可执行文件(有关如何操作的详细信息,请参阅 bit.ly/1nabIYC 上的 .NET 博客文章“使用 PerfView 提升应用的性能”或 bit.ly/23fwp6r 上的第 9 频道 PerfView 教程)。请记住勾选后台 JIT 复选框(仅在 .NET Framework 4.5 和 4.5.1 中需要),启用从此功能捕获事件的功能。等待 PerfView 完成,然后打开 JITStats 页面(请参阅图 3),PerfView 将告诉您该过程不使用后台 JIT 编译。这是因为在首次运行期间,已生成一个配置文件。

PerfView 中的 JITStats 位置
图 3 PerfView 中的 JITStats 位置

现在,您已生成后台 JIT 配置文件,可以使用 PerfView 启动并剖析可执行文件。然而,此时,当您打开 JITStats 页面时,将看到 PrintHelloWorld 方法已在后台 JIT 线程上经过 JIT 编译,而 Main 方法未经过 JIT 编译。这还表明,大约 92% 的 JIT 时间都用于编译应用线程中出现的所有 IL 方法。PerfView 报告还显示一个已经过 JIT 编译的所有方法的列表、每个方法的 IL 和二进制代码大小及谁对方法执行了 JIT 编译和其他信息。您还可以轻松访问有关后台 JIT 事件的全部信息。然而,由于篇幅有限,我就不作详细介绍。

您可能想知道休眠一秒的目的。要在后台线程对 PrintHelloWorld 进行 JIT 编译,这是必要步骤。否则,应用线程有可能会在后台线程之前开始编译该方法。换句话说,必须尽早调用 StartProfile,以便后台线程在大多数情况下都能够抢占先机。

总结

后台 JIT 是 .NET Framework 4.5 和更高版本中支持的一种按配置优化功能。本文讨论了对于此功能您需要了解的几乎所有信息。我已经详细说明了需要此优化的原因、优化工作原理以及如何在您的代码中正确使用它。在 NGEN 不便于使用或无法使用时可以使用此功能。由于它具有易用性,请尽管试用,无需过多考虑它是否有利于应用等因素。如果您对提升的速度感到满意,请继续使用。否则,可以轻松地删除它。Microsoft 使用后台 JIT 来提升部分应用的启动性能。我希望您也可以在应用中高效地使用它,从而让广泛使用 JIT 的方案和应用启动速度得到大幅提升。


Hadi Brais* 是德里印度理工学院具有博士学位的学者,主要研究下一代内存技术的编译器优化。他将大部分精力用于编写 C/C++/C# 代码上,并深入研究了运行时、编译器框架和计算机体系结构。他的博客网址是 hadibrais.wordpress.com。您可以通过 hadi.b@live.com 与他联系。*

衷心感谢以下 Microsoft 技术专家对本文的审阅: Vance Morrison