精通安卓性能优化-第四章(三)

来源:互联网 发布:永硕音乐网页源码 编辑:程序博客网 时间:2024/05/19 06:47

其他算法

我们已经看到不同的数据类型对产生的代码怎样的影响,现在是时间去看当处理大量数据的时候一些稍微复杂的算法。
Listing 4-10给出了3个简单的方法:一个简单的调用Arrays.sort()去排列一个array,一个查找Array中的最小的值,另一个把array里面所有的元素加和。

Listing 4-10 Java中的排列、查找和求和

private static void sort (int array[]) {    Arrays.sort(array);}private static int findMin (int array[]) {    int min = Integer.MAX_VALUE;    for (int e : array) {        if (e < min) min = e;    }    return min;} private static int addAll (int array[]) {    int sum = 0;    for (int e : array) {        sum += e;// 这里可能会溢出,我们暂时忽略它    }    return sum;}

Table 4-2显示当给出的array包含一百万个随机元素,完成这些函数需要多少毫秒。额外的,这些方法的变种(使用short、long、float和double类型)同样给出了。如何测量执行时间的更多的信息参考第6章。

Table 4-2 100万这个元素的Array的执行时间


我们可以根据这些结果得到两个结论:
(1) 排列一个short类型的array比其他类型的array速度要快很多
(2) 使用64位的类型(long或者double)比使用32位的类型要慢

Sorting Arrays

对一个16位的array排序会比排列32/64位的值快很多,因为它使用一个不同的算法。int和long array的排序使用快速排序算法的一些版本,short使用counting sort,在线性时间内排序。在这种情况下使用short类型可以一石二鸟:更少的内存消耗(2M而不是4M,对int值的array,8M对long值的array),性能提升。

NOTE: 许多人错误的认为快速排序算法是最有效的排序算法。可以参考安卓源代码里面的Arrays.java,去看每个类型是如何被排序的。

一个不怎么通用的方案是使用多个array保存你的数据。比如,如果你的应用需要保存在0到100000之间整形数,你可能尝试分配一个32位的array用来存储,因为short类型仅可以存储-32768到32767之间的整数。依赖于数据是如何分布,许多值可能等于或小于32767。在这样一个事情中,使用两个arrays会是有利的:一个用来存储0和32767之间的数据(即,一个short的array),另外一个用来存储所有大于32767的值(一个int类型的array)。尽管这很可能导致巨大的复杂度,内存节省和潜在的性能增加使得这个方案是值得的,可能允许你去优化这些方法的实现。比如,一个查找最大元素的方法可能只需要过一遍int值的array。(仅当int类型的array为空的时候需要过一遍short类型的array)。一个类型没有在Listing 4-9和Table 4-2中给出,boolean。实际上,排序一个boolean类型的array是没有什么意义的。然而,某些情况下你需要去存储一个比较大数量的boolean值,并且访问他们通过index。对这个目的,你可以创建一个array。尽管这样可行,也导致了许多位被浪费,因为8位数被分配给每个元素,实际上一个boolean值可以仅仅是true或者false。换句话说,仅仅需要1位去表示一个boolean值。对这个目的,定义了BitSet类:它允许你去在一个array中存储boolean值(允许你去通过index访问),同时array使用最小数量的memory(每个元素一位)。查看BitSet类的公有方法和它的实现BitSet.java,你会注意到几件事吸引了注意力:
(1) BitSet的后端是long的array。通过使用一个int的array,可能达到更好的性能,。(测试显示使用int array将会增加10%的收益)
(2) 为了更好的性能,代码中一些注释指示有些东西需要被改变(比如,FIXME开头的注释)
(3) 你可能不需要这个类的所有feature

基于这些原因,实现你自己的类是可接受的,很可能基于BitSet.java去提升性能。

定义你自己的类

在对象被创建后如果array不需要增长,需要做的操作仅是设置或者取array中的某一位,Listing 4-11给出了一个非常简单的可接受的实现,比如,你要实现自己的Bloom filter。当你使用这个简单实现而不是BitSet,测试显示性能大学提升了50%。我们使用了一个简单的array而不是SimpleBitSet类达到了更加好的性能:单独使用一个array大约比使用SimpleBitSet快50%(即使用一个array大约比使用BitSet对象快4倍)。这个练习实际上和面向对象设计的封闭原则相违背,所以你需要小心。

Listing 4-11 定义你自己的类似BitSet的类

public class SimpleBitSet {    private static final int SIZEOF_INT = 32;    private static final int OFFSET_MASK = SIZEOF_INT - 1; // 0x1F    private int[] bits;    SimpleBitSet(int nBits) {        bits = new int[(nBits + SIZEOF_INT -1) / SIZEOF_INT];    }    void set(int index, boolean value) {        int i = index / SIZEOF_INT;        int o = index & OFFSET_MASK;        if (value) {            bits[i] |= 1 << o; // 设置bit为1        } else {            bits[i] &= ~(1 << o); // 设置为0        }    }    boolean get(int index) {        int i = index / SIZEOF_INT;        int o = index & OFFSET_MASK;        return o != (bits[i] & (1 << o));    }}

或者,如果大多数位被设置为同一个值,可能需要使用SparseBooleanArray去节省内存(很可能以性能为代价)。再一次,可以使用在第二章讨论的策略模式去简单的选择一个实现或者另外一个。
归根究底,这些示例和技术可以被总结为:
(1) 当处理大量的数据,尽可能使用可以满足你的需求的最小的类型。比如,为了性能和内存消费的原因,选择一个sort的array而不是int array。如果你不需要额外的精度,使用float而不是double(如果需要,使用FloatMath)。
(2) 避免从一个类型转换到另外一个。如果可以的话尽可能在你的计算中持续使用一个类型。
(3) 为了更好的性能重构wheel,但是必须小心
当然,这些规则不是一成不变的。比如,你将发现自己处在一个情景,从一个类型转换到另外一个可以实际提升性能,即使存在转换开销。仅在一个问题存在的时候,决定修复,请务实。
多说胜过不说,使用更少的内存是一个好的规则。简单的给其他的task留更多的内存,使用更少的内存可以提升性能,因为CPU可以使用cache快速的访问数据和指令。

访问内存

像我们之前看到的,操作比较大的类型会消费比较大,因为更多的指令会被涉及到。直觉上,更多的指令经常导致更低的性能,因为CPU需要花费额外的时间去执行他们。另外,代码和数据都在内存中,访问内存需要时间。
因为访问内存是耗时操作,CPU会缓存最近访问的内存,不管是要读的内存还是要写的。实际上,CPU通常使用两个级别的cache:
(1) Level 1 cache(L1)
(2) Level 2 cache(L2)
L1 cache通常是比较快的,但是也是比较小的。比如,L1 cache可能是64K(32K的数据cache,和32K的指令cache),L2 cache可能会是512K。

NOTE:有些处理器可能有L3 cache,通常会是几M,但是没在嵌入式设备上看到过。

如果数据或者指令在cache中没有找到,叫做发生一次cache miss。这时数据或者指令需要从内存中取。有几种cache miss:
(1) 指令cache,Read miss
(2) 数据cache, Read miss
(3) Write miss
第一种cache miss是最严重的,CPU在它可以被执行之前,需要等待从内存中读出。第二种cache miss和第一种差不多,尽管CPU可以继续执行其他指令,不依赖于数据被取出。这个结果是一个指令的out-of-order执行。最后一种cache miss严重性要少很多,CPU通常可以继续执行指令。几乎不需要控制write miss,不需要担心它。你需要集中注意在需要去避免的前面两个miss。

Cache的Line size

除了它的总大小,cache的另外一个重要的属性是line size。Cache的每一个entry是一个line,包含几个字节。比如,Cortex A8 L1缓存一个cache line是64字节(16 words)。cache和cache line的背后的思想是本地化的原则:如果你的应用从某一个地址读写,最近的将来很可能从同一个地址读或者写,或者在足够接近的地址。比如,这个行为在Listing 4-9中findMin()和addAll()两个方法的实现是很明显的。
应用去了解cache的大小和cache line的大小是不容易的。然而,知道cache的存在并且有些了解cache如何工作可以帮助你写更好的代码和达到更好的性能。接下来的tips可以帮助你利用cache的优势,不是低级的优化,如第三章所示的PLD和PLI的汇编指令。减少从指令缓存cache read miss的数量:
(1) 在Thumb模式编译你的本地库。不保证你这将使代码运行更快,因为Thumb模式会比ARM慢(因为可能需要执行更多的指令)。参考第二章关于怎么样去在Thumb模式编译库。
(2) 保持你的代码相对的紧凑。尽管不能保证紧凑的Java代码可以产生紧凑的native代码,这仍然是一个经常的正确的假设。

从数据cache减少cache read miss的数量需要:
(1) 再一次,当在array中存储大量的数据尽量使用最小的数据类型
(2) 选择顺序访问而不是随机访问。最大数量的数据重用已经在cache中,防止数据被从cache移除,稍后再次被加载到cache。

NOTE:现在的CPU能够预取内存,自动的避免或者至少限制cachemiss。

通常,在应用程序性能要求严格的地方应用这些tips,经常只是你的代码的一小部分。从一方面说,在Thumb模式下编译是一个简单的优化,而且不会增加你的维护成本。另一方面说,从长期看写紧凑的代码可能使事情更加复杂。没有通用的优化,你有平衡多种选择的责任。
尽管你不需要去控制什么进入了缓存,怎样组织和使用你的数据会影响到最终在cache中的数据,因此可以影响性能。有些情况下,你需要能以特定的方式去排列你的数据使得cache hit最大化,尽管很可能产生巨大的复杂性能和维护成本。

0 0
原创粉丝点击