性能小贴士

Posted 流浪三毛

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了性能小贴士相关的知识,希望对你有一定的参考价值。

性能小贴士

本文主要介绍一些代码优化方面的小贴士,结合起来使用能整体性的提升应用性能。但是,这些技巧不可能带来戏剧性的性能改变。合适的算法和数据结构是解决性能的首选考虑(还有程序的执行流程优化),但这已经脱离了本文的范畴。

本文介绍的小贴士是每个有追求的程序员应有的编码习惯。

关于如何写出高效的代码,这里有两个基本的原则:

  • Don‘t do work that you don‘t need to do

  • Don‘t allocate memory if you can avoid it

面临的现状

一个非常棘手的问题,你的应用运行在不同类型的硬件设备上的。不同版本的虚拟机以不同的运行速度运行在不同的处理器上。通常,不能轻易的下结论说"设备x比设备y快或者慢了多少倍"。尤其是,模拟器和真机的差别非常大,模拟器上的性能测试不能充分反应真机的情况,况且,一个有JIT和一个没有JIT的设备之间也存在巨大的差别。

为了保证你的应用性能在绝大多数的设备上有一个好的表现,确保你的代码在各个方面的高效性并竭尽全力去优化你的性能。

避免创建不必要的对象

对象的创建绝不是免费的。现在主流的垃圾收集器中每个线程的临时对象分配池使得对象的创建更加低廉,但是分配内存的代价还是要比不分配要高昂的多。 正如你在程序里创建了大量的对象,垃圾回收会定期的触发,会造成一点点卡顿的感觉。虽然android在2.3引入了并发的垃圾回收机制,但不必要的工作还是应该避免。

因此,不要创建那些非必需的实例对象。以下的几个例子可能具有参考意义:

  • 如果你有一个方法返回一个string,而且你知道这个返回结果是要被加到一个StringBuffer中去的,那么改变你的方法实现,变成直接添加而不是创建一个短暂的临时对象。

  • 当从一个输入数据中提取字符串,尝试去返回原数据的子串,而不是创建原数据的一个拷贝。采用子串虽然也会创建一个新的string对象,但是数据的内容还是和原数据共用的。

  • 尽可能的使用一维数组,而不是多维的数组。

关于减少不必要的对象创建,我们还可以有以下的实践:

  • 使用Message.obtain()或者handler.ontainMessage()去获取一个Message对象,而不是直接new。

  • 使用优化过的数据结构,SparseArray来替代Map。

  • 使用线程池去重用线程而不是每次新建线程。

  • Handler随意创建,只为把内容放到主线程执行,这个时候可以公用一个Handler。

  • 在那些被高频调用的方法里,避免创建临时对象,比如View的onDraw(),在循环里等等。

  • 在API23以下,当做图片清理设置ImageView.setImageBitmap(null)时,方法内部会创建一个BitmapDrawable对象。所以,我们 使用ImageView.setImageDrawable(null)来代替。

总的来说,尽你所能的避免创建短暂的临时对象。垃圾回收对体验有较大的影响,越少的对象创建意味着越低频的垃圾回收。

Prefer Static Over Virtual

把一个方法定义成static的,相比于对象方法大概会有15~20%的速度提升。这也是一个好的实践,因为你可以从方法声明上分辨出这个方法调用不会改动对象的状态。

使用Static Final声明常量

以下是一个类在顶部的声明:

static int intVal = 42;
static String strVal = "Hello, world!";

编译器会生成一个类的初始化方法,叫做 <clinit>, 会在此类第一次被引用时执行。这个方法会将42存储到intVal,并且会为strVal从类文件的字符串常量池中提取一个引用。当这些值在稍后被引用到,它们通过字段查找被访问到。

我们可以通过使用“final”关键字来提升:

static final int intVal = 42;
static final String strVal = "Hello, world!";

如此一来,这个类的初始化<clinit>方法就不会被调用,因为这两个常量直接存放进dex文件的静态字段初始化器中。指向intVal的引用直接使用42,访问strVal也会使用一个相对廉价的字符串常量而不是通过字段查找的方式。

注意:这个优化只适用于基本类型以及字符串常量,而非任意的引用类型。但是,为常量加上static final的声明是一个好习惯。

避免内部的Getters/Setters

在像C++这样的本地语言中,通过getters方法的形式替代直接的字段访问会比较平常。这在C++里是一个优秀的习惯,在C#或者Java这样的面向对象的语言里也是被经常使用,因为大部分的编译器都做了内联优化。

然而,这在Android上并不是一个好的实践。方法的调用比实例字段的查找要昂贵的多。遵循面向对象的编程习惯提供setter和getter方法是好的,但是在类的内部应该直接使用字段访问。

在没有JIT的情况下,直接的字段访问要比调用一个无用的getter方法快大约3倍。在有JIT(直接的字段方法跟本地访问一样低廉)的情况下,直接的字段访问要比方法调用快大约7倍。(特地做了验证,在我6.0系统的motox设备上大约是10倍)

使用增强的for循环

增强的for循环也叫"for-each"循环,通常用于遍历那些实现了Iterable接口的集合。在集合中,一个迭代器一般会调用hasNext()next()。在使用ArrayList时,一个手写的计数的循环在有JIT的情况下性能大约是没有时的3倍,但是在使用其它的集合时使用增强的for循环的性能跟显示的调用迭代器的性能基本接近。

下面是迭代一个数组的几种可选方案:

static class Foo {
    int mSplat;
}

Foo[] mArray = ...

public void zero() {
    int sum = 0;
    for (int i = 0; i < mArray.length; ++i) {
         sum += mArray[i].mSplat;
    }
}

public void one() {
    int sum = 0;
    Foo[] localArray = mArray;
    int len = localArray.length;

    for (int i = 0; i < len; ++i) {
        sum += localArray[i].mSplat;
    }
}

public void two() {
    int sum = 0;
    for (Foo a : mArray) {
        sum += a.mSplat;
     }
}

zero()是最慢的,因为JIT不能优化掉每次循环中的数组长度的查找。

one()要快一点。它把所有的都放到本地变量,避免了查找。但其实这里只有数组长度的定义能提升性能。

two()在没有JIT的情况下是最快的,在有JIT的情况下跟one()基本一样。

所以,默认情况下我们应该使用增强的for循环,但在处理ArrayList时,如果对性能特别敏感的情况下,我们考虑使用手写的计数循环。(依然是motox手机,100万的数据量遍历,手写的带计数的循环的性能要比for loop快15倍左右)

对于私有的内部类,考虑使用包访问权限代替私有访问权限

如下的类定义:

public class Foo {
    private class Inner {
        void stuff() {
            Foo.this.doStuff(Foo.this.mValue);
        }
    }

    private int mValue;

    public void run() {
        Inner in = new Inner();
        mValue = 27;
        in.stuff();
    }

    private void doStuff(int value) {
        System.out.println("Value is " + value);
    }
}

上述代码中,我们定义了一个私有的内部类(Foo$Inner),这个私有类直接访问了外部类的一个私有方法doStuff(), 同时访问了外部对象的私有的字段。这是合法的,代码也如预期打印出了"Value is 27"。 问题在于虚拟机认为直接从Foo$Inner访问Foo的私有方法是不合法的,因为这两个是不同的类,然而Java的语法又允许这样的访问。为了解决这个问题,编译器生成了一系列的合成方法:

/*package*/ static int Foo.access$100(Foo foo) {
    return foo.mValue;
}
/*package*/ static void Foo.access$200(Foo foo, int value) {
    foo.doStuff(value);
}

当内部类代码需要访问外部类中的mValue字段或者调用doStuff()方法时会调用这些静态方法。这就意味着上述的代码归根结底到了这样一种场景,通过方法的存取来访问字段。在前面有讲到过,存取要比直接的字段方法要慢。所以,这个例子展示了一个特定的语言特性所引起的无形的性能损耗。

多使用系统提供的函数

除了常规的理由外,我们更愿意选择系统函数的原因是因为系统可能会使用汇编的方式去替换方法的实现,这比JIT生成的代码有着更好的性能。典型的有String.indexOf()和相关的APIS。类似的,在有JIT的Nexus One手机上,System.arraycopy()方法要比你手写的循环快9倍左右。

慎用本地方法

使用NDK通过C++等本地代码开发出来的应用的性能并不一定比用Java代码来的高效。至少,Java-Native是有传输消耗的,JIT也无法跨越这个边界进行优化。而且你还会面临以下的问题:

  • 如果你分配了本地的资源,就需要考虑如何及时的回收这些资源

  • 你需要为要支持的CPU架构编译不同的动态库

  • 就算是相同的CPU架构,你可能需要编译不同的版本。举个例子,为G1手机的ARM架构的处理器编译的动态库在Nexus One上不能发挥出应有的效果,而为Nexus One编译的动态库,在G1上根本就跑不起来。

本地代码最主要的使用场景是复用已有C/C++代码把它们使用到Android里来,而不是为了提升速度。

多测量

开始优化前,首先要确认问题是否存在。其次,你要能精确的测量出当前问题的性能数据,否则就不能很好的量化提升效果。(对于性能问题都需要用数据说话)

总结

今天讲的内容有什么用,能减小多少内存,提升多少程序性能呢?

    作为一个有追求的程序员,本文的小技巧都应该纳入个人的编码习惯。

现在的设备硬件性能这么强了,还有必要做这些细节优化吗?

    虽然,现在的设备的硬件性能非常强了,但是应用程序面临的内存、性能等问题仍然非常严峻。比如说,加载几张高清图,
不做任何限制就能让程序崩溃。
    又比如,上面提到过的语言层面的一些性能损耗。面向对象的java语言在创建对象时,会先创建它   所继承的父类的对象。
创建一个继承关系比较复杂的类的实例时,会顺带创建很多父类实例。这就是无形的性能损耗。然而面向对象能带来的好处让
我们只能选择妥协。所以,相比于这些我们无法解决或者很难解决的问题,本文所介绍的技巧可以算是性价比非常高的实践。

以上是关于性能小贴士的主要内容,如果未能解决你的问题,请参考以下文章

本周小贴士#120:返回值是不可触碰的

五个 .NET 性能小贴士

配置高性能ElasticSearch集群的9个小贴士

Q新闻丨MongoDB勒索软件已波及上万数据库!配置高性能Elasticsearch集群的9个小贴士

Tips For Deeping Learning---深度学习小贴士

本周小贴士#93:使用absl::Span