订阅移动开发RSS CSDN首页> 移动开发

如何在 Android* Marshmallow 中优化 Java* 代码

发表于2016-06-12 13:11| 次阅读| 来源CSDN| 0 条评论| 作者Intel

摘要:随着 Android* 生态系统不断演进,其各个组成部分也向最终用户提供了更出色的特性和用户体验。 Android Marshmallow 版本采用了一些全新特性和功能,以及多个 Android Runtime* (ART) 增强特性,可提供更高的应用性能、更低的内存开销和更快的多任务处理能力。

引言

随着 Android* 生态系统不断演进,其各个组成部分也向最终用户提供了更出色的特性和用户体验。 Android Marshmallow 版本采用了一些全新特性和功能,以及多个 Android Runtime* (ART) 增强特性,可提供更高的应用性能、更低的内存开销和更快的多任务处理能力。 本文主要介绍 Marshmallow 版本;此外,还有一篇相同的文章介绍 Lollipop

在使用每个版本时,应用开发人员必须了解 Android 虚拟机中做了哪些变更。 换言之,过去能够获得最佳性能的特性可能不再那么有效,新技术可能带来更佳的效果。 基本上很少发布有关 Android 变更的信息,因此开发人员必须通过试错弄清楚其中的不同。

本文从编程人员的角度介绍了生成 Marshmallow 代码的 Android* 运行时,并提供了一些有效技术,帮助开发人员向最终用户提供最佳用户体验。 本文介绍的提示和技巧可帮助您更好地生成代码并获得更出色的性能。 此外,文章还介绍了为何启用某些优化,或未在开发人员的 Java* 代码中使用它们。

Java 编程技巧助您实现更出色的 ART 性能

Android 生态系统非常复杂。 将 Java 转换为面向基于英特尔的设备的英特尔® x86 代码的编译器是其复杂的因素之一。 Android Marshmallow 包括一款全新的编译器 — 优化编译器,该工具可以优化 Java 程序,为其提供更出色的代码性能(较之 Lollipop 传统的 Quick 编译器)。 目前在 Marshmallow 中,几乎所有的程序都使用优化编译器进行编译。 Android System Framework 方法使用 Quick 编译器进行编译,以便更好地支持 Android 开发人员进行调试。

函数库选择和精度损失注意事项

浮点运算可提供多个相似运算变量。 在 Java 中,Math 和 StrictMath 可为浮点运算提供不同的精度级别。 虽然 StrictMath 提供的运算更易于重复,但是在多数情况下,Math 函数库即可满足需求。 以下方法可计算余弦:

 

 

1 public float myFunc (float x) { float a = (float) StrictMath.cos(x); return a; }

但是,如果精度损失可接受,开发人员可以使用 Math.cos(x) 而非 StrictMath.cos(x)。 Math 类使用面向英特尔® 架构的 Android Bionic 函数库进行了优化。 英特尔的 ART 实施可在 Bionic 库中调用数学函数库函数,其速度是同等 StrictMath 方法的 3.5 倍。

有一些情况必须使用 StrictMath 类,不能用 Math 类替换。 但是,多数情况下可以使用 Math。 这是精度损失的问题,而且还取决于算法和实施。

递归算法支持

递归调用在 Marshmallow 中比在 Lollipop 更有效。 当使用递归的方法为 Lollipop 编写代码时,self method 参数始终通过 Dalvik* Executable Format (dex) 高速缓存加载。 在 Marshmallow 中,递归函数同样使用初始参数列表中的该 self method 参数,而非从 dex 高速缓存中重新加载它。 当然,递归深度越深,Lollipop 和 Marshmallow 之间的性能差别便越大。 但是,当可以使用迭代版本的算法时,Marshmallow 版本的性能仍然较高。

使用 array.length 已取消边界校验

Marshmallow 中的优化编译器能够消除某些数组边界校验。 参阅本文,了解如何消除边界校验。

空循环:

 

 

1 for (int i = 0; i < max; i++) { }

变量 i 称归纳变量 (IV)。 如果使用 IV 访问数组,而且循环在每个元素上进行迭代,那么在最大值明确定义为数组长度时可以消除数组边界校验。 参阅本文了解关于归纳变量的更多信息。

示例

考虑下面的示例:代码使用变量尺寸作为 IV 的最大值:

 

 

1 int sum = 0for (int i = 0; i < size; i++) { sum += array[i]; }

在该程序中,数组索引 i 相当于尺寸。 我们假定尺寸定义在方法之外,并作为参数传递,或者定义在方法之内。 无论在哪种情况下,编译器都不能推断该尺寸为数组的长度。 由于该不确定性,编译器必须在 i 上生成一个运行时边界校验,作为每个数组访问的一部分。

按照如下方式重新编写代码,编译器将消除运行时边界校验。

 

 

1 int sum = 0for (int i = 0; i < array.length; i++) { sum += array[i]; }

包含两个数组的循环: 高级边界校验取消 (BCE)

上一部分展示了 BCE 优化以及如何在编译器中激活它的简单案例。 但是,有一些算法使用一个循环处理多个单独的长度相同的数组。 在这种情况下,编译器必须在所有访问上运行空值和边界校验。

下文更深入地介绍了 BCE 以及如何在使用多个数组时启用它。 通常,需要重写代码来启用编辑器以优化循环。

本部分的示例展示了在同一个循环中使用多个数组访问:

 

 

1 for (int i = 0; i < age.length ; i++) { totalAge += age[i]; totalSalary += salary[i]; }

该代码中有一个问题。 该程序不是校验工资的长度,可能会遇到数组越界异常 (array index out of bounds exception) 的风险。  该程序应校验长度是否与进入循环之前相同,如:

 

 

1 for (int i = 0; i < age.length && i < salary.length; i++) { totalAge += age[i]; totalSalary += salary[i]; }

目前代码已经正确,但是仍然还有一个问题,因为 BCE 无法在这种情况下使用。

在上述循环中,编程人员访问了两个单维数组:年龄和工资。 尽管归约变量,即变量 i,能够校验两个数组的长度,但是编译器无法取消多数组情况下的边界校验。

在显示的循环中,两个数组没有使用相同的内存。 因此,两个数组字段上执行的逻辑运算彼此独立。 将运算划分为两个独立的循环,如下:

 

 

1 <strong>for (int i = 0; i < age.length; i++) { totalAge += age[i]; } for (int i = 0;  < salary.length;  i++) { totalSalary += salary[i]; }</strong>

划分循环后,优化编辑器将消除两个循环中的数组边界校验。 Java 代码可将同样简单的循环的速度加快 3 至 4 倍。 

此处可以使用 BCE,但是现在函数包含两个循环,这有可能导致代码膨胀。 根据目标架构和循环尺寸或完成的次数,这可能会对最后生成的代码尺寸产生影响。

多线程编程技巧

在多线程程序中,开发人员在访问数据结构时必须小心。

假定程序在下面显示的循环前能够产生四个相同的线程。 然后每个线程访问一组名为 thread_array_sum 的整数,该整数是通过变量 myThreadIdx 访问每个线程的单元格,是唯一的整数,可识别每个线程。

 

 

1 <strong>for (int i = 0; i < number_tasks; i++) { thread_array_sum[myThreadIdx] += doWork(i); }</strong>

一些设备架构(如英特尔® 凌动™ x5-Z8000 处理器系列)不具备能够让所有处理器内核共享的 LastLevelCache (LLC)。 虽然使用单独 LLC 的响应速度更快(因为高速缓存保存至一个或两个处理器内核中),但是维护二者之间的连贯性可能导致行在 LLC 之间“弹跳”。 这种弹跳可能导致出现性能降级和处理器核心扩展问题。 参阅本文,了解更多信息。

由于高速缓存布局的问题,多线程写入相同的阵列可能会导致性能降级,因为可能会出现高级高速缓存抖动。 编程人员应使用本地变量来存储中间结果,然后更新数组。 然后,循环将变为:

 

 

1 <strong>int tmp = 0for (int i = 0; i < number_tasks; i++) { tmp += doWork(i); } thread_array_sum[myThreadIdx] += tmp;</strong>

在这种情况下,数组元素 thread_array_sum[myThreadIdx] 不受内部循环的影响,doWork() 的累积值可以存储到循环外的数组元素中。 这可显著降低潜在的高速缓存抖动。 执行 thread_array_sum[myThreadIdx] += tmp 指令过程中仍然会出现抖动,但是可能性大大降低。

将共享数据结构存储到循环中并不合适,除非在每个循环迭代结束时,所存储的值对其他线程可见。 一般而言,这种情况至少需要使用 volatile 字段和/或变量,但是这一内容不在本文的讨论内容之内。

适用于低存储设备的最佳代码性能技巧

Android 设备能够用于多种内存和存储配置。 Java 程序应可在设备上轻松优化,而不管内存大小如何。 低存储设备很可能会优化空间,即使用 ART 编译选项。 在 Marshmallow 中,不会编译尺寸大于 256 字节的方法,以便节省设备的存储空间,因此包含大型热门方法的 Java 程序将在解释程序中执行,且执行性能较差。 如要在 Marshmallow 中获得最佳性能,应在小型方法中使用常用的代码,以便全面支持编译器优化。

ART 更有可能使用编写为小型方法的 Java 程序,而不考虑其设备存储限制,这最多可将大型 Android 应用的性能提升 3 倍。

总结

每个 Android 版本都会采用新的元素和不同的技术。 KitKat 和 Lollipop 亦不例外,Marshmallow 对 Android 生态系统中的编译器技术做了很大的变革。

在 Lollipop 中,ART 使用 AoT 编译器,该工具一般可在安装时将用户应用转换为原生代码。 但是,Marshmallow 没有使用 Lollipop Quick 编译器,而使用了一款名为优化编辑器的新编译器。 虽然优化编译器有时会依从 Quick 编译器,但是它是 Android Java 二进制代码生成的新核心。

每种编译器都有自己的怪癖和优化方式,因此可能会生成不同的二进制代码,具体取决于 Java 程序的编写方式。 本文介绍了一些大家能够在 Marshmallow 版本中看到的主要区别,开发人员应该在使用时注意到。

从数学函数库使用到边界校验取消,优化编译器中增加了许多新特性。 其中一些很难通过示例来展示,因为许多优化是在“内部”完成的。 我们只知道 Android 内的编译器展现出较高的成熟度,因为其技术越来越高级,正在慢慢赶上其他优化编译器。 随着编辑器的演进,开发人员将可更好地优化其编写的代码,并提供更出色的用户体验,为所有人带来益处。

致谢(按字母排序)

Johnnie L Birch, Jr.、Dong Yuan Chen、Chris Elford、Haitao Feng、Paul Hohensee、Aleksey Ignatenko、Serguei Katkov、Razvan Lupusoru、Mark Mendell、Desikan Saravanan 和 Kumar Shiv

查看全文

更多内容请详见:英特尔开发人员专区

0
0