CLR

针对 ARM 处理器进行 .NET 开发

Andrew Pardoe

 

消费者今天是一个大型的驱动技术市场。 称为"IT 消费化"的趋势可见一斑,电池寿命长和始终连接和媒体丰富的经验是对所有技术客户很重要。 若要启用的电池寿命长的设备的最佳体验,Microsoft Windows 8 操作系统给带来系统建立在低功耗 ARM 处理器,而今天权力大多数移动设备上。 在本文中,我将讨论 Microsoft.net 框架和 ARM 处理器的详细信息、 你作为一个.net 开发人员应该请牢记和微软 (如我在 CLR 团队的项目经理) 我们不得不做把.net 的拿到臂。

作为.net 开发人员,你可以想象写在各种不同的处理器上运行的应用程序会构成有点左右为难。 ARM 处理器指令集体系结构 (ISA) 不兼容 x86 处理器的 ISA. 建基于 x86 在本机运行的应用程序运行 x64 上很好,因为 x 64 处理器 ISA 是 x 86 的超集 ISA. 但同样并不是真正的本机 x 86 在手臂上运行的应用程序 — — 他们需要重新编译该不兼容的体系结构上执行。 能够从一系列不同设备的选择是对消费者来说,很大,但它给开发商故事带来一定的复杂性。

在.net 语言编写您的应用程序不仅使您可以重用你现有的技能和代码,它还使您的应用程序,无需重新编译所有 Windows 8 处理器上运行。 由移植到手臂的.net 框架,我们帮助抽象的独特的特征的体系结构,对大多数 Windows 开发商不熟悉。 但仍有一些东西,您可能需要编写代码以在手臂上运行时的注意事项。

到臂之路:.NET 过去和现在

.NET 框架已经运行在 ARM 处理器上,但它不是在台式机运行的版本完全相同的.net 框架。 当我们开始.net 的第一个版本的工作,我们意识到能够跨处理器编写易于携带的代码是我们的提高开发人员的效率的价值主张的关键。 X86 处理器主导桌面计算的空间,但在嵌入式和移动的空间存在的五花八门的处理器。 要使开发人员能够针对那些处理器,我们创建了一个版本的.net 框架调用.net 框架精简版,在有内存和处理器的约束的机器上运行的。

.NET 框架精简版支持的第一次设备为 4 MB 的内存和 CPU 的 33 兆赫一点了。 .NET 框架精简版的设计强调 (使其能在这种约束的设备上运行) 的有效执行和可移植性 (以便它可以运行跨了大量的处理器所共有的移动和嵌入的空格)。 但在最受欢迎的移动设备 — — 智能手机 — — 现在运行是相当于 10 年前的计算机的配置。 桌面.net 框架,旨在与至少 300 MHz 的处理器和 128 MB RAM 的 Windows XP 计算机上运行。 Windows Phone 设备今天需要至少 256 MB 的 RAM 和现代的手臂皮质处理器。

.NET 框架精简版仍然是 Windows 嵌入式紧凑的开发商故事很大一部分。 在方案中嵌入设备运行在约束配置中,经常使用低达 32 MB 的 RAM。 我们还创建了一个版本的.net 框架调用.net 微 Framework 中,有一点为 64 KB 的内存的处理器运行。 所以我们实际上有三个版本的.net 框架,其中每个在一个不同的类的处理器上运行。 但这是我们的旗舰产品,桌面的.net Framework 中,加入了压缩和微框架在 ARM 处理器上运行的第一次。

在手臂上运行

尽管.net 框架旨在保持中立的平台,它大多已上运行 x 基于 x86 的硬件在它的存在。 这意味着几个 x 86 特定模式已陷入集体心中的.net 程序员。 您应该能够集中精力编写伟大的应用程序,而不是写作的处理器体系结构中,但在编写.net 代码在手臂上运行时,你应该保持在头脑中的几件事。 这些措施包括一个较弱的内存模型和更严格的数据对齐要求,以及一些函数参数区别对待的地方。 最后,有几个项目配置步骤在 Visual Studio 中的不同时您的目标设备。 我将讨论每个。

一种较弱的内存模型"内存模式"是指对多线程程序中的全局状态所做更改的可见性。 两个 (或更多) 的线程之间共享数据的程序通常会锁定该共享的数据。 根据特定的锁定使用,如果一个线程访问数据,试图访问数据的其他线程将阻塞直到第一个线程完成与共享的数据。 但锁是没有必要的如果你知道每个线程访问共享的数据会这样做而不会干扰该数据的其他线程的视图。 在这种方式中的进行编程称为"锁免费"算法。

你不知道,您的代码将执行的确切顺序时无锁算法的麻烦。 现代的处理器进行重新排序说明,以确保处理器可以在每个时钟周期上取得进展,并将写入到内存组合以减少延迟。 虽然几乎每个处理器执行这些优化,有差异的读取和写入顺序呈现方式的程序。 x 基于 x86 的处理器保证处理器将会看起来像它正在执行大多数的读取和写入程序指定它们的顺序相同。 这一保证被称为强内存模型,或强烈的写入顺序。 ARM 处理器别让尽可能多的担保 — — 他们一般自由地左右移动操作,只要这样做不会更改的代码将在单线程的程序中运行的方式。 ARM 处理器不会作出一些保证允许精心构造的无锁代码,但它有所谓的一种微弱的内存模型。

有趣的是,.net 框架 CLR 本身具有弱内存模型。 要写入订购 ECMA 公共语言基础结构 (CLI) 规范中的所有引用 (可用作在 PDF bit.ly/1Hv1xw) 的标准,CLR 是旨在满足、 挥发性的访问,请参阅。 在 C# 中,这意味着对变量的访问标记为 volatile 关键字 (请参阅参考的 CLI 规范第 126 条)。 但在过去十年中最托管的代码都已运行基于 x86 系统和 CLR 实时 (JIT) 编译器尚未添加到允许的硬件,所以没有哪里的内存模型会揭露潜并发错误的案件相对较少的 reorderings 多。 如果托管的代码的书面和测试仅对 x 基于 x86 的机器预计将手臂的系统上的工作方式相同,这可能会出现问题。

大多数的模式,需要在重新排序要格外谨慎是在托管代码中罕见。 但这些模式,确实存在一些看似简单。 下面是一个示例的代码,看上去不像它有一个 bug,但如果在另一个线程上更改了这些变量,则此代码可能会破坏机器上弱内存模型:

static bool isInitialized = false;
static SomeValueType myValue;
if (!isInitialized)
{
  myValue = new SomeValueType();
  isInitialized = true;
}
myValue.DoSomething();

若要使此代码正确,只是表明 isInitialized 标志是挥发性:

static volatile bool isInitialized = false; // Properly marked as volatile

执行此代码没有重新排序显示在左侧块图 1。 线程 0 是第一个在其本地的堆栈上初始化 SomeValueType,并将本地创建的 SomeValueType 复制到应用程序域的全局位置。 线程 1 确定通过检查它还需要创建 SomeValueType 的 isInitialized。 但这是没有问题,因为数据被写回到同一应用程序域全局位置。 (大多数情况下,在此示例中,DoSomething 方法所作的任何突变是幂等)。

Write Reordering
图 1 写重新排序

右侧块显示支持写入重新排序 (和方便地放置在执行档) 的系统具有相同的代码的执行。 此代码将无法正确执行,因为线程 1 通过阅读 SomeValueType 并不需要初始化的 isInitialized 的值来确定。 在调用 DoSomething 指未初始化的内存。 从某些值类型中读取的任何数据将由 0 到 CLR 设置。

您的代码不会经常执行错误由于这种重新排序,但是它不会发生。 是完全合法的这些 reorderings — — 的写操作顺序无关紧要时在单个线程上执行。 编写并行的代码来标记正确使用挥发性关键字的可变变量时,最好。

CLR 允许公开更强的内存模型比 ECMA CLI 规范要求。 例如,基于 x86,CLR 的内存模型是强因为处理器的内存模型很强。 .NET 团队可以做的内存模型,手臂一样强大模型基于 x86,但确保订购尽可能的完善可以对代码的执行性能产生显著影响。 我们做的有针对性的工作,以加强在胳膊上的内存模型 — — 具体来说,我们已经插入记忆障碍在关键点时写入到托管堆,保证类型安全 — — 但我们确信,只有这样做对性能影响最小。 该团队经历了多个设计审查,以确保在手臂 CLR 中的应用技术是正确的专家。 此外,性能基准测试显示.net 代码执行性能扩展本机 c + + 代码时相比跨 x86、 x 64 和手臂一样。

如果您的代码依赖于无锁的算法,取决于 x 86 执行 CLR (而不是 ECMA CLR 规范),您需要将 volatile 关键字添加到适当的相关变量。 一旦您已标记为易失性的共享的状态,CLR 会照顾一切为您。 如果你像大多数开发人员,你准备好要在手臂上运行,因为您已经使用锁来保护您的共享的数据、 正确标记为可变变量并测试您的应用程序在胳膊上。

数据对齐方式要求另一个差别可能会影响某些程序是 ARM 处理器需要一些数据的对齐。 你有没有在 64 位边界对齐 64 位值 (即 int64、 uint64 或一个双精度型) 时,对齐要求适用的特定模式。 CLR 会照顾你的对齐方式,但有两种方式,迫使未对齐的数据类型。 第一种方法是用 [ExplicitLayout] 自定义属性显式指定的结构布局。 第二种方式是结构的正确指定的托管代码和本机代码之间传递布局。

如果您注意到回来带垃圾的 P/Invoke 调用,可能要看一看被封送处理任何结构。 作为一个例子,我们同时移植的 COM 接口传递包含了 64 位双作为参数的托管代码中的函数的两个 32 位字段的 POINTL 结构一些.net 库修正 bug。 该函数使用位操作来获取两个 32 位字段。 这里是越野功能的简化的版本:

void CalledFromNative(int parameter, long point)
{
  // Unpack native POINTL from long point
  int x = (int)(point & 0xFFFFFFFF);
  int y = (int)((point >> 32) & 0xFFFFFFFF);
  ...  // Do something with POINTL here
}

本机代码没有要对齐的 POINTL 结构上 64 位的边界,因为它包含两个 32 位字段。 但手臂需要 64 位双要对齐时传递到托管函数。 使某些类型的指定相同,托管本机调用的两边都是关键,如果您的类型需要的对齐方式。

里面最 Visual Studio 开发商不会永远注意讨论过因为.net 代码的设计并不特定于任何处理器体系结构的差别。 但有一些 Visual Studio 时分析或调试 Windows 8 臂设备上的应用程序中的差异,因为 Visual Studio 不在手臂设备上运行。

如果您编写的应用程序的 Windows Phone,你已经熟悉的跨平台开发过程。 Visual Studio x 86 dev 盒上运行,并且您启动远程设备或模拟器上您的应用程序。 这款应用程序使用代理服务器安装在您的设备上与您的开发计算机,通过一个 IP 连接进行通信。 除最初的设置步骤、 调试和分析经验行为相同的所有处理器。

另一点要注意的是 Visual Studio 项目设置添加手臂向 x86 和 x 64 作为目标处理器的一个选择。 您通常会选择目标 AnyCPU,当你写一个.net 应用程序,Windows 的手臂,与您的应用程序将只在所有 Windows 8 版面上运行。

深入探讨支撑臂

过了这远到这篇文章,你已经知道了很多关于手臂。 现在我想分享一些有关.net 团队工作,支持 ARM 的有趣、 深入的技术的详细信息。 你不会要做这种工作在.net 代码中 — — 这是只是快速幕后瞧一瞧我们所做的工作的种类。

因为 CLR 被设计成便携式跨体系结构,clr 中本身的更改大部分都是简单明了。 我们确实要进行一些更改,使其符合到手臂应用程序二进制接口 (ABI)。 我们还必须重写到目标的手臂 CLR 程序集代码,改变我们 JIT 编译器产生的手臂拇指 2 说明。

阿比指定处理器的可编程接口的实现的方式。 它是相近的 API,用于指定哪些操作系统以编程方式可用功能。 该函数调用约定,登记公约 》 的 ABI,影响我们的工作的三个领域并调用堆栈的展开信息。 我将逐一讨论每个部分。

函数调用一个调用公约是调用一个函数的代码和被调用的函数之间的协议。 公约 》 指定参数和返回值如何布局的在内存中,以及注册需要有跨呼叫保留其值。 函数按顺序调用代码生成器工作跨边界 (如调用从程序到 OS),需要生成与公约 》,在处理器的定义,包括 64 位值的对齐方式相匹配的函数调用。

ARM 是第一次的 32 位处理器凡 CLR 不得不处理对齐参数和 64 位边界上托管堆上的对象。 最简单的解决方案将使所有参数,但阿比需要的代码生成器不离开泡沫在栈中时没有对齐方式是必需的所以没有性能退化。 因此推到堆栈上一堆参数的简单的操作变得更加微妙的 ARM 处理器上。 因为用户结构可以包含一个 int64,CLR 的解决方案是使用有点上每种类型来表示是否它需要的对齐方式。 这为 CLR 提供了足够的信息,以确保包含 64 位值的函数调用不会意外地损坏调用堆栈。

登记册公约 》 的数据对齐方式要求执行结束时结构完全或部分上注册的手臂。 这就意味着我们不得不修改代码,clr 中经常移动到寄存器使用内存中的数据,以确保登记册内的数据正确对齐。 这项工作必须要做的两种情况:第一,使某些 64 位值甚至寄存器,在启动和第二、 齐次浮点聚合 (HFAs) 置于适当的寄存器。

如果代码生成器注册 int64 手臂上的,必须将其存储在一个奇偶对 — — 即 R0 R1 或 R2 R3。 议定书 》 HFAs 均质结构中允许最多四个双或单一的浮点值。 如果这些都注册后,他们必须存储在任一 S (单个) 或 D (双线) 注册集,但不是在通用 R 寄存器。

展开信息展开信息记录在堆栈上的函数调用已和非挥发性位置寄存器的记录都保存在函数调用的效果。 在 x86 上) Windows 看 fs: 0 查看链接的列表的每个函数的异常登记信息发生未处理的异常。 64 位 Windows 引入的概念展开允许 Windows 进行爬网的堆栈发生未处理的异常的信息。 扩展的手臂设计的展开从 64 位设计信息。 CLR 代码生成器,反过来,不得不改变,以适应新的设计。

程序集代码即使 CLR 运行库引擎的大部分内容用 c + + 中,我们必须将其带入新的每个处理器的程序集代码。 此程序集代码的大部分是我们所说"的存根 (stub) 功能,"或"存根"。存根作为界面胶,使我们能够在一起配合的 c + + 和运行库 JIT 编译部分。 内部 CLR 程序集代码的其余部分是写的程序集的性能。 例如,垃圾回收器写屏障需要是速度非常快,因为它经常被称为 — — 在托管堆的对象引用写入对象的任何时间。

存根 (stub) 的一个例子是我们所说的"无序播放 thunk"。因为它搅乱跨寄存器的参数值,我们叫它无序播放 thunk。 有时 CLR 必须进行函数调用之前,只是改变在寄存器中参数的位置。 CLR 使用无序播放 thunk 调用委托时执行此操作。

在概念上,当您调用委托,你只要把对调用方法的调用。 在现实中,CLR 通过委托,而不是让一个命名的方法调用 (除非显式调用时调用通过反射) 的一个字段使间接调用。 此方法是远远快于命名的方法调用,因为运行库可以只是交换 (获得从目标指针) 委托的实例中的函数调用的委托。 也就是说,对于委托 d 实例美孚,调用 d.Member 方法映射到美孚。成员的方法。

如果你做一个封闭的实例,委托调用,此指针存储在用于传递的参数,R0,第一册和第一个参数存储在下一步的注册纪录册内,R1。 但只有当你有一个绑定到一个实例方法的委托。 如果您正在调用打开静态委托,将会发生什么? 在这种情况下,您期望的第一个参数存储在 R0 (像是没有这个指针。)无序播放 thunk 进入的第一个参数从 R1 R0 R0 进第二个参数,等等,作为放映中图 2。 因为此洗牌 thunk 的目的是要移动的值从登记册,它需要专门为每个处理器重写。

A “Shuffle Thunk” Shuffles Value Parameters Across Registers
图 2"无序播放 Thunk"搅乱值参数跨寄存器

只要专注于代码

进行审查、 移植到手臂的.net 框架是一个有意思的项目,有很多乐趣为.net 团队。 并编写.net 应用程序要在手臂上运行.net 框架应作为.net 开发人员为你开心。 您的.net 代码可能执行以不同的方式在手臂上而不是 x 在少数情况下,但.net 框架的虚拟执行环境中基于 x86 的处理器通常为您文摘这些差异。 这就意味着你不必担心什么处理器体系结构您的.net 应用程序上运行。 只是,您可以集中精力编写好的代码。

我相信在胳膊上有可用的 Windows 8 将会大为开发人员和最终用户。 ARM 处理器是,电池寿命更长,特别是适合的所以他们启用轻量、 便携式、 始终连接的设备。 移植到臂应用程序时,您将看到的最重要的问题是从台式机处理器的性能差异。 但是,请确保在胳膊上运行您的代码,说它实际上工程在胳膊上前 — — 不信任发展基于 x86 是不够。 对于大多数开发人员,这是所有有需要的。 如果您运行到任何问题,您可以引用回这篇文章要从哪里开始调查一些见解。

Andrew Pardoe 是 CLR 团队,帮助船舶在所有类型的处理器上的 Microsoft.net 框架程序经理。他个人的最爱仍然是安腾。他可以在达到 Andrew.Pardoe@microsoft.com

衷心感谢以下技术专家对本文的审阅:布兰登布雷、 蕾拉德里、 Eric Eilebrecht 和鲁迪马丁