Android常见优化方式-SparseArray

        在Android应用开发SDK中,有一个SparseArray类实现了HashMap的相似功能,如果key值的类型是int时,可以使用它来替代HashMap,以达到节省内存的目的。SparseArray采用的是有序数组来保持数据,而且是两个有序数组,一个用于保存int型的key值的数组keys,另一个用于保存value值的数组values,key值和value值在两个数组中的位置是一一对应的。在两个数组中索引号相同的数据构成了一对数据,也即相当于map表的KV键值对,只是没把它们放在一起,而是被单独分开存放了,因此在keys数组中找到了key值后,根据它的索引值,就可以从values数组中得到对应的value值,这正是SparseArray的KV键值对映射的原理。在查找操作时使用的是二分法查找,因此它的复杂度是O(lnN)。

        该类的数据结构使用的是数组,比较简单,key和value值按key值的顺序保存在数组里面,数据项和数据项之间没有“空隙”,因此内存占用比HashMap要低得多,而且不用像HashMap那样需要进行整数类型数据的装箱操作,所以该类在手机端等一些对内存敏感的场景要比使用HashMap更合适。不过,在插入数据时也涉及到了排序操作,当数据量比较大的时候可能会效率低,故不适合做大量数据的操作;其二就是该类的key只能是int型的,在有些地方限制了其使用。

        在看它的实现源码时,会发现它使用了两个数组来分别保存key和value值,为什么这样做呢?猛地一看,这样做缺点很明显:首先,需要分两次访问数组才能完成数据的读写操作,一次是访问Keys数组,另一次访问Values数组。其次是数据被分散了,这样在CPU读取数据时,需要先后两次刷新到它的数据高速缓存Cache,第一次是keys数组,找到key值后,再去values数组中获取对应的value值时,需要再次刷新Cache。那么,如果把它们放在一起组成kv键值对,作为一个整体,保存在同一个数组中,CPU在装载数组数据到Cache的时候,可以同时把KV键值对一起加载到Cache,岂不更省事?

      例如,把key和value组合在一起作为整体,比如数据结构是这样的:
class KvEntry {
    int key;
    Object value;
}

        假设保存KvEntry对象的数组名称为entries,显然,保存在entries里面的都是KvEntry的对象引用,根据key值使用二分法查找entries时,先得到一个KvEntry数据项,从中得到它的key值进行比较,经过平均lnN次后,得到key值相等的KvEntry数据项,直接从中返回value对象即可。

       再看一下两个数组时的查找过程,操作是这样的:先根据key值在keys数组中进行查找,经过平均lnN次后,找到了key所在的数组下标索引,然后使用该索引从values数组中取得value值。显然,第二次数组访问操作的复杂度为O(1),也就是说,虽然分别访问了两个数组,但是访问第二个数组仅仅是使用索引下标读了一次数组,开销很低,也还说得过去。

        在查找数据时,数据在一个数组和被分成两个数组的查找次数是一样多的,即它们的数据比较运算时间完全一样,没有区别,问题出在CPU访问内存的开销上。

        我们知道,CPU运算速度要比内存读写速度快很多,这样会使CPU花费很长时间等待数据到来或把数据写入内存,为了解决CPU运算速度与内存读写速度不匹配的矛盾,为CPU设计了高速缓存Cache,当CPU调用大量数据时,就可先缓存中调用,从而加快读取速度。

        分析一下在两种情况下是如何把数据装载到Cache的。

       在分为两个数组时,keys数组保存的是int型,CPU把它们加载到Cache后,可以直接使用它们进行比较操作。而当使用一个数组保存KV键值对时,由于保存的是对象引用,因此CPU把它们从内存加载到Cache后,得到的是KvEntry对象引用,也就是KvEntry对象在内存中的地址。因此,在进行比较时,需要再次从内存中加载KvEntry对象到Cache中,注意此时有可能要覆盖会掉上次刚刚加载到Cache中的entries数组中的数据,也就是说,CPU每一次对KvEntry进行比较,就得从内存中加载一次数据到Cache中去,有时候甚至加载两次。

        由于数据在一个数组和分成两个数组的查找次数是一样多,二者的性能上区别取决于数据从内存加载到Cache的性能和次数了。从网上查找了相关数据,可以看到CPU访问一次L1 Cache开销时间是0.5纳秒,而访问一次内存时间开销是100纳秒,二者差距了200倍,CPU进行分支预测时间开销是5纳秒,也远远小于100纳秒。可见,性能瓶颈点不是比较次数的多少,而是访问内存次数的多少,访问内存的次数越多,性能越慢。尽管也有lnN次的比较运算的时间开销,相比内存访问的时间开销反而微不足道。因此,分为两个数组时,虽然比使用一个数组时多了一次访问values数组的开销,但是仍然比使用一个数组速度要快的多。

        SpareArray使用了两个数组来保存key和value,根本原因就是访问key和value的次数不一样,而且key数据能够形成“密集型”数据。因此,让次数多的分离出来,这样,加载它到CPU的高速缓存Cache中时,能够一次加载更多的数据,在查找时,可以比较更多的数据,从而节省了时间。ArrayMap也正是遵循这样的原则进行设计的,因为ArrayMap的key值得类型是对象型的,不是int或者long型,因此,为了让key成为密集型的数据,就在keys数组中保存key对象的hashCode,因为hashCode是int型。同样数据库在设计索引时采用的B树数据结构,也是基于这样的考虑,由于磁盘的一次读写比访问内存开销要大得多,那就使用B树减少访问硬盘的次数,增加读取磁盘时的数据量来提高性能。

参考:

磁盘、内存、Cache访问速度 https://blog.csdn.net/wind19/article/details/22692737

猜你喜欢

转载自blog.csdn.net/moter/article/details/80279809
今日推荐