2009年5月16日星期六

blogspot在中国大陆再次被封

blogspot在中国大陆再次被封,气愤!!!!
blogspot is blocked in china.

这个博客即将关闭,博客另寻新址。
I will leave blogspot soon and stop refresh my blog here....

bye.

2009年5月10日星期日

Java性能优化之实时性[3]

——万事开头难


如果你的应用程序能够满足内存和速度的要求,有时还是不够的,对于某些应用程序来说,尤其是实时系统,它还必须满足良好的用户体验,这就要求你的程序能够做到好的实时性。

关于用户体验,有很多心理学的研究。如果你了解用户懒惰和缺乏耐心等心理,你也许会更加明白,实时性有时会如此重要。相关的文章比如网站打开速度的心理学人之初,性本懒 等等。但是实时性的要求也不局限于用户体验,有许多被称之为 RT 应用的程序要求必须严格地满足实时同步需求,比如控制飞机方向的应用程序不能够有任何原因的延迟,否则将导致灾难性的后果。

由于很多重要原因,Java 语言在实时系统中的应用非常有限,导致Java写出来的应用程序有时实时性很差。这些原因包括 Java 语言设计中固有的不确定性性能影响,例如动态类加载,以及 Java 运行时环境(Java Runtime Environment,JRE)本身的不确定性性能影响,例如垃圾收集器和本地代码编译。

当然,为了解决这些问题,使得Java能够用来构建实时系统,一些规范应运而生,比如RTSJ。关于实时系统和RTSJ,可以参照文章:实时 Java: 使用 Java 语言编写实时系统或者这里的转载。我这里不再赘述。

我主要讲一下我碰到的一个例子: 在某些Swing应用程序中发现,做某些操作时,第一次总是比较慢,以后就好了,有时候时间相差一个数量级,导致使用起来达不到正常的用户RT需求,用户就不高兴了,这正是没有满足用户缺乏耐心的需求,可以说是软障碍。为什么第一次总是比较慢呢,第一次新建慢,第一次编辑慢,第一次弹出某个对话框也慢?这真是奇怪。我之所以出现这样的困惑,其实是因为那时候我还不理解类加载和本地代码编译的具体细节。

在运行时,当我们想生成这个类的对象时,JVM首先检查这个类的Class对象是否已经加载。如果尚未加载,JVM就会根据类名查找.class文件,并将其载入内存。一个与 Java 一致的 JVM 必须延迟加载类,直到程序第一次引用该类。根据被加载类所在的介质(磁盘或其他)的速度、类的大小、类加载器本身的开销,类加载的时间有所不同。加载类的延迟通常高达 10 毫秒。如果需要加载几十或几百个类,则加载时间本身就会引起很长时间的意外延迟。仔细地设计应用程序,使应用程序在启动时加载所有的类,但是这必须手动完成,因为 Java 语言规范不让 JVM 提前执行这一步。

再让我们来看看编译:将 Java 代码编译为本地代码引发了与类加载类似的问题。大多数现代 JVM 开始先解释 Java 方法,然后仅将频繁执行的方法编译成本地代码。延迟编译促成了快速启动,并减少了应用程序运行期间执行的编译数量。但是使用解释后的代码执行任务和使用编译后的代码执行任务在时间上有巨大的差异。对于硬 RT 应用程序来说,由于无法预测何时发生编译,将导致很大程度的不确定性,从而无法有效地规划应用程序的行为。对于类加载,通过在应用程序启动阶段使用 Compiler 类以编程的方式编译方法可以减轻这一问题,但是维护这样的方法非常乏味并且容易发生错误。

一旦找到的问题的原因,那么解决问题的方法很简单,可以在某个合适的空闲的时候,提前进行类加载和编译,那么当第一次进行一些实时操作时,所要用到的类已经加载或者相关代码已经编译,第一次操作便不再慢了。

Java性能优化之实战算法[2]

——当你拿着一把锤子的时候,所有的东西看起来都像钉子。
——当你看到钉子的时候,手上拿着的东西看起来就像一把锤子。


性能优化有时候很像玩一个策略游戏(免费),对手不是计算机,程序员,设计架构,编译器,控制流,而是时间,内存,竞争条件,预算限制等等。有三个资源限制几乎所有应用都会面临:1.CPU速度和有效性2.系统内存3.磁盘I/O。

性能分析的关键是找到瓶颈所在,符合二八原理的是,计算机通常会将80%的时间花在20%的程序上,所以找出这20%的瓶颈很重要,这就是我上篇中所说的,为什么在优化之前一定要分析,不要相当然。我从二八原理学到的就是要统筹分析,使做事效率最优化,这个优化跟我们标题中提到Java性能优化道理上都是一样的。即应该把80%的时间花在造成问题的80%原因上面。呵呵,绕口吧。

比如,我在分析的时候,就发现了这么一个瓶颈,一个微不足道的步骤却消耗了大部分CPU资源,某些情况下CPU花了它96%的时间来处理它。

简化模型,描述起来,就是:

一维数组,每个元素都有保持它是第几个的一个index, 从头到尾插入元素,已有的元素的index需要做出调整,以保持一致性(即:array[i].index = i)。按照我们现在的算法,是每插入一条,其后面的每个元素记录的索引都要做出相应调整。简单估计一下,假设输入规模是N,大概需要O(N^2)的时间,当只有两三万的时候还好,大到8万就显得慢了(是两万时的16倍)。

这个问题本质上是个算法问题,就是怎么样把O(N^2)降到O(N)或者更低的问题,这样便可以大大的提高CPU效率和速度。

开始之前,我想了两种方法:
方法一:绕过这个问题,想办法直接计算出某元素在插入后的值,或者等到多次插入后,一起调整。
方法二:直接解决这个问题,不用字段index保存这个问题,想办法借助类似C++的指针地址,和数组地址来计算index。

对于方法二,需要借助语言特性,因为Java是没有办法取到对象值实例的地址,估计C++也悬,这条途径的复杂度和陷阱太多了,所以很快被抛弃了。
思考方法一:其实对于插入记录整个这个过程,许多记录被调整了多次,其实中间的这些调整是不用关心的,我们只需要知道最后状态是在某个位置就行了。因此,我们想要直接计算插入后的位置,让每条记录只进行一次索引调整。

最后的解决办法就是,首先将插入的记录排序,即从前往后插入,然后要做到取消中间的这些调整,使每条记录只调整一次,对于每个记录来说,只要用一个临时变量来记录它的前面已经插入的多少条记录,从前往后调整,轮到某条记录时,便可以计算出位置索引index的值,保证初始状态和插入结束后状态的一致性。

这个问题看起来是不是似乎很简单?但是,实际情况中的问题要复杂的多,比如,有可能这个插入是在某个大的循环中进行的,也可能在插入的同时,程序需要对这个数组中的记录做操作,可能还是个二维数组等等。关键的是,你需要做出某些努力,重构简化模型,然后抽象出模型,找出问题的本质。

原来的做法,每插入一次就调整一次为的是保证插入过程中每个记录状态都要保持一致性,这个要求太高了,所以导致了O(N^2)的高代价。真正的要求没有那么高,我们只需要开始和结束两个状态的一致性就行了,而这只需要N(O)就可以做到,这个代价就低了。这样这个瓶颈就通过算法来解决了,可以看出算法在性能优化中还是很重要的。

关注者