移动市场最新的关注点都集中在安卓 64 位系统。2013 年 9 月,Apple 发布了采用板载 64 位 A7 处理器的 iPhone* 5,进而掀起了新一轮移动技术竞争。
结果表明,基于安卓的内核 GNU/Linux* 在长时间内为带有 64 位寄存器的处理器提供了出色的支持。Ubuntu 是 "GNU/Linux",而安卓是 "Dalvik/Linux"。Dalvik 是 Google's 安卓操作系统中的程序虚拟机 (VM),专门执行为安卓系统编写的应用。Dalvik 因此成为了安卓软件堆栈不可或缺的一部分,该系统常见于手机和平板电脑等移动设备,最近还在智能电视和可穿戴设备领域有着广泛的应用。然而,所有使用 NDK 的开发人员都不得不根据最新架构重新构建程序,其难易程度取决于 Google 提供的工具。此外,Google 应提供向后兼容性,例如,NDK 32 位应用应能够在安卓 64 位中运行。
首款面向移动设备的英特尔 64 位处理器于 2013 年第 3季度创建,并随即成为移动和台式设备中功能强大的的全新多核系统芯片 (SoC)。全新 SoC 系列包括面向平板电脑和 2 合 1 设备的英特尔® 凌动TM 处理器、英特尔® 赛扬® 处理器,以及面向 2 合 1 设备、笔记本电脑、台式机和一体机的英特尔® 奔腾® 处理器。
2014 年 10 月,Google 发布了 64 位安卓 L(面向开发人员)的预览仿真器映像。它可支持开发人员在发布操作系统之前测试程序并重新编写代码(如有必要)。开发人员在 Google+ blog>Google+博客 中表示,完全使用 Java* 创建的程序无需移植。他们按照 L- 版仿真器原来的样子运行这些程序,以支持 64 位架构。而使用其他语言(特别是 C 和 C++)的开发人员则必须执行几个步骤,以按照新的安卓 NDK 创建程序。目前市场可提供几种采用 64 位处理器并且基于安卓系统设备的老旧版本。但制造商必须快速更新这些设备,否则将无法为用户提供充足的软件应用。
2014 年 6 月,Google 宣布安卓将在即将发布的 L 版本中支持 64 位。对于希望发挥设备和应用最佳性能的用户来说,这绝对是一个利好消息。Google 在本次更新过程中所重点强调的优势包括支持更多寄存器、增加可寻址内存空间,并支持全新指令集。
安卓仿真器可支持移动设备中的诸多硬件特性,包括:
对于构建钟爱的设备和应用来说,这是我们所取得的巨大进展。但遗憾的是,在享用这些全新的性能提升优势之前,我们必须等待Android L 的终止。Android L 发布几周后,原生开发套件 (NDK) 版本 10 应公布其对三种 64 位架构(arm64-v8a、x86_64 和 mips64)的支持,从而能够运行新版安卓系统。如果您已使用 Java 构建应用,您的代码将基于全新 x86 64 位架构自动实现性能提升。Google 已将 NDK 更新至版本 10b,并添加了仿真器映像,开发人员可使用该插件在内置了英特尔 64 位芯片的设备上运行其应用。
请记住,NDK 仅用于原生应用,而无法支持在常规安卓 SDK 上使用 Java 构建的应用。如果您希望应用基于 64 位运行,或者需要更新至最新版本的 NDK,请访问开发人员门户网站下载。
原生开发套件 (NDK) 是一套工具集,可支持您使用诸如 C 和 C++ 等原生代码语言实施部分应用。对于某些应用来说,这可能会有所帮助,因为您可以重复利用通过这些语言编写的现有代码库,但大多数应用无需使用安卓 NDK。使用 NDK 时,您需要平衡其优势与劣势。值得注意的是,使用安卓的原生代码通常不仅无法实现显著的性能提升,而且还会提高应用的复杂性。请在应用确实需要的时候使用 NDK,而不要仅仅出于使用 C/C++ 编程的目的。
您可通过以下网址下载最新版安卓 NDK:https://developer.android.com/tools/sdk/ndk/index.html
在本章节中,我将简单回顾如何使用安卓 NDK 编译示例应用。
$ANDROID_NDK/samples/san-angeles
原生代码位于 jni/
目录:
$ANDROID_NDK/samples/san-angeles/jni
针对指定 CPU 架构编译原生代码。安卓应用可能在一个 apk 文件中包含面向多个架构的库。
您需要在 jni/
目录中创建Application.mk
文件来设定目标架构。下列行可编译面向所有支持架构的原生库:
APP_ABI := all
通常,最佳的方法是指定目标架构列表。下行命令可编译面向 x86 和 ARM 架构的库:
APP_ABI := x86 armeabi armeabi-v7a
由于我们要构建 64 位应用,因此我们需要编译面向 x86_64 架构的库:
APP_ABI := x86_64
运行示例目录中的以下命令来构建库:
cd $ANDROID_NDK/samples/san-angeles
完成构建后,打开 Eclipse* 中作为安卓应用的示例,并点击“运行”。选择仿真器或连接的安卓设备,以在这些设备中运行应用。
为支持所有可用设备,您需要编译面向所有架构的应用。如果带有库(面向所有架构)的 apk 文件太大,请考虑使用 Google Play 多个 APK 支持 中的指令,为各平台准备单独的 apk 文件。
查看支持的架构
您可以使用该命令查看 apk 文件中所包含的架构:
aapt dump badging file.apk
下面的命令列出了所有架构:
native-code:'armeabi', 'armeabi-v7a', 'x86', 'x86_64'
还有一种方法是打开压缩的 apk 文件并查看 lib/
目录中的子目录。
当以 64 位模式编译程序时,其消耗的内存高于 32 位版本。人们通常忽视这种较高的内存消耗,但 64 位应用的内存消耗量有时是 32 位应用的三倍。内存消耗量由下列因素决定:
相比于 32 位系统,64 位系统通常为用户应用提供更多的内存。因此,如果在 2GB 内存的 32 位系统中,程序的内存消耗量为 300MB,而在 8GB 内存的 64 位系统中,该程序则需消耗 400MB,那么在相关单元中,该程序在 64 位系统中所消耗的内存可降低 3 倍。它的劣势在于性能损失。尽管 64 位程序的速度更快,但从内存中提取更多数据会抵消所有优势,甚至会降低性能。内存和微处理器(高速缓存)之间的数据传输也代价高昂。
可降低内存消耗量的另一种方法是优化数据结构。还有一种方法是使用内存节约型数据类型。例如,如果我们需要保存大量整数,并知道其数值不会超过 UINT_MAX,我们可以使用 "unsigned" 类型替换 "size t" 类型(将在下一章节中讨论)。
使用地址运算中的 ptrdiff_t 和 size_t 类型不仅能够额外提升性能,还可确保代码安全。例如,将大小不同于指示器容量的 int 类型用作指数,可增加二进制代码的数据转换命令。我们可能有 64 位代码,且指示器的大小为 64 位,但 int 类型的大小保持相同 - 32 位。
我们无法通过简单的例子来说明size_t 优于 unsigned。为公平起见,我们使用编译器的优化功能。但优化后代码的两个变量通常大不相同,以致于无法有效证明这种巨大的差异性。我们在尝试 6 次之后终于成功创建了简单示例。但示例还很不理想,因为它仅表示编译器能够使用 size_t 构建更高效的代码,而不是包含了上述不必要数据类别转换的代码。思考一下这个代码,它能够以倒序排列数组项:
01 |
|
02 |
unsigned arraySize; |
03 |
... |
04 |
05 |
for (unsigned i = 0 ; i < arraySize / 2 ; i++) |
06 |
{ |
07 |
float value = array[i]; |
08 |
array[i] = array[arraySize - i - 1 ]; |
09 |
array[arraySize - i - 1 ] = value; |
10 |
} |
示例中的变量 "arraySize" 和 "i" 含有 unsigned 类型。您可以轻松地用 size_t 替换它,并如表 1 所示对比一小段汇编代码。
表 1 - 使用 unsigned 类型和 size_t 类型对比 64 位汇编代码片段
array [arraySize - I - 1] = value; |
|
---|---|
arraySize, i : unsigned |
arraySize, i : size_t |
mov eax, DWORD PTR arraySize$[rsp] sub eax, r11d sub r11d, 1 add eax, -1 movss DWORD PTR [rbp + rax*4], xmm0 … |
mov rax, QWORD PTR arraySize$[rsp] sub rax, r11 add r11, 1
movss DWORD PTR [rdi + rax*4 - 4], xmm0 … |
使用 64 位寄存器时,编译器成功构建了更加简洁的代码。我们不想说使用 unsigned 类型(栏 1)创建的代码比使用 size_t 类型(栏 2)创建的代码慢。难以比较现代处理器的代码执行速度。但您可从本例中看出来,使用 64 位类型时编译器构建的代码更简短、更快。
现在让我们来看看这些示例,它们的ptrdiff_t 和 size_t 类型在性能提升方面具备优势。为便于展示,我们选取一种计算最小路径长度的简单算法。
函数 FindMinPath32 由带有 unsigned 类型的经典 32 位方式编写而成。函数 FindMinPath64 与之唯一的不同之处是其中所有的 unsigned 类型均被 size_t 类型替换掉。除此之外没有其他不同之处。现在我们来对比两种函数的执行速度(表 2)。
表 2 - 函数 FindMinPath32 和 FindMinPath64 的执行时间对比
型号和函数 | 函数执行时间 | |
---|---|---|
1 | 32 位编译模式。函数 FindMinPath32 | 1 |
2 | 32 位编译模式。函数 FindMinPath64 | 1.002 |
3 | 64 位编译模式。函数 FindMinPath32 | 0.93 |
4 | 64 位编译模式。函数 FindMinPath64 | 0.85 |
如表 2 所示,基于 32 位系统的函数 FindMinPath32 缩短了与执行速度相关的时间。该表格的目的在于清楚地进行对比。第一行中
FindMinPath32 函数的运行时间为 1(基于 32 位系统)。这是我们的基准测量单位。
第二行显示 FindMinPath64 函数的运行时间也为 1(基于 32 位系统)。因为 32 位系统上的 unsigned 类型与 size_t 类型一致,所以 FindMinPath32 和 FindMinPath64 函数的运行时间也相同。细微误差 (1.002) 仅仅表明测量过程中出现了细小错误。
我们在第三行中发现性能提升了 7%。我们希望是重新编译面向 64 位系统的代码实现了性能提升。
第四行是我们最感兴趣的地方。性能提升了 15%。仅用 size_t 类型替换 unsigned 类型后,编译器所构建的代码便在效率上得到了显著提升 - 8%!
这清楚地表明,与机器语言大小不同的数据会降低运算性能。只需用ptrdiff_t 和 size_t 类型替换 int 和unsigned 类型,便可显著提升性能。该结果可首先应用于这些情形,即在索引数组和地址运算中使用这些数据类型,以及安排循环。
注:PVS-Studio 是一种面向 C、C++ 和 C++11 的商用静态程序分析工具。尽管不是针对优化程序而设计,但这种工具可帮助您进行代码重构,从而提高代码效率。例如,当 >memsize-types 修复与地址运算相关的潜在错误时,您可以使用 memsize-type ,支持编译器构建更加优化的代码。
内在函数是依赖于系统的特别函数,可执行无法以 C/C++ 级别的代码所执行的行为,也可更高效地执行这些函数。事实上,它可帮助您消除不需要或通常无法使用的行内汇编代码。
由于缺乏开销费用调用通用函数,编程人员可使用内在函数创建速度更快的代码。当然,这种代码会更大一些。MSDN列出了能够用内在版本替换的函数。它们包括 memcpy、strcmp 等。
除了能够自动用内在版本替换常用函数,您还可以在代码中明确使用内在函数。这将会取得显著优势,原因包括:
在自动模式下使用内在函数(得益于编译器开关的帮助)可助您提升性能,且使用“手动”开关还可获得更大的性能提升。因此,使用内在函数是一种非常有用的方法。
数据结构对齐是在计算机内存中安排和访问数据的一种方法。它涉及两个相互独立,相互关联的问题:数据对齐和数据结构填充。当从内存地址读取数据或将数据写入内存地址时,现代计算机需在语言大小 (e.g., 4-byte chunks on 32-bit systems) 或更大的数据块中执行数据结构对齐。数据对齐 指将数据放入相当于该语言大小几倍的内存偏移量,由于 CPU 处理内存的方式,这样可提升系统的性能。为了使数据对齐,可能需要在上一个数据结构的结尾和下一个数据结构的起点之间插入一些无任何意义的字节,即数据结构填充。
例如,当计算机的语言大小是 4 个字节(在大部分计算机上是 8 位,但可能因系统不同而不同)时,待读取的数据应在乘以 4 的内存偏移量中。如果不是这种情况,例如,数据从第 14 个字节而非 16 个字节开始,计算机则必须读取 2 个 4 字节的数据块,并在请求数据读取之前执行计算,或生成 对齐错误.尽管前一个数据结构在第 13 个字节处结束,但下一个数据结构应在第 16 个字节处开始。因此,需在这两个数据结构之间插入两个填充字节,以便下一数据结构与第 16 个字节对齐。
尽管所有现代计算机都面临数据结构对齐这一基本问题,但许多计算机语言和计算机语言实施功能均可自动进行数据对齐
最好在一些案例中帮助编译器通过手动定义对齐来增强性能。例如,流指令扩展 (SSE) 数据必须在 16 字节边界上对齐。您可以通过以下方法实现对齐:
1 |
|
2 |
// 16-byte aligned data |
3 |
__declspec(align( 16 )) double init_val[ 2 ] = { 3.14 , 3.14 }; |
4 |
// SSE2 movapd instruction |
5 |
_m128d vector_var = __mm_load_pd(init_val); |
6 |
7 |
8 |
|
安卓运行时 (ART) 应用由 Google 开发,用以替代 Dalvik。该运行时具备多项全新特性,可显著提升安卓平台和应用的性能与流畅性。ART 以 Android 4.4 KitKat 形式推出;在 Android 5.0 中,它可以全面取代 Dalvik。与 Dalvik 不同,ART 使用即时 (JIT) 编译器(运行时),这意味着 ART 可在安装期间编译应用。因此,程序可提高执行速度,电池续航时间也由此得以改善。
在向后兼容性方面,ART 与 Dalvik 使用相同的字节代码。
除了能够加快速度外,ART 还可提供另一种重要优势。由于 ART 直接运行应用机器代码(本地执行),其 CPU 占用率比在 Dalvik 上编译即时代码时低得多。CPU 占用率越低,电池消耗量越小,这是便携式设备通常具备的另一大优势。
既然如此,那为何不更早实施 ART?让我们来看看预 (AOT) 编译的劣势。首先,生成机器代码所需的空间比现有字节代码大。第二,代码预编译与安装同时进行,因此安装流程耗费的时间更长。最后,执行时所占的内存空间更大。这意味着只能同时运行较少的应用。首台安卓设备面市时,其内存和存储容量较小,直接导致了性能瓶颈。这就是为何 JIT 方法当时是客户首选的原因。如今,内存更加便宜,即使在低端设备上也具备更高的冗余性,因此 ART 成为自然的选择。
也许最重要的改进在于,当 ART 安装在用户设备上时,它可将您的应用编译成原生机器代码。由于编译器面向特定架构(比如 ARM、x86 或 MIPS)而构建而,具备预编译功能,因此可帮您实现显著的性能提升。这样,每次运行应用时,再也无需采用即时编译。因此,安装应用的时间更长,但如果在 Dalvik VM 运行时期间执行多个任务的同时(例如正在执行的类别和方法验证)启动应用,它可加快启动速度。
其次,ART 团队还合作优化了垃圾回收器(GC)。Dalvik 执行每次 GC 需要暂停两次,共耗费 10 毫秒,而现在只需暂停一次,通常不到 2 毫秒。他们还实现了 GC 部分运行的并行化,并优化了回收战略,使 GC 能够感知设备状态。例如,只有当手机锁定时才会运行完整的 GC,而且是否对用户做出响应已经不再重要。对于对帧速下降比较敏感的应用来说,这是一次巨大的改进。此外,未来的 ART 版本还将包含压缩回收机制,可将配置内存的数据块移到连续数据块之中,从而减少碎片,并无需牺牲旧版应用来分配大型内存区域。
最后,ART 可充分利用名为 “Rosalloc” 的全新内存分配程序(插槽运行分配程序)。大多数现代系统均使用基于 Doug Lea 设计(采用单个全局内存锁定)的分配程序。在面向对象的多线程环境中,这样会干扰垃圾回收器和其他内存操作。在 Rosalloc 环境中,Java 中常见的小型对象在不带锁定的线程局部区域中进行分配,而大型对象通常自带锁定。因此,当您的应用尝试为新对象分配内存时,无需等待垃圾回收器释放不相关内存区域。
目前,Dalvik 是安卓设备默认的运行时,而 ART 是部分安卓 4.4 设备(比如 Nexus 手机、Google Play 版设备、运行常用安卓系统的 Motorola 手机以及其他智能手机)的可选配置。ART 目前正处于开发阶段中,目前正在广泛征求开发人员和用户的反馈意见。一旦完全稳定下来,ART 将最终取代 Dalvik 运行时。到那时,如有兴趣尝试这项全新功能并体验其优越性能,使用兼容设备的用户可从 Dalvik 切换至 ART。
如需切换或启用 ART,您的设备必须运行 Android 4.4 KitKat 并能够兼容 ART。您可通过“设置” ->;“开发人员选项” ->;“运行时选项” 轻松打开 ART 运行时。(提示:如果在设置中找不到开发人员选项,您可前往“关于手机”,向下滚动,并轻击版本号 7 次,以启用开发人员选项。)手机将重启,并开始优化面向 ART 的应用,这一过程可能持续 15-20 分钟,取决于您手机安装应用的数量。您还将注意到,启用 ART 运行时后,安装应用的大小将所有增加。
注:切换至 ART 后,当您首次重启设备时,设备将再次优化所有应用,这一过程确实有些令人不悦。
由于 Dalvik 是安卓设备的默认运行时,部分应用可能无法在 ART 中运行,尽管大多数现有应用兼容 ART 并应该能够正常运行。但如果您使用 ART 时遭遇漏洞或应用崩溃,那么请最好切换回 ART 并保持在该状态。
切换至设备上的 ART 要求您知道切换选项在设备中的所处位置。Google 将其隐藏在设置选项之下。幸运的是,我们可以通过一种技巧启用基于 Android 4.4 KitKat 的设备的 ART 运行时。
免责声明:在尝试之前,您必须备份数据。如果设备因此变成砖头机(无论怎么尝试都无法打开),英特尔概不负责。尝试有风险,后果请自负!
启用 ART 时,请认真遵循以下步骤:
如果您希望恢复 Dalvik 运行时,只需采取上述步骤,并重新命名 persist.sys.dalvik.vm.lib file to libdvm.so 中的文本。
Google 发行了面向 Android L(即将面市)的 64 位仿真器映像 - 但仅面向英特尔 x86 芯片。全新的仿真器可支持开发人员构建或优化面向即将发布的 Android L 操作系统及其全新 64 位架构的老旧应用。迁移至 64 位能够增加可寻址内存空间,并支持更多的寄存器和面向开发人员的全新指令集,但 64 位应用无法因此而实现加速。
Java 应用由于其字节代码由全新的 64 位 ART VM 解译,因此可自动获得 64 位的卓越优势。这也说明无需对纯 Java 应用进行调整。基于 Android NDK 构建的应用将需要优化,以包含 x86_64 构建目标。英特尔可建议如何将目标定位 ARM 的代码移植到 x86/x64。借助全新仿真器,开发人员将能够创建面向基于英特尔® 凌动™ 处理器的芯片的应用。
英特尔为开发人员提供了丰富的工具和全面的安卓系统支持,特别是其英特尔® 硬件加速执行管理器(英特尔® HAXM)和一系列英特尔凌动操作系统映像。安卓程序员可定期在仿真的英特尔架构上进行测试,即使大多数部署均针对 ARM 设备。此外,全新仿真器也可 64 位升级至 HAXM 加速器,因此使用 HAXM 将会变得更具吸引力。英特尔引言:
"显然,我们不仅仅致力于交付行业首款面向英特尔架构的 64 位仿真器映像和安卓 L 开发人员预览 SDK 内的 64 位英特尔 HAXM,而且致力于在诸多领域实现更多创新,例如今年早期的首款面向安卓KitKat 的 64 位内核、64 位安卓原生开发套件 (NDK),以及在过去 10 年间不断实现的其他 64 位改进功能。"
在 32 位移动设备迁移至 64 位的进程中,英特尔架构是否也会有所变化?
安卓 SDK 包含一个虚拟移动设备仿真器,可在您的计算机上运行。该仿真器可支持构建安卓应用的原型,并开发和测试这种应用,而无需使用物理设备。安卓仿真器可模仿常用移动设备(无法拨打电话的设备除外)的所有软硬件特性。它可提供多个导航和控制键,您可使用鼠标或键盘“按”这些键,为您的应用生成事件。它还可提供屏幕显示您的应用以及其他活跃的安卓应用。
为了支持您更加轻松地建模和测试应用,仿真器使用 Android 虚拟设备 (AVD) 配置。AVD 可支持您定义仿真手机的部分硬件方面,并创建多项配置测试多个 Android 平台和硬件排列。如果您的应用基于仿真器运行,它可使用 Android 平台的服务调用其他应用、访问网络、播放音频和视频、保存和检索数据、通知用户,并提供图形转换和主题。