这是一篇围绕 iOS 来介绍 ARM 结构的文章,用词简单,逻辑清楚,偶见幽默。非开发者也值得一读,权当增长知识。
我在写「NEON on iPhone 入门」的时候,曾以为读者已经比较了解 iOS 设备的处理器知识。然而,看过网上的一些讨论,我才发现,原来这些知识并不普及,我的错。此外,我觉得了解这些东西对 iPhone 编程有益(不仅仅针对喜欢 NEON 的人),即便你用的是 Objective-C,虽然,不了解也无碍工作,但这些知识会让你成为一个更好的 iPhone 程序员。
基础
到目前为止,所有的 iOS 设备都使用 ARM 结构处理器,它和台式机上的 x86 和 PowerPC 有些不同,然而绝对不是「特殊」或「小众」的产品。几乎所有的手机(不只是智能手机)都基于 ARM,例如几乎所有的 iPod,几乎所有的 MP3 播放器,PDA 和 Pocket PC 更不用说了。任天堂从 GBA 开始转入 ARM,它甚至还侵入图形计算器的地盘,出现在一些德仪和惠普的计算器中。如果你还想继续溯本逐源,那么牛顿用的也是 ARM(苹果是 ARM 的早期投资者)。而且上面只说了一些小玩意,还有无数的 ARM 处理器运行在嵌入式系统中。
ARM 处理器因为低功耗和小尺寸而闻名,它的性能在同等功耗的产品中也很出色。这种结构(至少在 iOS 平台)使用小端(Little-endian)排序,就像 x86。它和 MIPS、PowerPC 一样,属于 32 位 RISC 结构。请注意,模拟器并不运行 ARM 代码,软件会被编译成 x86 可以运行的指令。因此接下来的内容适用于目标设备,而非模拟器。
ARMv7,ARM11,Cortex A8 和 A4,天哪!
多年来,ARM 结构演化出几个不同的版本,每一版都增加了新指令,在提升的同时保持了后向兼容的能力。初代 iPhone 使用了 ARMv6 结构的处理器(ARM 第六版的简称),而最新的 iPhone 4 支持 ARMv7。所以,编译代码的时候,依目标版本的指令集不同,生成不同的指令。汇编程序也一样,代码中使用的指令必须兼容特定的版本。最后,生成机器码,对应 ARMv6 或 ARMv7(或者 ARMv5 和 v4,不过 ARMv6 是 iOS 开发的底线,所以这两者就不用考虑了)。目标文件和可执行文件有标注自己对应的版本,可以通过运行 otool -vh foo.o 来查看。
不过呢,「初代 iPhone 4 搭载了 ARMv6 处理器」这种说法是错误的,因为 ARMv6 不是指特定的处理器,而是处理器可以运行的指令集。初代 iPhone 使用了 ARM 11 核心(确切说是 ARM1176JZF-S,不过这不重要,只要记得它是 ARM 11 家族的成员就行了),正如刚才提到的,这款处理器采用 ARMv6 指令集。之后的 iOS 设备仍采用 ARM11,直到 iPhone 3GS 发布,苹果开始尽数转向 Cortex A8 处理器核心(尽管尚不确定,但 iPhone 4 很可能用的就是 A8 )。这个核心采用了 ARMv7 指令集,或这么说,它支持 ARMv7。
我已经说过,不要在程序里植入设备判断代码,然后通过已知信息侦测设备所支持的 ARM 结构。这种代码极不可靠,而且运行在(软件完成后才发布的)新设备上会导致中断。所以请别这么做,否则我发誓,我会跑到你家里废了你。以上知识是为了让你粗略了解,有些设备支持 ARMv7,有些设备支持 ARMv6。至于如何侦测,我马上会谈到。
不过,你可能会想「iPad 和 iPhone 4 用的是 A4,不是 Cortex A8 吧?」不然,A4 其实是一个完整的单片系统(SOC),其中不只有 Cortex A8 内核,还包括了图形硬件、音视频编码加速器和其他数字模块。单片系统和处理器是两个很不相同的概念,处理器在硅片上甚至不占主要空间。
如果不懂得如何利用,即使设备支持 ARMv7 也无济于事。当然应用新的指令集也没有问题,但如果总是这么做,早先的设备就无法运行你写的代码了,我猜,这也许不是你想要的结果。那么,应该如何侦测设备所支持的结构呢?— 只有确定它是否支持 ARMv7 才能好好利用啊。答案是:没必要知道。相反,把代码编译两次,一次针对 ARMv6,另一次针对 ARMv7,接着把这两个可执行文件打包成一坨肥硕无比的二进制文件。好了,运行的时候,设备会自己决定打开哪一个更好。是的,Mach-O 不仅可以用来组合完全不同的 CPU 结构(例如 PowerPC 和 Intel),或者相同结构的 32 位和 64 位版本,它还可以对付同一种结构的 2 个变体,用 Mach-O 的术语来说,这叫 CPU 子类。从程序员的角度看,这么做的结果是:编译时决定一切。针对 ARMv6 编译的代码只运行在 ARMv6 设备上,同理,针对 ARMv7 编译的代码只运行在 ARMv7(或者更好)的设备上。
如果你读过了我写的 NEON 那帖,你也许会记得我推荐过一种在运行时(Runtime)中侦测和选择结构的方法。如果再去看,你会发现我已经把那部分移走了,现在,我不建议那么做,因为虽然这的确有用,但不能确保(或者说,所需技巧太复杂而不能确保不出错)在将来的 ARMv8 处理器上能够稳定运行。文档中是否有相关 API 的状态不重要(不在 iOS 的手册页中),如果你想在 ARMv6 上运行又希望利用 ARM7v,就用我刚才讲过的办法。
补充一点:在 iOS 环境下,ARM 结构不一定能反映处理器的型号。例如,对应 ARMv6 的 iOS 代码需要浮点指令的支持(VFPv2,准确的说),对 ARMv6 而言,虽然这是可选项,不过自从第一代 iPhone 发布以来就已经存在。所以,如果在 iOS 开发(例如编译器 -arch 设置或一个可执行文件的 CPU 子类)中提到了 ARMv6,就表示需要硬件浮点的支持。这对 ARMv7 和 NEON 也一样:虽然 NEON 实际上是 ARMv7-A 配置的一个可选项,但是因为它出现在所有支持 ARMv7 的 iOS 设备中,所以,提到 iOS NEON 即部分提到 ARMv7。
条件执行
ARM 结构一个实用的功能是,大多数指令可以有条件地执行 — 如果条件不满足,则指令无效。这可以缩短过程,让区块(Blocks)部署地更为有效。通常的办法是,如果区块不符合条件则跳过,但是通过把判断指令植入块内,省去了该步骤。
如果这仅仅是编译器用来提高代码效率的手段,我就不会在这里提到它了。虽然,这的确是它的一个功用,但之所以提到是因为,在调试(Debugging)时,它可能会令人吃惊。事实上,有时你会发现,调试器会进入状态为假的条件区块(if block,例如早期的错误回报),或者进入 if-else 的两个分支。这是因为,虽然代码尽数经过处理器,但是一部分没有实际执行,即条件执行。另外,如果你把断点置入这样的条件区块中,即使状态为假,它仍有可能执行。
话虽如此,但是在我有限的测试中,编译器似乎拒绝在调试配置中生成条件执行指令。因此它应该只发生在调试优化后的代码的时候,不幸的是,有时候你没得选择,只能这么做。
Thumb
Thumb 指令集是 ARM 指令集的一个子集,经过压缩,因此指令只有 16bits(所有 ARM 指令的大小都是 32bits,它仍然是 32 位结构,只是占用的空间少了)这不是一个全然不同的结构,而应将其视作常见 ARM 指令和功能的缩写。它的优点,显然是大为缩小代码尺寸,节约内存和缓存,以及代码带宽。虽然更适用于内存紧张的微控制器型应用程序,但是在 iOS 设备中,它仍然有用处,也因为如此,Xcode 默认在 iOS 项目中打开这项功能。虽然代码尺寸因此减少很多,但是不可能达到 50%,因为有时候完成一个 ARM 指令需要对应的两个 Thumb 指令。ARM 和 Thumb 指令不能随意混合,处理器需要针对二者切换不同的模式,而这只能在调用或从函数返回时发生。
当目标平台是 ARMv6 的时候,编译 Thumb 指令面临着很大的权衡取舍。ARMv6 的 Thumb 代码可以访问的寄存器较少,缺乏条件指令,特别是,它不能使用浮点硬件,例如浮点加法、减法、乘法等等。使用浮点 Thumb 代码必须调用系统函数,没错,听起来就像速度很慢的感觉。基于这个原因,针对 ARMv6 时,我建议禁用 Thumb 模式,但倘若你执意如此,请确保先分析代码。如果某些部分速度很慢,至少先试着禁用那部分 Thumb(很容易,在 Xcode 中使用命令行参数, -mno-thumb)。请记住,浮点运算在 iOS 中非常普遍,因为 Quartz 和 Core Animation 使用浮点坐标系统。
当目标变成了 ARMv7 的时候,所有这些缺点就消失了:ARMv7 包含 Thumb-2,它是 Thumb 指令的扩展集,增加了条件执行和可以访问所有 ARM 寄存器以及硬件浮点与 NEON 的 32 位 Thumb 指令。用 Thumb-2 缩减代码的代价几乎没有,所以最好是开着(如果关掉了请重新打开)。在 Xcode 的条件生成选项中,对 ARMv7 打开,对 ARMv6 关闭。
你也许在网上听到人们说,代码需要「互通」(Interworking)才能使用 Thumb,除非你想写汇编代码,否则不必担心,因为 iOS 平台的所有代码都是互通的。当显示汇编的时候,Shark 可能难以判断函数是 ARM 还是 Thumb。如果你看到无效或无意义的指令,最好互相对调一下。
对齐
iOS 支持非对齐访问,然而比起对齐访问,它的速度更慢,建议不要使用。在某些特殊情况下(涉及加载/存储多个指令,如果你有兴趣的话),非对齐访问的速度可能比对齐访问慢上百倍,因为处理器无法处理,而且必须请求操作系统的协助(参考此文,这和 PowerPC 上导致非对齐双精度浮点数变得超慢是同一个现象)。所以,要小心,而且,对齐仍然重要。
除法
这家伙总让每一个人吃惊。打开 ARM 结构手册(如果你还没有,请看「NEON on iPhone 入门」的结构概览那节),找到整数除法指令。去吧,我等你。找不到?正常正常,根本没有的。是的,ARM 结构不支持硬件整数除法,必须通过软件执行。如果你编译下面的代码:
int ThousandDividedBy(int divisor) { return 1000/divisor; }
在汇编代码中,你会看到编译器插入了一个调用函数的「___divsi3」— 这是一个系统函数,用来执行软件除法(注意,除数不能恒定,否则除法可能会被转换为乘法)。这意味着,在 ARM 上,整数除法实际代表了操作系统的性能。
「不过,」看完手册归来,你也许会说:「你错啦!里面有 ARM 除法指令,甚至还有两个呢!在这里,sdiv 和 udiv!」不好意思给您颇凉水啦,这些指令只可用于 ARMv7-R 和 ARMv7-M 配置(分别指实时和嵌入式环境 — 例如马达的微控制器和手表),iOS 设备用的 ARMv7-A 不支持,很抱歉!
GCC
GCC 生成的 ARM 代码质量之糟已不是秘密。在其他一些基于 ARM 的平台上,专业开发者使用 ARM 自家提供的工具链 — RVDS。不过,RVDS 不支持 OSX 用的 Mach-O 运行时,只支持 ELF 运行时,所以在 iOS 平台上没辙。但至少还有 GCC 的替代品,比如现在可以用 LLVM。虽然我没怎么测试,但是当使用 LLVM 的时候,至少看到了 64 位整数码的显著改进(这一点,GCC 在 ARM 上尤其弱)。假以时日,LLVM 全面超越 GCC 可以指望。
你瞧,现在你是更好的 iOS 开发者了!
[原文链接;作者: Pierre Lebeaupin]